refactor!: change the entire purpose of this script
This commit is contained in:
parent
217e176975
commit
71d1050adb
20 changed files with 1124 additions and 872 deletions
|
|
@ -1,33 +0,0 @@
|
|||
FROM python:3.14-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
tzdata \
|
||||
xvfb \
|
||||
xauth \
|
||||
ca-certificates \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY pyproject.toml uv.lock /app/
|
||||
RUN pip install --no-cache-dir uv
|
||||
RUN uv sync --frozen --no-dev
|
||||
RUN /app/.venv/bin/python -m playwright install --with-deps chromium
|
||||
|
||||
COPY *.py /app/
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PORT=8000
|
||||
ENV DATA_DIR=/data
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
|
||||
VOLUME ["/data"]
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
STOPSIGNAL SIGINT
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
CMD ["/entrypoint.sh"]
|
||||
108
src/browser.py
Normal file
108
src/browser.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from playwright.async_api import Browser, Playwright
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CHROME_FLAGS = [
|
||||
"--no-startup-window",
|
||||
"--disable-field-trial-config",
|
||||
"--disable-background-networking",
|
||||
"--disable-background-timer-throttling",
|
||||
"--disable-backgrounding-occluded-windows",
|
||||
"--disable-back-forward-cache",
|
||||
"--disable-breakpad",
|
||||
"--disable-client-side-phishing-detection",
|
||||
"--disable-component-extensions-with-background-pages",
|
||||
"--disable-component-update",
|
||||
"--no-default-browser-check",
|
||||
"--disable-default-apps",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-extensions",
|
||||
"--disable-popup-blocking",
|
||||
"--disable-prompt-on-repost",
|
||||
"--disable-renderer-backgrounding",
|
||||
"--disable-hang-monitor",
|
||||
"--disable-ipc-flooding-protection",
|
||||
"--force-color-profile=srgb",
|
||||
"--metrics-recording-only",
|
||||
"--no-first-run",
|
||||
"--password-store=basic",
|
||||
"--use-mock-keychain",
|
||||
"--disable-infobars",
|
||||
"--disable-sync",
|
||||
"--enable-unsafe-swiftshader",
|
||||
"--no-sandbox",
|
||||
"--disable-search-engine-choice-screen",
|
||||
]
|
||||
|
||||
|
||||
def _fetch_ws_endpoint(port: int) -> str | None:
|
||||
try:
|
||||
with urllib.request.urlopen(
|
||||
f"http://127.0.0.1:{port}/json/version",
|
||||
timeout=1,
|
||||
) as resp:
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
return data.get("webSocketDebuggerUrl")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ManagedBrowser:
|
||||
browser: Browser
|
||||
process: subprocess.Popen
|
||||
profile_dir: Path
|
||||
|
||||
async def close(self) -> None:
|
||||
try:
|
||||
await self.browser.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.process.terminate()
|
||||
try:
|
||||
self.process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
if self.profile_dir.exists():
|
||||
shutil.rmtree(self.profile_dir, ignore_errors=True)
|
||||
|
||||
|
||||
async def launch(playwright: Playwright, cdp_port: int | None = None) -> ManagedBrowser:
|
||||
chrome_path = os.environ.get("CHROMIUM_PATH") or playwright.chromium.executable_path
|
||||
cdp_port = cdp_port or int(os.environ.get("CDP_PORT", "9222"))
|
||||
profile_dir = Path(tempfile.mkdtemp(prefix="megapt_profile-", dir="/tmp"))
|
||||
|
||||
args = [
|
||||
chrome_path,
|
||||
*CHROME_FLAGS,
|
||||
f"--user-data-dir={profile_dir}",
|
||||
f"--remote-debugging-port={cdp_port}",
|
||||
]
|
||||
|
||||
proc = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
ws_endpoint = None
|
||||
for _ in range(60):
|
||||
ws_endpoint = await asyncio.to_thread(_fetch_ws_endpoint, cdp_port)
|
||||
if ws_endpoint:
|
||||
break
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
if not ws_endpoint:
|
||||
proc.terminate()
|
||||
raise RuntimeError(f"CDP websocket not available on port {cdp_port}")
|
||||
|
||||
logger.info("CDP websocket: %s", ws_endpoint)
|
||||
browser = await playwright.chromium.connect_over_cdp(ws_endpoint)
|
||||
return ManagedBrowser(browser=browser, process=proc, profile_dir=profile_dir)
|
||||
5
src/email_providers/__init__.py
Normal file
5
src/email_providers/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .base import BaseProvider
|
||||
from .ten_minute_mail import TenMinuteMailProvider
|
||||
from .temp_mail_org import TempMailOrgProvider
|
||||
|
||||
__all__ = ["BaseProvider", "TenMinuteMailProvider", "TempMailOrgProvider"]
|
||||
16
src/email_providers/base.py
Normal file
16
src/email_providers/base.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from abc import ABC, abstractmethod
|
||||
|
||||
from playwright.async_api import BrowserContext
|
||||
|
||||
|
||||
class BaseProvider(ABC):
|
||||
def __init__(self, browser_session: BrowserContext):
|
||||
self.browser_session = browser_session
|
||||
|
||||
@abstractmethod
|
||||
async def get_new_email(self) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_latest_message(self, email: str) -> str | None:
|
||||
pass
|
||||
125
src/email_providers/temp_mail_org.py
Normal file
125
src/email_providers/temp_mail_org.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
|
||||
from playwright.async_api import BrowserContext, Page
|
||||
|
||||
from .base import BaseProvider
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TempMailOrgProvider(BaseProvider):
|
||||
def __init__(self, browser_session: BrowserContext):
|
||||
super().__init__(browser_session)
|
||||
self.page: Page | None = None
|
||||
|
||||
async def _ensure_page(self) -> Page:
|
||||
if self.page is None or self.page.is_closed():
|
||||
self.page = await self.browser_session.new_page()
|
||||
return self.page
|
||||
|
||||
async def get_new_email(self) -> str:
|
||||
page = await self._ensure_page()
|
||||
logger.info("[temp-mail.org] Opening mailbox page")
|
||||
await page.goto("https://temp-mail.org", wait_until="domcontentloaded")
|
||||
await page.locator("input#mail, #mail, input[value*='@']").first.wait_for(
|
||||
state="visible",
|
||||
timeout=30000,
|
||||
)
|
||||
|
||||
selectors = ["#mail", "input#mail", "input[value*='@']"]
|
||||
end_at = asyncio.get_running_loop().time() + 60
|
||||
while asyncio.get_running_loop().time() < end_at:
|
||||
await page.bring_to_front()
|
||||
for selector in selectors:
|
||||
try:
|
||||
field = page.locator(selector).first
|
||||
if await field.is_visible(timeout=1000):
|
||||
value = (await field.input_value()).strip()
|
||||
if "@" in value:
|
||||
logger.info(
|
||||
"[temp-mail.org] selector matched: %s -> %s",
|
||||
selector,
|
||||
value,
|
||||
)
|
||||
return value
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
try:
|
||||
body = await page.inner_text("body")
|
||||
found = extract_email(body)
|
||||
if found:
|
||||
logger.info("[temp-mail.org] email found by body scan: %s", found)
|
||||
return found
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
raise RuntimeError("Could not get temp email from temp-mail.org")
|
||||
|
||||
async def get_latest_message(self, email: str) -> str | None:
|
||||
page = await self._ensure_page()
|
||||
logger.info("[temp-mail.org] Waiting for latest message for %s", email)
|
||||
|
||||
if page.is_closed():
|
||||
raise RuntimeError("temp-mail.org tab was closed unexpectedly")
|
||||
|
||||
await page.bring_to_front()
|
||||
|
||||
items = page.locator("div.inbox-dataList ul li")
|
||||
|
||||
# temp-mail updates inbox via websocket; do not refresh/reload page.
|
||||
for attempt in range(30):
|
||||
try:
|
||||
count = await items.count()
|
||||
logger.info("[temp-mail.org] inbox items: %s", count)
|
||||
except Exception:
|
||||
count = 0
|
||||
|
||||
if count > 0:
|
||||
for idx in reversed(range(count)):
|
||||
try:
|
||||
item = items.nth(idx)
|
||||
if not await item.is_visible(timeout=1000):
|
||||
continue
|
||||
text = (await item.inner_text()).strip().replace("\n", " ")
|
||||
logger.info("[temp-mail.org] item[%s]: %s", idx, text[:160])
|
||||
except Exception:
|
||||
continue
|
||||
if text:
|
||||
try:
|
||||
await item.click()
|
||||
logger.info("[temp-mail.org] opened item[%s]", idx)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
message_text = text
|
||||
try:
|
||||
content = await page.content()
|
||||
if content and "Your ChatGPT code is" in content:
|
||||
message_text = content
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
await page.go_back(
|
||||
wait_until="domcontentloaded", timeout=5000
|
||||
)
|
||||
logger.info("[temp-mail.org] returned back to inbox")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return message_text
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
logger.warning("[temp-mail.org] No messages received within 60 seconds")
|
||||
return None
|
||||
|
||||
|
||||
def extract_email(text: str) -> str | None:
|
||||
match = re.search(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}", text)
|
||||
return match.group(0) if match else None
|
||||
100
src/email_providers/ten_minute_mail.py
Normal file
100
src/email_providers/ten_minute_mail.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import asyncio
|
||||
import logging
|
||||
|
||||
from playwright.async_api import BrowserContext, Page
|
||||
|
||||
from .base import BaseProvider
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TenMinuteMailProvider(BaseProvider):
|
||||
def __init__(self, browser_session: BrowserContext):
|
||||
super().__init__(browser_session)
|
||||
self.page: Page | None = None
|
||||
|
||||
async def _ensure_page(self) -> Page:
|
||||
if self.page is None or self.page.is_closed():
|
||||
self.page = await self.browser_session.new_page()
|
||||
return self.page
|
||||
|
||||
async def get_new_email(self) -> str:
|
||||
page = await self._ensure_page()
|
||||
logger.info("[10min] Opening https://10minutemail.com")
|
||||
await page.goto("https://10minutemail.com", wait_until="domcontentloaded")
|
||||
await page.wait_for_timeout(3000)
|
||||
|
||||
email_input = page.locator("#mail_address")
|
||||
await email_input.first.wait_for(state="visible", timeout=60000)
|
||||
|
||||
email = (await email_input.first.input_value()).strip()
|
||||
if not email or "@" not in email:
|
||||
raise RuntimeError("10MinuteMail did not return a valid email")
|
||||
|
||||
logger.info("[10min] New email acquired: %s", email)
|
||||
return email
|
||||
|
||||
async def get_latest_message(self, email: str) -> str | None:
|
||||
page = await self._ensure_page()
|
||||
logger.info("[10min] Waiting for latest message for %s", email)
|
||||
|
||||
seen_count = 0
|
||||
for attempt in range(60):
|
||||
try:
|
||||
count = await page.evaluate(
|
||||
"""
|
||||
async () => {
|
||||
const response = await fetch('/messages/messageCount', { credentials: 'include' });
|
||||
const data = await response.json();
|
||||
return Number(data.messageCount || 0);
|
||||
}
|
||||
"""
|
||||
)
|
||||
except Exception:
|
||||
count = 0
|
||||
|
||||
if count > 0:
|
||||
if count != seen_count:
|
||||
logger.info("[10min] Inbox has %s message(s)", count)
|
||||
seen_count = count
|
||||
|
||||
try:
|
||||
messages = await page.evaluate(
|
||||
"""
|
||||
async () => {
|
||||
const response = await fetch('/messages/messagesAfter/0', { credentials: 'include' });
|
||||
const data = await response.json();
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
"""
|
||||
)
|
||||
except Exception:
|
||||
messages = []
|
||||
|
||||
text = ""
|
||||
if messages:
|
||||
latest = messages[-1]
|
||||
subject = str(latest.get("subject") or "")
|
||||
sender = str(latest.get("sender") or "")
|
||||
body_plain = str(latest.get("bodyPlainText") or "")
|
||||
body_html = str(latest.get("bodyHtmlContent") or "")
|
||||
text = "\n".join(
|
||||
part
|
||||
for part in [subject, sender, body_plain, body_html]
|
||||
if part
|
||||
)
|
||||
|
||||
if text:
|
||||
logger.info("[10min] Latest message received")
|
||||
return text
|
||||
|
||||
if attempt % 3 == 0:
|
||||
try:
|
||||
await page.reload(wait_until="domcontentloaded", timeout=60000)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
logger.warning("[10min] No messages received within timeout")
|
||||
return None
|
||||
|
|
@ -12,4 +12,4 @@ cleanup() {
|
|||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
exec /app/.venv/bin/python -u proxy.py
|
||||
exec /app/.venv/bin/python -u server.py
|
||||
|
|
|
|||
|
|
@ -1,397 +0,0 @@
|
|||
import asyncio
|
||||
import os
|
||||
import re
|
||||
import random
|
||||
import secrets
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import aiohttp
|
||||
import pkce
|
||||
from urllib.parse import urlencode, urlparse, parse_qs
|
||||
from playwright.async_api import async_playwright, Page, Browser
|
||||
from tokens import DATA_DIR, TOKENS_FILE
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
||||
AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize"
|
||||
TOKEN_URL = "https://auth.openai.com/oauth/token"
|
||||
REDIRECT_URI = "http://localhost:1455/auth/callback"
|
||||
SCOPE = "openid profile email offline_access"
|
||||
|
||||
|
||||
class AutomationError(Exception):
|
||||
def __init__(self, step: str, message: str, page: Page | None = None):
|
||||
self.step = step
|
||||
self.message = message
|
||||
self.page = page
|
||||
super().__init__(f"[{step}] {message}")
|
||||
|
||||
|
||||
async def save_error_screenshot(page: Page | None, step: str):
|
||||
if page:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
screenshots_dir = DATA_DIR / "screenshots"
|
||||
screenshots_dir.mkdir(parents=True, exist_ok=True)
|
||||
filename = screenshots_dir / f"error_{step}_{timestamp}.png"
|
||||
try:
|
||||
await page.screenshot(path=str(filename))
|
||||
logger.error(f"Screenshot saved: {filename}")
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def generate_pkce():
|
||||
return pkce.generate_pkce_pair()
|
||||
|
||||
|
||||
def generate_state():
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
def create_auth_url(verifier: str, challenge: str, state: str) -> str:
|
||||
params = {
|
||||
"response_type": "code",
|
||||
"client_id": CLIENT_ID,
|
||||
"redirect_uri": REDIRECT_URI,
|
||||
"scope": SCOPE,
|
||||
"code_challenge": challenge,
|
||||
"code_challenge_method": "S256",
|
||||
"state": state,
|
||||
"id_token_add_organizations": "true",
|
||||
"codex_cli_simplified_flow": "true",
|
||||
"originator": "opencode",
|
||||
}
|
||||
return f"{AUTHORIZE_URL}?{urlencode(params)}"
|
||||
|
||||
|
||||
async def get_temp_email(page: Page) -> str:
|
||||
logger.info("Getting temp email...")
|
||||
for i in range(30):
|
||||
mail_input = page.locator("#mail")
|
||||
if await mail_input.count() > 0:
|
||||
val = await mail_input.input_value()
|
||||
if val and "@" in val:
|
||||
logger.info(f"Got email: {val}")
|
||||
return val
|
||||
await page.wait_for_timeout(1000)
|
||||
raise AutomationError("get_email", "Failed to get email", page)
|
||||
|
||||
|
||||
async def get_verification_code(page: Page, used_codes: list | None = None) -> str:
|
||||
logger.info("Waiting for verification code...")
|
||||
if used_codes is None:
|
||||
used_codes = []
|
||||
await page.wait_for_timeout(10000)
|
||||
|
||||
for attempt in range(20):
|
||||
mail_items = page.locator(".inbox-dataList ul li")
|
||||
count = await mail_items.count()
|
||||
logger.debug(f"Attempt {attempt + 1}: {count} emails")
|
||||
|
||||
if count > 0:
|
||||
codes = []
|
||||
for i in range(count):
|
||||
try:
|
||||
item = mail_items.nth(i)
|
||||
text = await item.inner_text()
|
||||
match = re.search(
|
||||
r"Your ChatGPT code is (\d{6})", text, re.IGNORECASE
|
||||
)
|
||||
if match:
|
||||
code = match.group(1)
|
||||
if code not in used_codes:
|
||||
codes.append(code)
|
||||
except:
|
||||
pass
|
||||
|
||||
if codes:
|
||||
logger.info(f"Got code: {codes[0]}")
|
||||
return codes[0]
|
||||
|
||||
await page.wait_for_timeout(5000)
|
||||
await page.reload(wait_until="domcontentloaded")
|
||||
await page.wait_for_timeout(5000)
|
||||
|
||||
raise AutomationError("get_code", "Code not found", page)
|
||||
|
||||
|
||||
async def fill_date_field(page: Page, month: str, day: str, year: str):
|
||||
async def type_segment(segment_type: str, value: str):
|
||||
field = page.locator(f'[data-type="{segment_type}"]')
|
||||
if await field.count() == 0:
|
||||
raise AutomationError(
|
||||
"profile", f"Missing birthday segment: {segment_type}", page
|
||||
)
|
||||
|
||||
target = field.first
|
||||
await target.scroll_into_view_if_needed()
|
||||
await target.focus()
|
||||
await page.keyboard.press("Control+A")
|
||||
await page.keyboard.press("Backspace")
|
||||
await page.keyboard.type(value)
|
||||
await page.wait_for_timeout(200)
|
||||
|
||||
await type_segment("month", month)
|
||||
await type_segment("day", day)
|
||||
await type_segment("year", year)
|
||||
|
||||
|
||||
def generate_name():
|
||||
first_names = [
|
||||
"Alex",
|
||||
"Jordan",
|
||||
"Taylor",
|
||||
"Morgan",
|
||||
"Casey",
|
||||
"Riley",
|
||||
"Quinn",
|
||||
"Avery",
|
||||
"Parker",
|
||||
"Blake",
|
||||
]
|
||||
last_names = [
|
||||
"Smith",
|
||||
"Johnson",
|
||||
"Williams",
|
||||
"Brown",
|
||||
"Jones",
|
||||
"Davis",
|
||||
"Miller",
|
||||
"Wilson",
|
||||
"Moore",
|
||||
"Clark",
|
||||
]
|
||||
return f"{random.choice(first_names)} {random.choice(last_names)}"
|
||||
|
||||
|
||||
async def exchange_code_for_tokens(code: str, verifier: str) -> dict:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
data = {
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": CLIENT_ID,
|
||||
"code": code,
|
||||
"code_verifier": verifier,
|
||||
"redirect_uri": REDIRECT_URI,
|
||||
}
|
||||
|
||||
async with session.post(TOKEN_URL, data=data) as resp:
|
||||
if not resp.ok:
|
||||
text = await resp.text()
|
||||
raise Exception(f"Token exchange failed: {resp.status} {text}")
|
||||
|
||||
json_resp = await resp.json()
|
||||
return {
|
||||
"access_token": json_resp["access_token"],
|
||||
"refresh_token": json_resp["refresh_token"],
|
||||
"expires_in": json_resp["expires_in"],
|
||||
}
|
||||
|
||||
|
||||
async def get_new_token(headless: bool = False) -> bool:
|
||||
logger.info("=== Starting token generation ===")
|
||||
|
||||
password = "TempPass123!"
|
||||
full_name = generate_name()
|
||||
birth_month, birth_day, birth_year = "01", "15", "1995"
|
||||
|
||||
verifier, challenge = generate_pkce()
|
||||
state = generate_state()
|
||||
auth_url = create_auth_url(verifier, challenge, state)
|
||||
|
||||
redirect_url_captured = None
|
||||
browser: Browser | None = None
|
||||
current_page: Page | None = None
|
||||
|
||||
try:
|
||||
async with async_playwright() as p:
|
||||
chromium_path = os.environ.get("CHROMIUM_PATH")
|
||||
if chromium_path:
|
||||
browser = await p.chromium.launch(
|
||||
headless=headless,
|
||||
executable_path=chromium_path,
|
||||
)
|
||||
else:
|
||||
browser = await p.chromium.launch(headless=headless)
|
||||
context = await browser.new_context()
|
||||
page = await context.new_page()
|
||||
current_page = page
|
||||
|
||||
logger.info("[1/6] Getting email...")
|
||||
await page.goto("https://temp-mail.org", wait_until="domcontentloaded")
|
||||
email = await get_temp_email(page)
|
||||
tempmail_page = page
|
||||
|
||||
logger.info("[2/6] Registering ChatGPT...")
|
||||
chatgpt_page = await context.new_page()
|
||||
current_page = chatgpt_page
|
||||
await chatgpt_page.goto("https://chatgpt.com")
|
||||
await chatgpt_page.wait_for_load_state("domcontentloaded")
|
||||
|
||||
await chatgpt_page.get_by_text("Sign up for free", exact=True).click()
|
||||
await chatgpt_page.wait_for_timeout(2000)
|
||||
|
||||
await chatgpt_page.locator('input[type="email"]').fill(email)
|
||||
await chatgpt_page.wait_for_timeout(500)
|
||||
await chatgpt_page.get_by_role(
|
||||
"button", name="Continue", exact=True
|
||||
).click()
|
||||
await chatgpt_page.wait_for_timeout(3000)
|
||||
|
||||
await chatgpt_page.locator('input[type="password"]').fill(password)
|
||||
await chatgpt_page.wait_for_timeout(500)
|
||||
await chatgpt_page.get_by_role(
|
||||
"button", name="Continue", exact=True
|
||||
).click()
|
||||
await chatgpt_page.wait_for_timeout(5000)
|
||||
|
||||
logger.info("[3/6] Getting verification code...")
|
||||
await tempmail_page.bring_to_front()
|
||||
code = await get_verification_code(tempmail_page)
|
||||
|
||||
await chatgpt_page.bring_to_front()
|
||||
code_input = chatgpt_page.get_by_placeholder("Code")
|
||||
if await code_input.count() > 0:
|
||||
await code_input.fill(code)
|
||||
await chatgpt_page.wait_for_timeout(5000)
|
||||
|
||||
continue_btn = chatgpt_page.get_by_role(
|
||||
"button", name="Continue", exact=True
|
||||
)
|
||||
if await continue_btn.count() > 0:
|
||||
await continue_btn.click()
|
||||
await chatgpt_page.wait_for_timeout(5000)
|
||||
|
||||
logger.info("[4/6] Setting profile...")
|
||||
name_input = chatgpt_page.get_by_placeholder("Full name")
|
||||
if await name_input.count() > 0:
|
||||
await name_input.fill(full_name)
|
||||
|
||||
await chatgpt_page.wait_for_timeout(500)
|
||||
await fill_date_field(chatgpt_page, birth_month, birth_day, birth_year)
|
||||
await chatgpt_page.wait_for_timeout(1000)
|
||||
|
||||
continue_btn = chatgpt_page.get_by_role(
|
||||
"button", name="Continue", exact=True
|
||||
)
|
||||
if await continue_btn.count() > 0:
|
||||
await continue_btn.click()
|
||||
|
||||
logger.info("Account registered!")
|
||||
await chatgpt_page.wait_for_timeout(10000)
|
||||
await chatgpt_page.wait_for_load_state("networkidle", timeout=30000)
|
||||
await chatgpt_page.wait_for_timeout(5000)
|
||||
|
||||
used_codes = [code]
|
||||
|
||||
logger.info("[5/6] OAuth flow...")
|
||||
oauth_page = await context.new_page()
|
||||
current_page = oauth_page
|
||||
|
||||
def handle_request(request):
|
||||
nonlocal redirect_url_captured
|
||||
url = request.url
|
||||
if "localhost:1455" in url and "code=" in url:
|
||||
logger.info("Redirect URL captured!")
|
||||
redirect_url_captured = url
|
||||
|
||||
oauth_page.on("request", handle_request)
|
||||
|
||||
await oauth_page.goto(auth_url)
|
||||
await oauth_page.wait_for_load_state("domcontentloaded")
|
||||
await oauth_page.wait_for_timeout(3000)
|
||||
|
||||
await oauth_page.locator('input[type="email"], input[name="email"]').fill(
|
||||
email
|
||||
)
|
||||
await oauth_page.wait_for_timeout(500)
|
||||
await oauth_page.get_by_role("button", name="Continue", exact=True).click()
|
||||
await oauth_page.wait_for_timeout(3000)
|
||||
|
||||
password_input = oauth_page.locator('input[type="password"]')
|
||||
if await password_input.count() > 0:
|
||||
await password_input.fill(password)
|
||||
await oauth_page.wait_for_timeout(500)
|
||||
await oauth_page.get_by_role(
|
||||
"button", name="Continue", exact=True
|
||||
).click()
|
||||
await oauth_page.wait_for_timeout(5000)
|
||||
|
||||
await tempmail_page.bring_to_front()
|
||||
await tempmail_page.reload(wait_until="domcontentloaded")
|
||||
await tempmail_page.wait_for_timeout(3000)
|
||||
|
||||
try:
|
||||
oauth_code = await get_verification_code(tempmail_page, used_codes)
|
||||
except AutomationError:
|
||||
logger.info("Reopening mail...")
|
||||
tempmail_page = await context.new_page()
|
||||
current_page = tempmail_page
|
||||
await tempmail_page.goto(
|
||||
"https://temp-mail.org", wait_until="domcontentloaded"
|
||||
)
|
||||
await tempmail_page.wait_for_timeout(10000)
|
||||
oauth_code = await get_verification_code(tempmail_page, used_codes)
|
||||
|
||||
await oauth_page.bring_to_front()
|
||||
code_input = oauth_page.get_by_placeholder("Code")
|
||||
if await code_input.count() > 0:
|
||||
await code_input.fill(oauth_code)
|
||||
await oauth_page.wait_for_timeout(500)
|
||||
await oauth_page.get_by_role(
|
||||
"button", name="Continue", exact=True
|
||||
).click()
|
||||
await oauth_page.wait_for_timeout(5000)
|
||||
|
||||
for btn_text in ["Continue", "Allow", "Authorize"]:
|
||||
btn = oauth_page.get_by_role("button", name=btn_text, exact=True)
|
||||
if await btn.count() > 0:
|
||||
await btn.click()
|
||||
break
|
||||
|
||||
await oauth_page.wait_for_timeout(5000)
|
||||
|
||||
logger.info("[6/6] Exchanging code for tokens...")
|
||||
if redirect_url_captured and "code=" in redirect_url_captured:
|
||||
parsed = urlparse(redirect_url_captured)
|
||||
params = parse_qs(parsed.query)
|
||||
auth_code = params.get("code", [None])[0]
|
||||
|
||||
if auth_code:
|
||||
tokens = await exchange_code_for_tokens(auth_code, verifier)
|
||||
|
||||
token_data = {
|
||||
"access_token": tokens["access_token"],
|
||||
"refresh_token": tokens["refresh_token"],
|
||||
"expires_at": time.time() + tokens["expires_in"],
|
||||
}
|
||||
TOKENS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(TOKENS_FILE, "w") as f:
|
||||
json.dump(token_data, f, indent=2)
|
||||
|
||||
logger.info(f"Tokens saved to {TOKENS_FILE}")
|
||||
return True
|
||||
|
||||
raise AutomationError("token_exchange", "Failed to get tokens", oauth_page)
|
||||
|
||||
except AutomationError as e:
|
||||
logger.error(f"Error at step [{e.step}]: {e.message}")
|
||||
await save_error_screenshot(e.page, e.step)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}")
|
||||
await save_error_screenshot(current_page, "unexpected")
|
||||
return False
|
||||
finally:
|
||||
if browser:
|
||||
await asyncio.sleep(2)
|
||||
await browser.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
success = asyncio.run(get_new_token())
|
||||
exit(0 if success else 1)
|
||||
3
src/providers/__init__.py
Normal file
3
src/providers/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .base import Provider, ProviderTokens
|
||||
|
||||
__all__ = ["Provider", "ProviderTokens"]
|
||||
54
src/providers/base.py
Normal file
54
src/providers/base.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProviderTokens:
|
||||
"""Base token structure for any provider"""
|
||||
|
||||
access_token: str
|
||||
refresh_token: str | None
|
||||
expires_at: float
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
import time
|
||||
|
||||
return time.time() >= self.expires_at - 10
|
||||
|
||||
|
||||
class Provider(ABC):
|
||||
"""Base class for all account providers"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
"""Provider name (e.g., 'chatgpt', 'claude')"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_token(self) -> str | None:
|
||||
"""Get valid access token, refreshing if needed"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def register_new_account(self) -> bool:
|
||||
"""Register a new account and get tokens"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_usage_info(self, access_token: str) -> dict[str, Any]:
|
||||
"""Get usage information for the current token"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def load_tokens(self) -> ProviderTokens | None:
|
||||
"""Load tokens from storage"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_tokens(self, tokens: ProviderTokens) -> None:
|
||||
"""Save tokens to storage"""
|
||||
pass
|
||||
3
src/providers/chatgpt/__init__.py
Normal file
3
src/providers/chatgpt/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .provider import ChatGPTProvider
|
||||
|
||||
__all__ = ["ChatGPTProvider"]
|
||||
68
src/providers/chatgpt/provider.py
Normal file
68
src/providers/chatgpt/provider.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import logging
|
||||
from typing import Callable
|
||||
from typing import Any
|
||||
|
||||
from playwright.async_api import BrowserContext
|
||||
|
||||
from providers.base import Provider, ProviderTokens
|
||||
from email_providers import BaseProvider
|
||||
from email_providers import TempMailOrgProvider
|
||||
from .tokens import load_tokens, save_tokens, get_valid_tokens
|
||||
from .usage import get_usage_percent
|
||||
from .registration import register_chatgpt_account
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChatGPTProvider(Provider):
|
||||
"""ChatGPT account provider"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
email_provider_factory: Callable[[BrowserContext], BaseProvider] | None = None,
|
||||
):
|
||||
self.email_provider_factory = email_provider_factory or TempMailOrgProvider
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "chatgpt"
|
||||
|
||||
async def get_token(self) -> str | None:
|
||||
"""Get valid access token, refreshing if needed"""
|
||||
tokens = await get_valid_tokens()
|
||||
if not tokens:
|
||||
logger.info("No valid tokens, registering new account")
|
||||
success = await self.register_new_account()
|
||||
if not success:
|
||||
return None
|
||||
tokens = await get_valid_tokens()
|
||||
if not tokens:
|
||||
return None
|
||||
return tokens.access_token
|
||||
|
||||
async def register_new_account(self) -> bool:
|
||||
"""Register a new ChatGPT account"""
|
||||
return await register_chatgpt_account(
|
||||
email_provider_factory=self.email_provider_factory,
|
||||
)
|
||||
|
||||
async def get_usage_info(self, access_token: str) -> dict[str, Any]:
|
||||
"""Get usage information for the current token"""
|
||||
usage_percent = get_usage_percent(access_token)
|
||||
if usage_percent < 0:
|
||||
return {"error": "Failed to get usage"}
|
||||
|
||||
remaining = max(0, 100 - usage_percent)
|
||||
return {
|
||||
"used_percent": usage_percent,
|
||||
"remaining_percent": remaining,
|
||||
"exhausted": usage_percent >= 100,
|
||||
}
|
||||
|
||||
def load_tokens(self) -> ProviderTokens | None:
|
||||
"""Load tokens from storage"""
|
||||
return load_tokens()
|
||||
|
||||
def save_tokens(self, tokens: ProviderTokens) -> None:
|
||||
"""Save tokens to storage"""
|
||||
save_tokens(tokens)
|
||||
477
src/providers/chatgpt/registration.py
Normal file
477
src/providers/chatgpt/registration.py
Normal file
|
|
@ -0,0 +1,477 @@
|
|||
import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import secrets
|
||||
import string
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import os
|
||||
from typing import Callable
|
||||
from urllib.parse import parse_qs, urlencode, urlparse
|
||||
|
||||
import aiohttp
|
||||
from playwright.async_api import async_playwright, Page, BrowserContext
|
||||
|
||||
from browser import launch as launch_browser
|
||||
from email_providers import BaseProvider
|
||||
from providers.base import ProviderTokens
|
||||
from .tokens import CLIENT_ID, save_tokens
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATA_DIR = Path(os.environ.get("DATA_DIR", "./data"))
|
||||
AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize"
|
||||
TOKEN_URL = "https://auth.openai.com/oauth/token"
|
||||
REDIRECT_URI = "http://localhost:1455/auth/callback"
|
||||
SCOPE = "openid profile email offline_access"
|
||||
|
||||
|
||||
class AutomationError(Exception):
|
||||
def __init__(self, step: str, message: str, page: Page | None = None):
|
||||
self.step = step
|
||||
self.message = message
|
||||
self.page = page
|
||||
super().__init__(f"[{step}] {message}")
|
||||
|
||||
|
||||
async def save_error_screenshot(page: Page | None, step: str):
|
||||
if page:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
screenshots_dir = DATA_DIR / "screenshots"
|
||||
screenshots_dir.mkdir(parents=True, exist_ok=True)
|
||||
filename = screenshots_dir / f"error_{step}_{timestamp}.png"
|
||||
try:
|
||||
await page.screenshot(path=str(filename))
|
||||
logger.error(f"Screenshot saved: {filename}")
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def generate_password(length: int = 20) -> str:
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return "".join(random.choice(alphabet) for _ in range(length))
|
||||
|
||||
|
||||
def generate_name() -> str:
|
||||
first_names = [
|
||||
"James",
|
||||
"John",
|
||||
"Robert",
|
||||
"Michael",
|
||||
"William",
|
||||
"David",
|
||||
"Richard",
|
||||
"Joseph",
|
||||
"Thomas",
|
||||
"Charles",
|
||||
"Christopher",
|
||||
"Daniel",
|
||||
"Matthew",
|
||||
"Anthony",
|
||||
"Mark",
|
||||
"Donald",
|
||||
"Steven",
|
||||
"Paul",
|
||||
"Andrew",
|
||||
"Joshua",
|
||||
]
|
||||
last_names = [
|
||||
"Smith",
|
||||
"Johnson",
|
||||
"Williams",
|
||||
"Brown",
|
||||
"Jones",
|
||||
"Garcia",
|
||||
"Miller",
|
||||
"Davis",
|
||||
"Rodriguez",
|
||||
"Martinez",
|
||||
"Hernandez",
|
||||
"Lopez",
|
||||
"Gonzalez",
|
||||
"Wilson",
|
||||
"Anderson",
|
||||
"Thomas",
|
||||
"Taylor",
|
||||
"Moore",
|
||||
"Jackson",
|
||||
"Martin",
|
||||
]
|
||||
return f"{random.choice(first_names)} {random.choice(last_names)}"
|
||||
|
||||
|
||||
def extract_verification_code(message: str) -> str | None:
|
||||
normalized = re.sub(r"\s+", " ", message)
|
||||
|
||||
preferred = re.search(
|
||||
r"Your\s+ChatGPT\s+code\s+is\s*(\d{6})",
|
||||
normalized,
|
||||
re.IGNORECASE,
|
||||
)
|
||||
if preferred:
|
||||
return preferred.group(1)
|
||||
|
||||
openai_otp = re.search(r"OpenAI\s+otp.*?(\d{6})", normalized, re.IGNORECASE)
|
||||
if openai_otp:
|
||||
return openai_otp.group(1)
|
||||
|
||||
all_codes = re.findall(r"\b(\d{6})\b", normalized)
|
||||
if all_codes:
|
||||
return all_codes[-1]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def generate_pkce_pair() -> tuple[str, str]:
|
||||
verifier = secrets.token_urlsafe(64)
|
||||
digest = hashlib.sha256(verifier.encode("utf-8")).digest()
|
||||
challenge = base64.urlsafe_b64encode(digest).decode("utf-8").rstrip("=")
|
||||
return verifier, challenge
|
||||
|
||||
|
||||
def generate_state() -> str:
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
def build_authorize_url(verifier: str, challenge: str, state: str) -> str:
|
||||
del verifier
|
||||
params = {
|
||||
"response_type": "code",
|
||||
"client_id": CLIENT_ID,
|
||||
"redirect_uri": REDIRECT_URI,
|
||||
"scope": SCOPE,
|
||||
"code_challenge": challenge,
|
||||
"code_challenge_method": "S256",
|
||||
"id_token_add_organizations": "true",
|
||||
"codex_cli_simplified_flow": "true",
|
||||
"state": state,
|
||||
"originator": "opencode",
|
||||
}
|
||||
return f"{AUTHORIZE_URL}?{urlencode(params)}"
|
||||
|
||||
|
||||
async def exchange_code_for_tokens(code: str, verifier: str) -> ProviderTokens:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
payload = {
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": CLIENT_ID,
|
||||
"code": code,
|
||||
"code_verifier": verifier,
|
||||
"redirect_uri": REDIRECT_URI,
|
||||
}
|
||||
async with session.post(TOKEN_URL, data=payload) as resp:
|
||||
if not resp.ok:
|
||||
text = await resp.text()
|
||||
raise RuntimeError(f"Token exchange failed: {resp.status} {text}")
|
||||
body = await resp.json()
|
||||
|
||||
expires_in = int(body["expires_in"])
|
||||
return ProviderTokens(
|
||||
access_token=body["access_token"],
|
||||
refresh_token=body["refresh_token"],
|
||||
expires_at=time.time() + expires_in,
|
||||
)
|
||||
|
||||
|
||||
async def get_new_verification_code(
|
||||
email_provider: BaseProvider,
|
||||
email: str,
|
||||
used_codes: set[str],
|
||||
timeout_seconds: int = 240,
|
||||
) -> str | None:
|
||||
attempts = max(1, timeout_seconds // 5)
|
||||
for _ in range(attempts):
|
||||
message = await email_provider.get_latest_message(email)
|
||||
if message:
|
||||
all_codes = re.findall(r"\b(\d{6})\b", message)
|
||||
for candidate in all_codes:
|
||||
if candidate not in used_codes:
|
||||
return candidate
|
||||
await asyncio.sleep(5)
|
||||
return None
|
||||
|
||||
|
||||
async def get_latest_code(email_provider: BaseProvider, email: str) -> str | None:
|
||||
message = await email_provider.get_latest_message(email)
|
||||
if not message:
|
||||
return None
|
||||
return extract_verification_code(message)
|
||||
|
||||
|
||||
async def fill_date_field(page: Page, month: str, day: str, year: str):
|
||||
month_field = page.locator('[data-type="month"]').first
|
||||
if await month_field.count() == 0:
|
||||
raise AutomationError("profile", "Missing birthday month field", page)
|
||||
|
||||
await month_field.scroll_into_view_if_needed()
|
||||
await month_field.click()
|
||||
await page.wait_for_timeout(120)
|
||||
|
||||
await page.keyboard.type(f"{month}{day}{year}")
|
||||
await page.wait_for_timeout(200)
|
||||
|
||||
|
||||
async def wait_for_signup_stabilization(page: Page):
|
||||
try:
|
||||
await page.wait_for_load_state("networkidle", timeout=15000)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Signup page did not reach networkidle quickly; continuing with fallback"
|
||||
)
|
||||
try:
|
||||
await page.wait_for_load_state("domcontentloaded", timeout=10000)
|
||||
except Exception:
|
||||
pass
|
||||
await page.wait_for_timeout(3000)
|
||||
|
||||
|
||||
async def register_chatgpt_account(
|
||||
email_provider_factory: Callable[[BrowserContext], BaseProvider] | None = None,
|
||||
) -> bool:
|
||||
logger.info("=== Starting ChatGPT account registration ===")
|
||||
|
||||
if email_provider_factory is None:
|
||||
logger.error("No email provider factory configured")
|
||||
return False
|
||||
|
||||
birth_month, birth_day, birth_year = "01", "15", "1995"
|
||||
|
||||
current_page: Page | None = None
|
||||
redirect_url_captured: str | None = None
|
||||
managed = None
|
||||
|
||||
try:
|
||||
async with async_playwright() as p:
|
||||
managed = await launch_browser(p)
|
||||
browser = managed.browser
|
||||
context = (
|
||||
browser.contexts[0] if browser.contexts else await browser.new_context()
|
||||
)
|
||||
email_provider = email_provider_factory(context)
|
||||
|
||||
logger.info("[1/6] Getting new email from configured provider...")
|
||||
email = await email_provider.get_new_email()
|
||||
if not email:
|
||||
raise AutomationError(
|
||||
"email_provider", "Email provider returned empty email"
|
||||
)
|
||||
|
||||
password = generate_password()
|
||||
full_name = generate_name()
|
||||
verifier, challenge = generate_pkce_pair()
|
||||
oauth_state = generate_state()
|
||||
authorize_url = build_authorize_url(verifier, challenge, oauth_state)
|
||||
|
||||
logger.info("[2/6] Registering ChatGPT for %s", email)
|
||||
chatgpt_page = await context.new_page()
|
||||
current_page = chatgpt_page
|
||||
await chatgpt_page.goto("https://chatgpt.com")
|
||||
await chatgpt_page.wait_for_load_state("domcontentloaded")
|
||||
|
||||
await chatgpt_page.get_by_text("Sign up for free", exact=True).click()
|
||||
await chatgpt_page.wait_for_timeout(2000)
|
||||
|
||||
await chatgpt_page.locator('input[type="email"]').fill(email)
|
||||
await chatgpt_page.wait_for_timeout(500)
|
||||
await chatgpt_page.get_by_role(
|
||||
"button", name="Continue", exact=True
|
||||
).click()
|
||||
await chatgpt_page.wait_for_timeout(3000)
|
||||
|
||||
await chatgpt_page.locator('input[type="password"]').fill(password)
|
||||
await chatgpt_page.wait_for_timeout(500)
|
||||
await chatgpt_page.get_by_role(
|
||||
"button", name="Continue", exact=True
|
||||
).click()
|
||||
await chatgpt_page.wait_for_timeout(5000)
|
||||
|
||||
logger.info("[3/6] Getting verification message from email provider...")
|
||||
code = await get_latest_code(email_provider, email)
|
||||
if not code:
|
||||
raise AutomationError(
|
||||
"email_provider", "Email provider returned no verification message"
|
||||
)
|
||||
logger.info("[3/6] Verification code extracted: %s", code)
|
||||
used_codes = {code}
|
||||
|
||||
await chatgpt_page.bring_to_front()
|
||||
code_input = chatgpt_page.get_by_placeholder("Code")
|
||||
if await code_input.count() > 0:
|
||||
await code_input.fill(code)
|
||||
await chatgpt_page.wait_for_timeout(5000)
|
||||
|
||||
continue_btn = chatgpt_page.get_by_role(
|
||||
"button", name="Continue", exact=True
|
||||
)
|
||||
if await continue_btn.count() > 0:
|
||||
await continue_btn.click()
|
||||
await chatgpt_page.wait_for_timeout(5000)
|
||||
|
||||
logger.info("[4/6] Setting profile...")
|
||||
name_input = chatgpt_page.get_by_placeholder("Full name")
|
||||
if await name_input.count() > 0:
|
||||
await name_input.fill(full_name)
|
||||
|
||||
await chatgpt_page.wait_for_timeout(500)
|
||||
await fill_date_field(chatgpt_page, birth_month, birth_day, birth_year)
|
||||
await chatgpt_page.wait_for_timeout(1000)
|
||||
|
||||
continue_btn = chatgpt_page.get_by_role(
|
||||
"button", name="Continue", exact=True
|
||||
)
|
||||
if await continue_btn.count() > 0:
|
||||
await continue_btn.click()
|
||||
|
||||
logger.info("Account registered!")
|
||||
await wait_for_signup_stabilization(chatgpt_page)
|
||||
|
||||
logger.info("[5/6] Skipping onboarding...")
|
||||
|
||||
for _ in range(5):
|
||||
skip_btn = chatgpt_page.locator(
|
||||
'button:has-text("Skip"):not(:has-text("Skip Tour"))'
|
||||
)
|
||||
if await skip_btn.count() > 0:
|
||||
for i in range(await skip_btn.count()):
|
||||
try:
|
||||
btn = skip_btn.nth(i)
|
||||
if await btn.is_visible():
|
||||
await btn.click(timeout=5000)
|
||||
logger.info("Clicked: Skip")
|
||||
await chatgpt_page.wait_for_timeout(1500)
|
||||
except:
|
||||
pass
|
||||
await chatgpt_page.wait_for_timeout(1000)
|
||||
|
||||
skip_tour = chatgpt_page.locator('button:has-text("Skip Tour")')
|
||||
if await skip_tour.count() > 0:
|
||||
try:
|
||||
await skip_tour.first.wait_for(state="visible", timeout=5000)
|
||||
await skip_tour.first.click(timeout=5000)
|
||||
logger.info("Clicked: Skip Tour")
|
||||
await chatgpt_page.wait_for_timeout(2000)
|
||||
except:
|
||||
pass
|
||||
|
||||
await chatgpt_page.wait_for_timeout(2000)
|
||||
|
||||
for _ in range(3):
|
||||
continue_btn = chatgpt_page.locator('button:has-text("Continue")')
|
||||
if await continue_btn.count() > 0:
|
||||
try:
|
||||
await continue_btn.first.wait_for(state="visible", timeout=5000)
|
||||
await continue_btn.first.click(timeout=5000)
|
||||
logger.info("Clicked: Continue")
|
||||
await chatgpt_page.wait_for_timeout(2000)
|
||||
except:
|
||||
pass
|
||||
|
||||
await chatgpt_page.wait_for_timeout(2000)
|
||||
|
||||
okay_btn = chatgpt_page.locator('button:has-text("Okay, let")')
|
||||
for _ in range(10):
|
||||
try:
|
||||
await okay_btn.first.wait_for(state="visible", timeout=3000)
|
||||
await okay_btn.first.click(timeout=5000)
|
||||
logger.info("Clicked: Okay, let's go")
|
||||
await chatgpt_page.wait_for_timeout(3000)
|
||||
break
|
||||
except:
|
||||
await chatgpt_page.wait_for_timeout(1000)
|
||||
|
||||
logger.info("Skipping subscription/card flow (disabled)")
|
||||
await chatgpt_page.wait_for_timeout(2000)
|
||||
|
||||
logger.info("[6/6] Running OAuth flow to get tokens...")
|
||||
oauth_page = await context.new_page()
|
||||
current_page = oauth_page
|
||||
|
||||
def handle_request(request):
|
||||
nonlocal redirect_url_captured
|
||||
url = request.url
|
||||
if "localhost:1455" in url and "code=" in url:
|
||||
redirect_url_captured = url
|
||||
logger.info("Captured OAuth redirect URL")
|
||||
|
||||
oauth_page.on("request", handle_request)
|
||||
|
||||
await oauth_page.goto(authorize_url, wait_until="domcontentloaded")
|
||||
await oauth_page.wait_for_timeout(2000)
|
||||
|
||||
email_input = oauth_page.locator('input[type="email"], input[name="email"]')
|
||||
if await email_input.count() > 0:
|
||||
await email_input.first.fill(email)
|
||||
await oauth_page.wait_for_timeout(400)
|
||||
|
||||
continue_button = oauth_page.get_by_role("button", name="Continue")
|
||||
if await continue_button.count() > 0:
|
||||
await continue_button.first.click()
|
||||
await oauth_page.wait_for_timeout(2500)
|
||||
|
||||
password_input = oauth_page.locator('input[type="password"]')
|
||||
if await password_input.count() > 0:
|
||||
await password_input.first.fill(password)
|
||||
await oauth_page.wait_for_timeout(400)
|
||||
continue_button = oauth_page.get_by_role("button", name="Continue")
|
||||
if await continue_button.count() > 0:
|
||||
await continue_button.first.click()
|
||||
await oauth_page.wait_for_timeout(2500)
|
||||
|
||||
for label in ["Continue", "Allow", "Authorize"]:
|
||||
button = oauth_page.get_by_role("button", name=label)
|
||||
if await button.count() > 0:
|
||||
try:
|
||||
await button.first.click(timeout=5000)
|
||||
await oauth_page.wait_for_timeout(2000)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not redirect_url_captured:
|
||||
try:
|
||||
await oauth_page.wait_for_timeout(4000)
|
||||
current_url = oauth_page.url
|
||||
if "localhost:1455" in current_url and "code=" in current_url:
|
||||
redirect_url_captured = current_url
|
||||
logger.info("Captured OAuth redirect from page URL")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not redirect_url_captured:
|
||||
raise AutomationError(
|
||||
"oauth", "OAuth redirect with code was not captured", oauth_page
|
||||
)
|
||||
|
||||
parsed = urlparse(redirect_url_captured)
|
||||
params = parse_qs(parsed.query)
|
||||
auth_code = params.get("code", [None])[0]
|
||||
returned_state = params.get("state", [None])[0]
|
||||
|
||||
if not auth_code:
|
||||
raise AutomationError(
|
||||
"oauth", "OAuth code missing in redirect", oauth_page
|
||||
)
|
||||
if returned_state != oauth_state:
|
||||
raise AutomationError("oauth", "OAuth state mismatch", oauth_page)
|
||||
|
||||
tokens = await exchange_code_for_tokens(auth_code, verifier)
|
||||
save_tokens(tokens)
|
||||
logger.info("OAuth tokens saved successfully")
|
||||
|
||||
return True
|
||||
|
||||
except AutomationError as e:
|
||||
logger.error(f"Error at step [{e.step}]: {e.message}")
|
||||
await save_error_screenshot(e.page, e.step)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}")
|
||||
await save_error_screenshot(current_page, "unexpected")
|
||||
return False
|
||||
finally:
|
||||
if managed:
|
||||
await asyncio.sleep(2)
|
||||
await managed.close()
|
||||
|
|
@ -1,46 +1,34 @@
|
|||
import json
|
||||
import time
|
||||
import os
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
import aiohttp
|
||||
from pathlib import Path
|
||||
|
||||
from providers.base import ProviderTokens
|
||||
|
||||
DATA_DIR = Path(os.environ.get("DATA_DIR", "./data"))
|
||||
TOKENS_FILE = DATA_DIR / "tokens.json"
|
||||
TOKENS_FILE = DATA_DIR / "chatgpt_tokens.json"
|
||||
|
||||
CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
||||
TOKEN_URL = "https://auth.openai.com/oauth/token"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Tokens:
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
expires_at: float # unix timestamp
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
return time.time() >= self.expires_at - 10
|
||||
|
||||
|
||||
def load_tokens() -> Tokens | None:
|
||||
def load_tokens() -> ProviderTokens | None:
|
||||
if not TOKENS_FILE.exists():
|
||||
return None
|
||||
try:
|
||||
with open(TOKENS_FILE) as f:
|
||||
data = json.load(f)
|
||||
access_token = data["access_token"]
|
||||
return Tokens(
|
||||
access_token=access_token,
|
||||
return ProviderTokens(
|
||||
access_token=data["access_token"],
|
||||
refresh_token=data["refresh_token"],
|
||||
expires_at=data["expires_at"],
|
||||
)
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
except json.JSONDecodeError, KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def save_tokens(tokens: Tokens):
|
||||
def save_tokens(tokens: ProviderTokens):
|
||||
TOKENS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(TOKENS_FILE, "w") as f:
|
||||
json.dump(
|
||||
|
|
@ -54,7 +42,7 @@ def save_tokens(tokens: Tokens):
|
|||
)
|
||||
|
||||
|
||||
async def refresh_tokens(refresh_token: str) -> Tokens | None:
|
||||
async def refresh_tokens(refresh_token: str) -> ProviderTokens | None:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
data = {
|
||||
"grant_type": "refresh_token",
|
||||
|
|
@ -68,14 +56,14 @@ async def refresh_tokens(refresh_token: str) -> Tokens | None:
|
|||
return None
|
||||
json_resp = await resp.json()
|
||||
expires_in = json_resp["expires_in"]
|
||||
return Tokens(
|
||||
return ProviderTokens(
|
||||
access_token=json_resp["access_token"],
|
||||
refresh_token=json_resp["refresh_token"],
|
||||
expires_at=time.time() + expires_in,
|
||||
)
|
||||
|
||||
|
||||
async def get_valid_tokens() -> Tokens | None:
|
||||
async def get_valid_tokens() -> ProviderTokens | None:
|
||||
tokens = load_tokens()
|
||||
if not tokens:
|
||||
print("No tokens found")
|
||||
|
|
@ -8,7 +8,7 @@ from typing import Any
|
|||
def clamp_percent(value: Any) -> int:
|
||||
try:
|
||||
num = float(value)
|
||||
except (TypeError, ValueError):
|
||||
except TypeError, ValueError:
|
||||
return 0
|
||||
if num < 0:
|
||||
return 0
|
||||
|
|
@ -35,7 +35,7 @@ def get_usage_percent(access_token: str, timeout_ms: int = 10000) -> int:
|
|||
body = res.read().decode("utf-8", errors="replace")
|
||||
except urllib.error.HTTPError as e:
|
||||
return -1
|
||||
except (urllib.error.URLError, socket.timeout):
|
||||
except urllib.error.URLError, socket.timeout:
|
||||
return -1
|
||||
|
||||
try:
|
||||
|
|
@ -48,14 +48,3 @@ def get_usage_percent(access_token: str, timeout_ms: int = 10000) -> int:
|
|||
return clamp_percent(primary.get("used_percent") or 0)
|
||||
|
||||
return -1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from tokens import load_tokens
|
||||
|
||||
tokens = load_tokens()
|
||||
if tokens:
|
||||
usage = get_usage_percent(tokens.access_token)
|
||||
print(f"{usage}%")
|
||||
else:
|
||||
print("No tokens")
|
||||
436
src/proxy.py
436
src/proxy.py
|
|
@ -1,436 +0,0 @@
|
|||
import os
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
import secrets
|
||||
import json
|
||||
import base64
|
||||
import uuid
|
||||
from urllib.parse import urlencode
|
||||
from aiohttp import web
|
||||
import aiohttp
|
||||
|
||||
from tokens import get_valid_tokens, load_tokens, DATA_DIR
|
||||
from codex_usage import get_usage_percent
|
||||
from get_new_token import get_new_token
|
||||
|
||||
CODEX_BASE_URL = "https://chatgpt.com/backend-api"
|
||||
PORT = int(os.environ.get("PORT", "8080"))
|
||||
USAGE_THRESHOLD = int(os.environ.get("USAGE_THRESHOLD", "85"))
|
||||
CHECK_INTERVAL = int(os.environ.get("CHECK_INTERVAL", "60"))
|
||||
FAKE_EXPIRES_IN = 9999999999999
|
||||
AUTH_FILE = DATA_DIR / "auth.json"
|
||||
JWT_AUTH_CLAIM_PATH = "https://api.openai.com/auth"
|
||||
JWT_PROFILE_CLAIM_PATH = "https://api.openai.com/profile"
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
refresh_in_progress = False
|
||||
auth_codes: dict[str, dict] = {}
|
||||
|
||||
|
||||
def _b64url(data: bytes) -> str:
|
||||
return base64.urlsafe_b64encode(data).decode("utf-8").rstrip("=")
|
||||
|
||||
|
||||
def _generate_jwt_like() -> str:
|
||||
account_id = str(uuid.uuid4())
|
||||
now = int(time.time())
|
||||
header = {"alg": "HS256", "typ": "JWT"}
|
||||
user_id = f"user-{secrets.token_urlsafe(18)}"
|
||||
account_user_id = f"{user_id}__{account_id}"
|
||||
payload = {
|
||||
"aud": ["https://api.openai.com/v1"],
|
||||
"client_id": "app_EMoamEEZ73f0CkXaXp7hrann",
|
||||
"iss": "https://auth.openai.com",
|
||||
"iat": now,
|
||||
"nbf": now,
|
||||
"exp": now + 315360000,
|
||||
"jti": str(uuid.uuid4()),
|
||||
"scp": ["openid", "profile", "email", "offline_access"],
|
||||
"session_id": f"authsess_{secrets.token_urlsafe(24)}",
|
||||
JWT_AUTH_CLAIM_PATH: {
|
||||
"chatgpt_account_id": account_id,
|
||||
"chatgpt_account_user_id": account_user_id,
|
||||
"chatgpt_compute_residency": "no_constraint",
|
||||
"chatgpt_plan_type": "plus",
|
||||
"chatgpt_user_id": user_id,
|
||||
"user_id": user_id,
|
||||
},
|
||||
JWT_PROFILE_CLAIM_PATH: {
|
||||
"email": f"proxy-{secrets.token_hex(4)}@example.local",
|
||||
"email_verified": True,
|
||||
},
|
||||
"sub": f"auth0|{secrets.token_urlsafe(20)}",
|
||||
}
|
||||
head = _b64url(json.dumps(header, separators=(",", ":")).encode("utf-8"))
|
||||
body = _b64url(json.dumps(payload, separators=(",", ":")).encode("utf-8"))
|
||||
sign = _b64url(secrets.token_bytes(32))
|
||||
return f"{head}.{body}.{sign}"
|
||||
|
||||
|
||||
def _generate_refresh_like() -> str:
|
||||
return f"rt_{secrets.token_urlsafe(40)}.{secrets.token_urlsafe(32)}"
|
||||
|
||||
|
||||
def _mask(value: str, head: int = 8, tail: int = 6) -> str:
|
||||
if not value:
|
||||
return "<empty>"
|
||||
if len(value) <= head + tail:
|
||||
return "<hidden>"
|
||||
return f"{value[:head]}...{value[-tail:]}"
|
||||
|
||||
|
||||
def load_or_create_auth() -> dict:
|
||||
if AUTH_FILE.exists():
|
||||
with open(AUTH_FILE) as f:
|
||||
data = json.load(f)
|
||||
if (
|
||||
data.get("access_token")
|
||||
and data.get("refresh_token")
|
||||
and data.get("expires_at")
|
||||
):
|
||||
return data
|
||||
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
access_token = _generate_jwt_like()
|
||||
|
||||
data = {
|
||||
"access_token": access_token,
|
||||
"refresh_token": _generate_refresh_like(),
|
||||
"expires_at": FAKE_EXPIRES_IN,
|
||||
}
|
||||
with open(AUTH_FILE, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
return data
|
||||
|
||||
|
||||
@web.middleware
|
||||
async def request_log_middleware(request: web.Request, handler):
|
||||
started = time.perf_counter()
|
||||
response = None
|
||||
try:
|
||||
response = await handler(request)
|
||||
return response
|
||||
finally:
|
||||
elapsed_ms = int((time.perf_counter() - started) * 1000)
|
||||
status = getattr(response, "status", "ERR")
|
||||
logger.info(
|
||||
"%s %s -> %s (%d ms)",
|
||||
request.method,
|
||||
request.path_qs,
|
||||
status,
|
||||
elapsed_ms,
|
||||
)
|
||||
|
||||
|
||||
def check_auth(request: web.Request) -> bool:
|
||||
auth_data = load_or_create_auth()
|
||||
expected_token = auth_data["access_token"]
|
||||
auth = request.headers.get("Authorization", "")
|
||||
if auth.lower().startswith("bearer "):
|
||||
token = auth[7:].strip()
|
||||
return token == expected_token
|
||||
return False
|
||||
|
||||
|
||||
async def oauth_authorize_handler(request: web.Request) -> web.Response:
|
||||
params = request.rel_url.query
|
||||
redirect_uri = params.get("redirect_uri")
|
||||
state = params.get("state", "")
|
||||
|
||||
if not redirect_uri:
|
||||
return web.json_response(
|
||||
{"error": "invalid_request", "error_description": "Missing redirect_uri"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
code = f"ac_{secrets.token_urlsafe(48)}"
|
||||
auth_codes[code] = {
|
||||
"state": state,
|
||||
"created_at": time.time(),
|
||||
}
|
||||
|
||||
query = urlencode(
|
||||
{
|
||||
"code": code,
|
||||
"scope": "openid profile email offline_access",
|
||||
"state": state,
|
||||
}
|
||||
)
|
||||
location = f"{redirect_uri}?{query}"
|
||||
logger.info("OAuth authorize: issued code")
|
||||
raise web.HTTPFound(location=location)
|
||||
|
||||
|
||||
async def oauth_token_handler(request: web.Request) -> web.Response:
|
||||
auth_data = load_or_create_auth()
|
||||
|
||||
content_type = request.content_type or ""
|
||||
grant_type = None
|
||||
refresh_token = None
|
||||
code = None
|
||||
if content_type.startswith("application/json"):
|
||||
body = await request.json()
|
||||
grant_type = body.get("grant_type")
|
||||
refresh_token = body.get("refresh_token")
|
||||
code = body.get("code")
|
||||
else:
|
||||
form = await request.post()
|
||||
grant_type = form.get("grant_type")
|
||||
refresh_token = form.get("refresh_token")
|
||||
code = form.get("code")
|
||||
|
||||
if grant_type == "authorization_code":
|
||||
code = str(code) if code else ""
|
||||
if not code or code not in auth_codes:
|
||||
return web.json_response(
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Invalid authorization code",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
created_at = auth_codes[code]["created_at"]
|
||||
del auth_codes[code]
|
||||
if time.time() - created_at > 300:
|
||||
return web.json_response(
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Authorization code expired",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
"access_token": auth_data["access_token"],
|
||||
"refresh_token": auth_data["refresh_token"],
|
||||
"token_type": "Bearer",
|
||||
"expires_in": FAKE_EXPIRES_IN,
|
||||
}
|
||||
)
|
||||
|
||||
if grant_type == "refresh_token":
|
||||
if refresh_token != auth_data["refresh_token"]:
|
||||
return web.json_response(
|
||||
{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Invalid refresh token",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
"access_token": auth_data["access_token"],
|
||||
"refresh_token": auth_data["refresh_token"],
|
||||
"token_type": "Bearer",
|
||||
"expires_in": FAKE_EXPIRES_IN,
|
||||
}
|
||||
)
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
"error": "unsupported_grant_type",
|
||||
"error_description": "Only authorization_code and refresh_token are supported",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
|
||||
async def refresh_tokens_task():
|
||||
global refresh_in_progress
|
||||
if refresh_in_progress:
|
||||
logger.info("Token refresh already in progress")
|
||||
return
|
||||
|
||||
refresh_in_progress = True
|
||||
logger.info("Starting token refresh...")
|
||||
|
||||
try:
|
||||
success = await get_new_token(headless=False)
|
||||
if success:
|
||||
logger.info("Token refresh completed successfully")
|
||||
else:
|
||||
logger.error("Token refresh failed")
|
||||
except Exception as e:
|
||||
logger.error(f"Error during token refresh: {e}")
|
||||
finally:
|
||||
refresh_in_progress = False
|
||||
|
||||
|
||||
async def usage_monitor():
|
||||
while True:
|
||||
for _ in range(1):
|
||||
tokens = load_tokens()
|
||||
|
||||
if not tokens:
|
||||
if not refresh_in_progress:
|
||||
logger.warning("No tokens found, starting refresh...")
|
||||
asyncio.create_task(refresh_tokens_task())
|
||||
break
|
||||
|
||||
usage = get_usage_percent(tokens.access_token)
|
||||
|
||||
if usage < 0:
|
||||
logger.warning("Failed to get usage, token may be invalid")
|
||||
asyncio.create_task(refresh_tokens_task())
|
||||
break
|
||||
|
||||
logger.info(f"Current usage: {usage}%")
|
||||
|
||||
if usage >= USAGE_THRESHOLD:
|
||||
logger.info(
|
||||
f"Usage {usage}% >= threshold {USAGE_THRESHOLD}%, starting refresh..."
|
||||
)
|
||||
asyncio.create_task(refresh_tokens_task())
|
||||
break
|
||||
|
||||
await asyncio.sleep(CHECK_INTERVAL)
|
||||
|
||||
|
||||
async def proxy_handler(request: web.Request) -> web.StreamResponse | web.Response:
|
||||
if not check_auth(request):
|
||||
auth = request.headers.get("Authorization", "")
|
||||
auth_preview = auth[:24] + ("..." if len(auth) > 24 else "")
|
||||
logger.warning(
|
||||
"Auth failed: method=%s path=%s auth_present=%s auth_preview=%s ua=%s",
|
||||
request.method,
|
||||
request.path,
|
||||
bool(auth),
|
||||
auth_preview,
|
||||
request.headers.get("User-Agent", ""),
|
||||
)
|
||||
return web.json_response({"error": "Unauthorized"}, status=401)
|
||||
|
||||
tokens = await get_valid_tokens()
|
||||
if not tokens:
|
||||
return web.json_response({"error": "No valid tokens"}, status=500)
|
||||
|
||||
path = request.path
|
||||
target_url = f"{CODEX_BASE_URL}{path}"
|
||||
logger.info(
|
||||
"Proxying request: %s %s -> %s",
|
||||
request.method,
|
||||
request.path_qs,
|
||||
target_url,
|
||||
)
|
||||
|
||||
headers = {}
|
||||
for key, value in request.headers.items():
|
||||
if key.lower() not in ("host", "authorization", "content-length"):
|
||||
headers[key] = value
|
||||
headers["Authorization"] = f"Bearer {tokens.access_token}"
|
||||
|
||||
if request.method in ("POST", "PUT", "PATCH"):
|
||||
body = await request.read()
|
||||
else:
|
||||
body = None
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
async with session.request(
|
||||
method=request.method,
|
||||
url=target_url,
|
||||
headers=headers,
|
||||
data=body,
|
||||
params=request.query,
|
||||
) as resp:
|
||||
content_type = resp.content_type or "application/json"
|
||||
is_stream = (
|
||||
content_type == "text/event-stream" or "stream" in content_type
|
||||
)
|
||||
|
||||
if is_stream:
|
||||
response = web.StreamResponse(
|
||||
status=resp.status,
|
||||
reason=resp.reason,
|
||||
headers={
|
||||
"Content-Type": content_type,
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
},
|
||||
)
|
||||
await response.prepare(request)
|
||||
|
||||
async for chunk in resp.content.iter_any():
|
||||
await response.write(chunk)
|
||||
|
||||
await response.write_eof()
|
||||
return response
|
||||
else:
|
||||
response_body = await resp.read()
|
||||
if resp.status >= 400:
|
||||
preview = response_body[:500].decode("utf-8", errors="replace")
|
||||
logger.warning(
|
||||
"Upstream error: status=%s path=%s body=%s",
|
||||
resp.status,
|
||||
request.path,
|
||||
preview,
|
||||
)
|
||||
return web.Response(
|
||||
status=resp.status,
|
||||
body=response_body,
|
||||
headers={"Content-Type": content_type},
|
||||
)
|
||||
except aiohttp.ClientError as e:
|
||||
return web.json_response({"error": f"Proxy error: {e}"}, status=502)
|
||||
|
||||
|
||||
async def health_handler(request: web.Request) -> web.Response:
|
||||
tokens = await get_valid_tokens()
|
||||
usage = -1
|
||||
if tokens:
|
||||
usage = get_usage_percent(tokens.access_token)
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
"status": "ok" if tokens else "no_tokens",
|
||||
"has_tokens": tokens is not None,
|
||||
"usage_percent": usage,
|
||||
"refresh_in_progress": refresh_in_progress,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def start_background_tasks(app: web.Application):
|
||||
app["usage_monitor"] = asyncio.create_task(usage_monitor())
|
||||
|
||||
|
||||
async def cleanup_background_tasks(app: web.Application):
|
||||
app["usage_monitor"].cancel()
|
||||
try:
|
||||
await app["usage_monitor"]
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
|
||||
def create_app() -> web.Application:
|
||||
app = web.Application(middlewares=[request_log_middleware])
|
||||
app.router.add_get("/oauth/authorize", oauth_authorize_handler)
|
||||
app.router.add_post("/oauth/token", oauth_token_handler)
|
||||
app.router.add_get("/health", health_handler)
|
||||
app.router.add_route("*", "/{path:.*}", proxy_handler)
|
||||
app.on_startup.append(start_background_tasks)
|
||||
app.on_cleanup.append(cleanup_background_tasks)
|
||||
return app
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info(f"Starting proxy on port {PORT}")
|
||||
logger.info(f"Usage threshold: {USAGE_THRESHOLD}%")
|
||||
logger.info(f"Check interval: {CHECK_INTERVAL}s")
|
||||
|
||||
auth_data = load_or_create_auth()
|
||||
logger.info("Client access token: %s", _mask(auth_data["access_token"]))
|
||||
logger.info("Client refresh token: %s", _mask(auth_data["refresh_token"]))
|
||||
|
||||
startup_tokens = load_tokens()
|
||||
if startup_tokens:
|
||||
logger.info("Upstream access token: %s", _mask(startup_tokens.access_token))
|
||||
else:
|
||||
logger.warning("No upstream token found at %s", DATA_DIR / "tokens.json")
|
||||
app = create_app()
|
||||
web.run_app(app, host="0.0.0.0", port=PORT)
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
[project]
|
||||
name = "megapt"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"playwright==1.58.0",
|
||||
"aiohttp==3.13.3",
|
||||
"pkce==1.0.3",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
package = false
|
||||
147
src/server.py
Normal file
147
src/server.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from providers.chatgpt import ChatGPTProvider
|
||||
|
||||
PORT = int(os.environ.get("PORT", "8080"))
|
||||
USAGE_REFRESH_THRESHOLD = int(os.environ.get("USAGE_REFRESH_THRESHOLD", "85"))
|
||||
LIMIT_EXHAUSTED_PERCENT = 100
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Registry of available providers
|
||||
PROVIDERS = {
|
||||
"chatgpt": ChatGPTProvider(),
|
||||
}
|
||||
|
||||
refresh_locks = {name: asyncio.Lock() for name in PROVIDERS.keys()}
|
||||
background_refresh_tasks: dict[str, asyncio.Task | None] = {
|
||||
name: None for name in PROVIDERS.keys()
|
||||
}
|
||||
|
||||
|
||||
@web.middleware
|
||||
async def request_log_middleware(request: web.Request, handler):
|
||||
response = await handler(request)
|
||||
logger.info("%s %s -> %s", request.method, request.path_qs, response.status)
|
||||
return response
|
||||
|
||||
|
||||
def build_limit(usage_percent: int) -> dict[str, int | bool]:
|
||||
remaining = max(0, 100 - usage_percent)
|
||||
return {
|
||||
"used_percent": usage_percent,
|
||||
"remaining_percent": remaining,
|
||||
"exhausted": usage_percent >= LIMIT_EXHAUSTED_PERCENT,
|
||||
"needs_refresh": usage_percent >= USAGE_REFRESH_THRESHOLD,
|
||||
}
|
||||
|
||||
|
||||
async def issue_new_token(provider_name: str) -> str | None:
|
||||
provider = PROVIDERS.get(provider_name)
|
||||
if not provider:
|
||||
return None
|
||||
|
||||
async with refresh_locks[provider_name]:
|
||||
logger.info(f"[{provider_name}] Generating new token")
|
||||
success = await provider.register_new_account()
|
||||
if not success:
|
||||
logger.error(f"[{provider_name}] Token generation failed")
|
||||
return None
|
||||
|
||||
token = await provider.get_token()
|
||||
if not token:
|
||||
logger.error(f"[{provider_name}] Token was generated but not available")
|
||||
return None
|
||||
|
||||
return token
|
||||
|
||||
|
||||
async def background_refresh_worker(provider_name: str, reason: str):
|
||||
try:
|
||||
logger.info(f"[{provider_name}] Starting background token refresh ({reason})")
|
||||
new_token = await issue_new_token(provider_name)
|
||||
if new_token:
|
||||
logger.info(f"[{provider_name}] Background token refresh completed")
|
||||
else:
|
||||
logger.error(f"[{provider_name}] Background token refresh failed")
|
||||
except Exception:
|
||||
logger.exception(
|
||||
f"[{provider_name}] Unhandled error in background token refresh"
|
||||
)
|
||||
|
||||
|
||||
def trigger_background_refresh(provider_name: str, reason: str):
|
||||
task = background_refresh_tasks.get(provider_name)
|
||||
if task and not task.done():
|
||||
logger.info(
|
||||
f"[{provider_name}] Background refresh already running, skip ({reason})"
|
||||
)
|
||||
return
|
||||
background_refresh_tasks[provider_name] = asyncio.create_task(
|
||||
background_refresh_worker(provider_name, reason)
|
||||
)
|
||||
|
||||
|
||||
async def token_handler(request: web.Request) -> web.Response:
|
||||
provider_name = request.match_info.get("provider", "chatgpt")
|
||||
|
||||
provider = PROVIDERS.get(provider_name)
|
||||
if not provider:
|
||||
return web.json_response(
|
||||
{"error": f"Unknown provider: {provider_name}"},
|
||||
status=404,
|
||||
)
|
||||
|
||||
# Get or create token
|
||||
token = await provider.get_token()
|
||||
if not token:
|
||||
return web.json_response(
|
||||
{"error": "Failed to get active token"},
|
||||
status=503,
|
||||
)
|
||||
|
||||
# Get usage info
|
||||
usage_info = await provider.get_usage_info(token)
|
||||
if "error" in usage_info:
|
||||
return web.json_response(
|
||||
{"error": usage_info["error"]},
|
||||
status=503,
|
||||
)
|
||||
|
||||
usage_percent = usage_info.get("used_percent", 0)
|
||||
|
||||
# Trigger background refresh if needed
|
||||
if usage_percent >= USAGE_REFRESH_THRESHOLD:
|
||||
trigger_background_refresh(
|
||||
provider_name,
|
||||
f"usage {usage_percent}% >= threshold {USAGE_REFRESH_THRESHOLD}%",
|
||||
)
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
"token": token,
|
||||
"limit": build_limit(usage_percent),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def create_app() -> web.Application:
|
||||
app = web.Application(middlewares=[request_log_middleware])
|
||||
# New route: /{provider}/token
|
||||
app.router.add_get("/{provider}/token", token_handler)
|
||||
# Legacy route for backward compatibility
|
||||
app.router.add_get("/token", token_handler)
|
||||
return app
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info("Starting token service on port %s", PORT)
|
||||
logger.info("Usage refresh threshold: %s%%", USAGE_REFRESH_THRESHOLD)
|
||||
logger.info("Available providers: %s", ", ".join(PROVIDERS.keys()))
|
||||
app = create_app()
|
||||
web.run_app(app, host="0.0.0.0", port=PORT)
|
||||
355
src/uv.lock
generated
355
src/uv.lock
generated
|
|
@ -1,355 +0,0 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.14"
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
version = "2.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.13.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohappyeyeballs" },
|
||||
{ name = "aiosignal" },
|
||||
{ name = "attrs" },
|
||||
{ name = "frozenlist" },
|
||||
{ name = "multidict" },
|
||||
{ name = "propcache" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiosignal"
|
||||
version = "1.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "frozenlist" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "25.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frozenlist"
|
||||
version = "1.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/89/b95f2ddcc5f3c2bc09c8ee8d77be312df7f9e7175703ab780f2014a0e781/greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d", size = 671455, upload-time = "2026-01-23T16:15:57.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/cb/c21a3fd5d2c9c8b622e7bede6d6d00e00551a5ee474ea6d831b5f567a8b4/greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a", size = 228125, upload-time = "2026-01-23T15:32:45.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/8e/8a2db6d11491837af1de64b8aff23707c6e85241be13c60ed399a72e2ef8/greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79", size = 227519, upload-time = "2026-01-23T15:31:47.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/25/c51a63f3f463171e09cb586eb64db0861eb06667ab01a7968371a24c4f3b/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab", size = 662574, upload-time = "2026-01-23T16:15:58.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "megapt"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
{ name = "pkce" },
|
||||
{ name = "playwright" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiohttp", specifier = "==3.13.3" },
|
||||
{ name = "pkce", specifier = "==1.0.3" },
|
||||
{ name = "playwright", specifier = "==1.58.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkce"
|
||||
version = "1.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/29/ea/ddd845c2ec21bf1e8555c782b32dc39b82f0b12764feb9f73ccbb2470f13/pkce-1.0.3.tar.gz", hash = "sha256:9775fd76d8a743d39b87df38af1cd04a58c9b5a5242d5a6350ef343d06814ab6", size = 2757, upload-time = "2021-02-08T18:29:07.07Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/51/52c22ec0812d25f5bf297a01153604bfa7bfa59ed66f6cd8345beb3c2b2a/pkce-1.0.3-py3-none-any.whl", hash = "sha256:55927e24c7d403b2491ebe182b95d9dcb1807643243d47e3879fbda5aad4471d", size = 3200, upload-time = "2021-02-08T18:29:05.678Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "playwright"
|
||||
version = "1.58.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet" },
|
||||
{ name = "pyee" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/c9/9c6061d5703267f1baae6a4647bfd1862e386fbfdb97d889f6f6ae9e3f64/playwright-1.58.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:96e3204aac292ee639edbfdef6298b4be2ea0a55a16b7068df91adac077cc606", size = 42251098, upload-time = "2026-01-30T15:09:24.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/40/59d34a756e02f8c670f0fee987d46f7ee53d05447d43cd114ca015cb168c/playwright-1.58.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:70c763694739d28df71ed578b9c8202bb83e8fe8fb9268c04dd13afe36301f71", size = 41039625, upload-time = "2026-01-30T15:09:27.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/ee/3ce6209c9c74a650aac9028c621f357a34ea5cd4d950700f8e2c4b7fe2c4/playwright-1.58.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:185e0132578733d02802dfddfbbc35f42be23a45ff49ccae5081f25952238117", size = 42251098, upload-time = "2026-01-30T15:09:30.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/af/009958cbf23fac551a940d34e3206e6c7eed2b8c940d0c3afd1feb0b0589/playwright-1.58.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:c95568ba1eda83812598c1dc9be60b4406dffd60b149bc1536180ad108723d6b", size = 46235268, upload-time = "2026-01-30T15:09:33.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/a6/0e66ad04b6d3440dae73efb39540c5685c5fc95b17c8b29340b62abbd952/playwright-1.58.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f9999948f1ab541d98812de25e3a8c410776aa516d948807140aff797b4bffa", size = 45964214, upload-time = "2026-01-30T15:09:36.751Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/4b/236e60ab9f6d62ed0fd32150d61f1f494cefbf02304c0061e78ed80c1c32/playwright-1.58.0-py3-none-win32.whl", hash = "sha256:1e03be090e75a0fabbdaeab65ce17c308c425d879fa48bb1d7986f96bfad0b99", size = 36815998, upload-time = "2026-01-30T15:09:39.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/f8/5ec599c5e59d2f2f336a05b4f318e733077cd5044f24adb6f86900c3e6a7/playwright-1.58.0-py3-none-win_amd64.whl", hash = "sha256:a2bf639d0ce33b3ba38de777e08697b0d8f3dc07ab6802e4ac53fb65e3907af8", size = 36816005, upload-time = "2026-01-30T15:09:42.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/c4/cc0229fea55c87d6c9c67fe44a21e2cd28d1d558a5478ed4d617e9fb0c93/playwright-1.58.0-py3-none-win_arm64.whl", hash = "sha256:32ffe5c303901a13a0ecab91d1c3f74baf73b84f4bedbb6b935f5bc11cc98e1b", size = 33085919, upload-time = "2026-01-30T15:09:45.71Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "propcache"
|
||||
version = "0.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyee"
|
||||
version = "13.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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]]
|
||||
name = "yarl"
|
||||
version = "1.22.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "multidict" },
|
||||
{ name = "propcache" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue