Fix openrent
This commit is contained in:
parent
b3a7ab40c8
commit
4d08f5d08d
5 changed files with 135 additions and 53 deletions
|
|
@ -5,6 +5,7 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY pyproject.toml ./
|
COPY pyproject.toml ./
|
||||||
RUN uv pip install --system -r pyproject.toml
|
RUN uv pip install --system -r pyproject.toml
|
||||||
|
RUN playwright install --with-deps chromium
|
||||||
|
|
||||||
COPY *.py ./
|
COPY *.py ./
|
||||||
COPY property-data/arcgis_data.parquet /data/arcgis_data.parquet
|
COPY property-data/arcgis_data.parquet /data/arcgis_data.parquet
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
"""OpenRent (openrent.co.uk) scraper — rental properties only.
|
"""OpenRent (openrent.co.uk) scraper — rental properties only.
|
||||||
|
|
||||||
OpenRent is behind AWS WAF, so we use FlareSolverr to solve the challenge
|
OpenRent is behind AWS WAF, so we use Playwright (headless Chromium) to solve
|
||||||
and get valid cookies. Then we use curl_cffi with Chrome TLS impersonation
|
the challenge and get valid cookies. Then we use curl_cffi with Chrome TLS
|
||||||
to make requests with those cookies (same pattern as homecouk.py).
|
impersonation to make requests with those cookies.
|
||||||
|
|
||||||
OpenRent is a rental-only platform, so this scraper only handles RENT channel.
|
OpenRent is a rental-only platform, so this scraper only handles RENT channel.
|
||||||
|
|
||||||
|
|
@ -18,10 +18,10 @@ import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import httpx
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from curl_cffi.requests import Session
|
from curl_cffi.requests import Session
|
||||||
from curl_cffi.requests.errors import RequestsError
|
from curl_cffi.requests.errors import RequestsError
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
from constants import (
|
from constants import (
|
||||||
DELAY_BETWEEN_PAGES,
|
DELAY_BETWEEN_PAGES,
|
||||||
|
|
@ -44,72 +44,61 @@ class WafChallengeError(Exception):
|
||||||
"""Raised when OpenRent returns a WAF challenge, indicating cookies need refresh."""
|
"""Raised when OpenRent returns a WAF challenge, indicating cookies need refresh."""
|
||||||
|
|
||||||
|
|
||||||
FLARESOLVERR_URL = os.environ.get("FLARESOLVERR_URL", "http://flaresolverr:8191")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Cookie / session management (mirrors homecouk.py pattern)
|
# Cookie / session management via Playwright
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def solve_waf() -> tuple[dict[str, str], str] | None:
|
def solve_waf() -> tuple[dict[str, str], str] | None:
|
||||||
"""Use FlareSolverr to solve the AWS WAF challenge.
|
"""Use Playwright (headless Chromium) to solve the AWS WAF challenge.
|
||||||
Returns (cookies_dict, user_agent) or None on failure."""
|
Returns (cookies_dict, user_agent) or None on failure."""
|
||||||
log.info("Solving AWS WAF challenge via FlareSolverr at %s", FLARESOLVERR_URL)
|
log.info("Solving AWS WAF challenge via Playwright")
|
||||||
try:
|
try:
|
||||||
with httpx.Client(timeout=120) as client:
|
with sync_playwright() as p:
|
||||||
resp = client.post(
|
browser = p.chromium.launch(
|
||||||
f"{FLARESOLVERR_URL}/v1",
|
headless=True,
|
||||||
json={
|
args=["--no-sandbox", "--disable-blink-features=AutomationControlled"],
|
||||||
"cmd": "request.get",
|
|
||||||
"url": f"{OPENRENT_BASE}/properties-to-rent/?term=london&isLive=true",
|
|
||||||
"maxTimeout": 60000,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
if resp.status_code != 200:
|
context = browser.new_context()
|
||||||
log.error("FlareSolverr returned HTTP %d", resp.status_code)
|
page = context.new_page()
|
||||||
return None
|
|
||||||
|
|
||||||
data = resp.json()
|
url = f"{OPENRENT_BASE}/properties-to-rent/?term=london&isLive=true"
|
||||||
if data.get("status") != "ok":
|
log.info("Navigating to %s", url)
|
||||||
log.error("FlareSolverr error: %s", data.get("message", "unknown"))
|
page.goto(url, wait_until="domcontentloaded", timeout=60000)
|
||||||
return None
|
|
||||||
|
|
||||||
solution = data["solution"]
|
content = page.content()
|
||||||
raw_cookies = solution.get("cookies", [])
|
if "AwsWafIntegration" in content:
|
||||||
user_agent = solution.get("userAgent", "")
|
log.info("Got WAF challenge page, waiting for resolution...")
|
||||||
|
page.wait_for_selector(
|
||||||
|
"a.pli, .pli, [class*=propertyListing]", timeout=30000,
|
||||||
|
)
|
||||||
|
|
||||||
cookies = {}
|
raw_cookies = context.cookies()
|
||||||
for c in raw_cookies:
|
user_agent = page.evaluate("navigator.userAgent")
|
||||||
name = c.get("name", "")
|
browser.close()
|
||||||
if name:
|
|
||||||
cookies[name] = c["value"]
|
|
||||||
|
|
||||||
if not cookies:
|
cookies = {c["name"]: c["value"] for c in raw_cookies}
|
||||||
log.error("FlareSolverr solved but returned no cookies")
|
if "aws-waf-token" not in cookies:
|
||||||
flaresolverr_attempts_total.labels(result="no_cookies").inc()
|
log.error("Playwright solved page but no aws-waf-token cookie found")
|
||||||
return None
|
flaresolverr_attempts_total.labels(result="no_cookies").inc()
|
||||||
|
return None
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
"AWS WAF solved — got %d cookies, UA: %s",
|
"AWS WAF solved — got %d cookies, UA: %s",
|
||||||
len(cookies), user_agent[:60],
|
len(cookies), user_agent[:60],
|
||||||
)
|
)
|
||||||
flaresolverr_attempts_total.labels(result="success").inc()
|
flaresolverr_attempts_total.labels(result="success").inc()
|
||||||
return cookies, user_agent
|
return cookies, user_agent
|
||||||
|
|
||||||
except (httpx.ConnectError, httpx.ReadTimeout) as e:
|
|
||||||
log.warning("FlareSolverr not available: %s", e)
|
|
||||||
flaresolverr_attempts_total.labels(result="unavailable").inc()
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error("FlareSolverr error: %s", e)
|
log.error("Playwright WAF solve failed: %s", e)
|
||||||
flaresolverr_attempts_total.labels(result="error").inc()
|
flaresolverr_attempts_total.labels(result="error").inc()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def load_cookies() -> tuple[dict[str, str], str] | None:
|
def load_cookies() -> tuple[dict[str, str], str] | None:
|
||||||
"""Get OpenRent cookies + user-agent.
|
"""Get OpenRent cookies + user-agent.
|
||||||
Tries FlareSolverr first, then falls back to environment variables."""
|
Tries Playwright first, then falls back to environment variables."""
|
||||||
result = solve_waf()
|
result = solve_waf()
|
||||||
if result:
|
if result:
|
||||||
return result
|
return result
|
||||||
|
|
@ -208,12 +197,18 @@ def fetch_page(
|
||||||
|
|
||||||
def _extract_price(text: str) -> tuple[int, str] | None:
|
def _extract_price(text: str) -> tuple[int, str] | None:
|
||||||
"""Extract price and frequency from text like '£1,500 pcm' or '£350 pw'.
|
"""Extract price and frequency from text like '£1,500 pcm' or '£350 pw'.
|
||||||
Returns (price_int, frequency) or None."""
|
Returns (price_int, frequency) or None.
|
||||||
|
|
||||||
|
OpenRent card text shows both monthly and weekly prices (e.g.
|
||||||
|
'£2,800 per month £646 per week'), so check monthly *before* weekly
|
||||||
|
to match the first (monthly) price that the regex captures."""
|
||||||
match = re.search(r"£([\d,]+)", text)
|
match = re.search(r"£([\d,]+)", text)
|
||||||
if not match:
|
if not match:
|
||||||
return None
|
return None
|
||||||
price = int(match.group(1).replace(",", ""))
|
price = int(match.group(1).replace(",", ""))
|
||||||
lower = text.lower()
|
lower = text.lower()
|
||||||
|
if "pcm" in lower or "per month" in lower or "/m" in lower:
|
||||||
|
return price, "monthly"
|
||||||
if "pw" in lower or "per week" in lower or "/w" in lower:
|
if "pw" in lower or "per week" in lower or "/w" in lower:
|
||||||
return price, "weekly"
|
return price, "weekly"
|
||||||
if "pa" in lower or "per annum" in lower or "/y" in lower:
|
if "pa" in lower or "per annum" in lower or "/y" in lower:
|
||||||
|
|
@ -368,8 +363,12 @@ def parse_property_detail(html: str) -> dict:
|
||||||
elif "furnish" in label:
|
elif "furnish" in label:
|
||||||
details["furnished"] = value
|
details["furnished"] = value
|
||||||
|
|
||||||
# Extract postcode from page title / address heading
|
# Extract postcode from page title / address heading.
|
||||||
title_tag = soup.select_one("h1, .property-title, [class*='title']")
|
# Prefer h1 (the actual property title) over generic [class*='title']
|
||||||
|
# which may match nav elements like "Log in".
|
||||||
|
title_tag = soup.select_one("h1")
|
||||||
|
if not title_tag:
|
||||||
|
title_tag = soup.select_one(".property-title, [class*='title']")
|
||||||
if title_tag:
|
if title_tag:
|
||||||
title_text = title_tag.get_text(strip=True)
|
title_text = title_tag.get_text(strip=True)
|
||||||
details["title"] = title_text
|
details["title"] = title_text
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,5 @@ dependencies = [
|
||||||
"fake-useragent>=2.2.0",
|
"fake-useragent>=2.2.0",
|
||||||
"prometheus-client",
|
"prometheus-client",
|
||||||
"beautifulsoup4",
|
"beautifulsoup4",
|
||||||
|
"playwright>=1.58.0",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -232,8 +232,11 @@ def run_scrape(
|
||||||
log.debug("Outcode %s (%d/%d) — %d properties so far",
|
log.debug("Outcode %s (%d/%d) — %d properties so far",
|
||||||
outcode, i + 1, len(shuffled), len(all_properties))
|
outcode, i + 1, len(shuffled), len(all_properties))
|
||||||
|
|
||||||
|
made_requests = False
|
||||||
|
|
||||||
# --- Rightmove ---
|
# --- Rightmove ---
|
||||||
if SCRAPE_RIGHTMOVE:
|
if SCRAPE_RIGHTMOVE:
|
||||||
|
made_requests = True
|
||||||
try:
|
try:
|
||||||
outcode_id = resolve_outcode_id(client, outcode)
|
outcode_id = resolve_outcode_id(client, outcode)
|
||||||
if not outcode_id:
|
if not outcode_id:
|
||||||
|
|
@ -255,6 +258,7 @@ def run_scrape(
|
||||||
|
|
||||||
# --- home.co.uk ---
|
# --- home.co.uk ---
|
||||||
if hk_client and not hk_failed:
|
if hk_client and not hk_failed:
|
||||||
|
made_requests = True
|
||||||
try:
|
try:
|
||||||
hk_props = homecouk_search_outcode(
|
hk_props = homecouk_search_outcode(
|
||||||
hk_client, outcode, channel_name, pc_index,
|
hk_client, outcode, channel_name, pc_index,
|
||||||
|
|
@ -298,6 +302,7 @@ def run_scrape(
|
||||||
|
|
||||||
# --- OpenRent (RENT channel only) ---
|
# --- OpenRent (RENT channel only) ---
|
||||||
if or_client and not or_failed and channel_name == "RENT":
|
if or_client and not or_failed and channel_name == "RENT":
|
||||||
|
made_requests = True
|
||||||
try:
|
try:
|
||||||
or_props = openrent_search_outcode(
|
or_props = openrent_search_outcode(
|
||||||
or_client, outcode, pc_index, pc_coords,
|
or_client, outcode, pc_index, pc_coords,
|
||||||
|
|
@ -350,7 +355,7 @@ def run_scrape(
|
||||||
log.info("Outcode %s: total %d (rm: %d, hk: %d, or: %d)",
|
log.info("Outcode %s: total %d (rm: %d, hk: %d, or: %d)",
|
||||||
outcode, len(all_properties), rm_count, hk_count, or_count)
|
outcode, len(all_properties), rm_count, hk_count, or_count)
|
||||||
|
|
||||||
if i < len(shuffled) - 1:
|
if made_requests and i < len(shuffled) - 1:
|
||||||
time.sleep(DELAY_BETWEEN_OUTCODES)
|
time.sleep(DELAY_BETWEEN_OUTCODES)
|
||||||
|
|
||||||
# Write parquet
|
# Write parquet
|
||||||
|
|
|
||||||
76
finder/uv.lock
generated
76
finder/uv.lock
generated
|
|
@ -166,6 +166,7 @@ dependencies = [
|
||||||
{ name = "fake-useragent" },
|
{ name = "fake-useragent" },
|
||||||
{ name = "flask" },
|
{ name = "flask" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
|
{ name = "playwright" },
|
||||||
{ name = "polars" },
|
{ name = "polars" },
|
||||||
{ name = "prometheus-client" },
|
{ name = "prometheus-client" },
|
||||||
]
|
]
|
||||||
|
|
@ -177,6 +178,7 @@ requires-dist = [
|
||||||
{ name = "fake-useragent", specifier = ">=2.2.0" },
|
{ name = "fake-useragent", specifier = ">=2.2.0" },
|
||||||
{ name = "flask" },
|
{ name = "flask" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
|
{ name = "playwright", specifier = ">=1.58.0" },
|
||||||
{ name = "polars" },
|
{ name = "polars" },
|
||||||
{ name = "prometheus-client" },
|
{ name = "prometheus-client" },
|
||||||
]
|
]
|
||||||
|
|
@ -198,6 +200,49 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
|
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "greenlet"
|
||||||
|
version = "3.3.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h11"
|
name = "h11"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
|
|
@ -328,6 +373,25 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "polars"
|
name = "polars"
|
||||||
version = "1.39.0"
|
version = "1.39.0"
|
||||||
|
|
@ -374,6 +438,18 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyee"
|
||||||
|
version = "13.0.1"
|
||||||
|
source = { registry = "https://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]]
|
[[package]]
|
||||||
name = "soupsieve"
|
name = "soupsieve"
|
||||||
version = "2.8.3"
|
version = "2.8.3"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue