feat: add vault
This commit is contained in:
16
.dockerignore
Normal file
16
.dockerignore
Normal file
@@ -0,0 +1,16 @@
|
||||
README.md
|
||||
LICENSE
|
||||
tests/
|
||||
__pycache__/
|
||||
.mypy_cache
|
||||
.pytest_cache
|
||||
.gitignore
|
||||
.editorconfig
|
||||
.pre-commit-config.yaml
|
||||
ruff.toml
|
||||
pytest.ini
|
||||
.idea/
|
||||
.git/
|
||||
mypy.ini
|
||||
bandit.yaml
|
||||
ruf
|
||||
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
.idea/
|
||||
venv/
|
||||
__pycache__/
|
||||
.ruff_cache/
|
||||
.mypy_cache
|
||||
.pytest_cache
|
||||
.vscode/
|
||||
.pycharm/
|
||||
/venv/
|
||||
/.idea/
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.editorconfig
|
||||
.env
|
||||
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl \
|
||||
fonts-liberation \
|
||||
libasound2 libatk-bridge2.0-0 libatk1.0-0 libcups2 libdbus-1-3 \
|
||||
libdrm2 libgbm1 libnspr4 libnss3 libx11-6 libx11-xcb1 libxcb1 \
|
||||
libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 \
|
||||
libxrandr2 libxrender1 libxshmfence1 libxss1 libxtst6 \
|
||||
libpango-1.0-0 libpangocairo-1.0-0 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install --no-cache-dir uv
|
||||
|
||||
COPY pyproject.toml /app/pyproject.toml
|
||||
|
||||
RUN uv pip compile pyproject.toml -o /app/requirements.txt \
|
||||
&& uv pip install --system -r /app/requirements.txt
|
||||
|
||||
RUN python -m patchright install chromium
|
||||
|
||||
COPY src /app/src
|
||||
|
||||
CMD ["uv", "run", "python", "-m", "src.main"]
|
||||
6
docker-compose.yml
Normal file
6
docker-compose.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
services:
|
||||
crypto_parser:
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
10
pyproject.toml
Normal file
10
pyproject.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[project]
|
||||
name = "crypto-parser"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"dotenv>=0.9.9",
|
||||
"patchright>=1.57.2",
|
||||
"redis>=7.1.0",
|
||||
]
|
||||
392
src/main.py
Normal file
392
src/main.py
Normal file
@@ -0,0 +1,392 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import ssl
|
||||
import gzip
|
||||
import math
|
||||
from datetime import datetime, timezone
|
||||
from urllib.request import Request, urlopen
|
||||
from dotenv import load_dotenv
|
||||
import redis.asyncio as redis
|
||||
from patchright.async_api import async_playwright
|
||||
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
INTERVAL_SEC = int(os.getenv('INTERVAL_SEC', '20'))
|
||||
DIGITS_AFTER_DECIMAL = int(os.getenv('DIGITS_AFTER_DECIMAL', '5'))
|
||||
USDT_URL = os.getenv('USDT_URL', 'https://tradex.by/trading/ru/usdt/rub')
|
||||
USDC_URL = os.getenv('USDC_URL', 'https://tradex.by/trading/ru/usdc/rub')
|
||||
PRICE_SELECTOR = os.getenv('PRICE_SELECTOR', 'div.ng-star-inserted:has(span) > span')
|
||||
PRICE_INDEX = int(os.getenv('PRICE_INDEX', '2'))
|
||||
USDT_ADD_RUB = float(os.getenv('USDT_ADD_RUB', '2.5'))
|
||||
USDC_ADD_RUB = float(os.getenv('USDC_ADD_RUB', '2.0'))
|
||||
FREEZEX_TICKER_URL = os.getenv('FREEZEX_TICKER_URL', 'https://cryptottlivewebapi.free2ex.net:8443/api/v2/public/ticker')
|
||||
FREEZEX_SYMBOL = os.getenv('FREEZEX_SYMBOL', 'USDTRUB')
|
||||
FREEZEX_ADD_RUB = float(os.getenv('FREEZEX_ADD_RUB', '2.0'))
|
||||
REDIS_KEY_LAST = os.getenv('REDIS_KEY_LAST', 'tradex:rub:last')
|
||||
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO').upper()
|
||||
USE_CHROME_CHANNEL = os.getenv('USE_CHROME_CHANNEL', '0') == '1'
|
||||
HTTP_USER_AGENT = os.getenv('HTTP_USER_AGENT', 'curl/8.0.0')
|
||||
HTTP_TIMEOUT_SEC = float(os.getenv('HTTP_TIMEOUT_SEC', '30'))
|
||||
|
||||
VAULT_ADDR = os.getenv('VAULT_ADDR', '').rstrip('/')
|
||||
VAULT_MOUNT_POINT = os.getenv('VAULT_MOUNT_POINT', '')
|
||||
VAULT_ROLE_ID = os.getenv('VAULT_ROLE_ID', '')
|
||||
VAULT_SECRET_ID = os.getenv('VAULT_SECRET_ID', '')
|
||||
VAULT_SECRET_PATH = os.getenv('VAULT_SECRET_PATH', 'keydb')
|
||||
|
||||
KEYDB_KEY_RATE = os.getenv('KEYDB_KEY_RATE', 'tradex:rub:rate')
|
||||
|
||||
logger = logging.getLogger('tradexparser')
|
||||
|
||||
|
||||
def setup_logging() -> None:
|
||||
logging.basicConfig(
|
||||
level=LOG_LEVEL,
|
||||
format='%(asctime)s | %(levelname)s | %(name)s | %(message)s',
|
||||
)
|
||||
logging.getLogger('websockets').setLevel(logging.WARNING)
|
||||
logging.getLogger('asyncio').setLevel(logging.WARNING)
|
||||
|
||||
|
||||
def ceil_to_2(value: float) -> float:
|
||||
return math.ceil(value * 100.0) / 100.0
|
||||
|
||||
|
||||
def to_float_str(text: str | None) -> str | None:
|
||||
if not text:
|
||||
return None
|
||||
t = text.split('\n')[0].strip().replace(',', '.')
|
||||
try:
|
||||
return f'{float(t):.{DIGITS_AFTER_DECIMAL}f}'
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def fmt_rate(value: float) -> str:
|
||||
return f'{value:.{DIGITS_AFTER_DECIMAL}f}'
|
||||
|
||||
|
||||
def fmt_ceil_2(rate_str: str | None) -> str | None:
|
||||
if rate_str is None:
|
||||
return None
|
||||
try:
|
||||
v = float(rate_str)
|
||||
except ValueError:
|
||||
return None
|
||||
return f'{ceil_to_2(v):.2f}'
|
||||
|
||||
|
||||
def add_to_rate(rate_str: str | None, add_rub: float) -> str | None:
|
||||
if rate_str is None:
|
||||
return None
|
||||
try:
|
||||
return fmt_rate(float(rate_str) + add_rub)
|
||||
except ValueError:
|
||||
return rate_str
|
||||
|
||||
|
||||
def avg_rates(a: str | None, b: str | None) -> str | None:
|
||||
if a is None and b is None:
|
||||
return None
|
||||
if a is None:
|
||||
return b
|
||||
if b is None:
|
||||
return a
|
||||
try:
|
||||
return fmt_rate((float(a) + float(b)) / 2.0)
|
||||
except ValueError:
|
||||
return a
|
||||
|
||||
|
||||
def _http_get_json(url: str) -> object:
|
||||
req = Request(
|
||||
url,
|
||||
headers={
|
||||
'accept': 'application/json',
|
||||
'user-agent': HTTP_USER_AGENT,
|
||||
'accept-encoding': 'gzip',
|
||||
},
|
||||
)
|
||||
ctx = ssl.create_default_context()
|
||||
with urlopen(req, timeout=HTTP_TIMEOUT_SEC, context=ctx) as resp:
|
||||
raw = resp.read()
|
||||
if str(resp.headers.get('Content-Encoding', '')).lower() == 'gzip':
|
||||
raw = gzip.decompress(raw)
|
||||
return json.loads(raw.decode('utf-8'))
|
||||
|
||||
|
||||
def _pick_mid_price(row: dict) -> float | None:
|
||||
def f(key: str) -> float | None:
|
||||
v = row.get(key)
|
||||
try:
|
||||
fv = float(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return fv if fv != 0.0 else None
|
||||
|
||||
bid = f('BestBid')
|
||||
ask = f('BestAsk')
|
||||
if bid is not None and ask is not None:
|
||||
return (bid + ask) / 2.0
|
||||
|
||||
lb = f('LastBuyPrice')
|
||||
ls = f('LastSellPrice')
|
||||
if lb is not None and ls is not None:
|
||||
return (lb + ls) / 2.0
|
||||
|
||||
lp = f('LastPrice')
|
||||
if lp is not None:
|
||||
return lp
|
||||
|
||||
return ask if ask is not None else bid
|
||||
|
||||
|
||||
def _http_post_json(url: str, payload: dict) -> object:
|
||||
body = json.dumps(payload).encode('utf-8')
|
||||
req = Request(
|
||||
url,
|
||||
data=body,
|
||||
headers={
|
||||
'content-type': 'application/json',
|
||||
'accept': 'application/json',
|
||||
'user-agent': HTTP_USER_AGENT,
|
||||
},
|
||||
method='POST',
|
||||
)
|
||||
ctx = ssl.create_default_context()
|
||||
with urlopen(req, timeout=HTTP_TIMEOUT_SEC, context=ctx) as resp:
|
||||
raw = resp.read()
|
||||
if str(resp.headers.get('Content-Encoding', '')).lower() == 'gzip':
|
||||
raw = gzip.decompress(raw)
|
||||
return json.loads(raw.decode('utf-8'))
|
||||
|
||||
|
||||
def _vault_get_json(url: str, token: str) -> object:
|
||||
req = Request(
|
||||
url,
|
||||
headers={
|
||||
'accept': 'application/json',
|
||||
'user-agent': HTTP_USER_AGENT,
|
||||
'x-vault-token': token,
|
||||
},
|
||||
)
|
||||
ctx = ssl.create_default_context()
|
||||
with urlopen(req, timeout=HTTP_TIMEOUT_SEC, context=ctx) as resp:
|
||||
raw = resp.read()
|
||||
return json.loads(raw.decode('utf-8'))
|
||||
|
||||
|
||||
async def load_keydb_url_from_vault() -> str:
|
||||
if not VAULT_ADDR or not VAULT_MOUNT_POINT or not VAULT_ROLE_ID or not VAULT_SECRET_ID:
|
||||
raise RuntimeError('Vault env is not fully configured (VAULT_ADDR, VAULT_MOUNT_POINT, VAULT_ROLE_ID, VAULT_SECRET_ID)')
|
||||
|
||||
login_url = f'{VAULT_ADDR}/v1/auth/approle/login'
|
||||
login_payload = {'role_id': VAULT_ROLE_ID, 'secret_id': VAULT_SECRET_ID}
|
||||
login_resp = await asyncio.to_thread(_http_post_json, login_url, login_payload)
|
||||
if not isinstance(login_resp, dict):
|
||||
raise RuntimeError('Vault login failed: unexpected response')
|
||||
auth = login_resp.get('auth')
|
||||
if not isinstance(auth, dict):
|
||||
raise RuntimeError('Vault login failed: missing auth field')
|
||||
token = auth.get('client_token')
|
||||
if not isinstance(token, str) or not token:
|
||||
raise RuntimeError('Vault login failed: missing client_token')
|
||||
|
||||
secret_url = f'{VAULT_ADDR}/v1/{VAULT_MOUNT_POINT}/data/{VAULT_SECRET_PATH}'
|
||||
secret_resp = await asyncio.to_thread(_vault_get_json, secret_url, token)
|
||||
if not isinstance(secret_resp, dict):
|
||||
raise RuntimeError('Vault read failed: unexpected response')
|
||||
data = secret_resp.get('data')
|
||||
if not isinstance(data, dict):
|
||||
raise RuntimeError('Vault read failed: missing data field')
|
||||
data2 = data.get('data')
|
||||
if not isinstance(data2, dict):
|
||||
raise RuntimeError('Vault read failed: missing data.data field')
|
||||
|
||||
host = str(data2.get('host', '')).strip()
|
||||
port = str(data2.get('port', '')).strip()
|
||||
password = str(data2.get('password', '')).strip()
|
||||
database = str(data2.get('database', '')).strip()
|
||||
|
||||
if not host or not port or not password or database == '':
|
||||
raise RuntimeError('Vault secret is missing required keydb fields: host,port,password,database')
|
||||
|
||||
return f'redis://:{password}@{host}:{port}/{database}'
|
||||
|
||||
|
||||
async def read_freezex_usdt_rub() -> str | None:
|
||||
try:
|
||||
data = await asyncio.to_thread(_http_get_json, FREEZEX_TICKER_URL)
|
||||
if not isinstance(data, list):
|
||||
logger.warning('Freezex ticker unexpected payload type=%s', type(data).__name__)
|
||||
return None
|
||||
symbol_u = FREEZEX_SYMBOL.upper()
|
||||
for row in data:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
if str(row.get('Symbol', '')).upper() != symbol_u:
|
||||
continue
|
||||
price = _pick_mid_price(row)
|
||||
return fmt_rate(price) if price is not None else None
|
||||
logger.warning('Freezex ticker: symbol not found: %s', FREEZEX_SYMBOL)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning('Freezex ticker fetch failed: %s', e)
|
||||
return None
|
||||
|
||||
|
||||
async def read_price(page) -> str | None:
|
||||
try:
|
||||
locator = page.locator(PRICE_SELECTOR).nth(PRICE_INDEX)
|
||||
raw = await locator.inner_text(timeout=5000)
|
||||
return to_float_str(raw)
|
||||
except Exception as e:
|
||||
logger.warning('Failed to read price: %s', e)
|
||||
return None
|
||||
|
||||
|
||||
async def write_to_redis(
|
||||
r: redis.Redis,
|
||||
ts_iso: str,
|
||||
usdt: str | None,
|
||||
usdc: str | None,
|
||||
extra: dict[str, str] | None = None,
|
||||
) -> None:
|
||||
data = {'ts': ts_iso}
|
||||
if usdt is not None:
|
||||
data['usdt_rub'] = usdt
|
||||
if usdc is not None:
|
||||
data['usdc_rub'] = usdc
|
||||
if extra:
|
||||
data.update(extra)
|
||||
await r.hset(REDIS_KEY_LAST, mapping=data)
|
||||
|
||||
|
||||
async def write_rate_value(r: redis.Redis, ts_iso: str, rate_str: str | None) -> None:
|
||||
if rate_str is None:
|
||||
return
|
||||
value2 = fmt_ceil_2(rate_str)
|
||||
if value2 is None:
|
||||
return
|
||||
await r.hset(KEYDB_KEY_RATE, mapping={'ts': ts_iso, 'value': value2})
|
||||
|
||||
|
||||
async def run_loop() -> None:
|
||||
setup_logging()
|
||||
logger.info(
|
||||
'Starting worker interval=%ss key=%s selector=%s index=%s freezex=%s symbol=%s vault=%s mount=%s secret=%s',
|
||||
INTERVAL_SEC, REDIS_KEY_LAST, PRICE_SELECTOR, PRICE_INDEX, FREEZEX_TICKER_URL, FREEZEX_SYMBOL, VAULT_ADDR, VAULT_MOUNT_POINT, VAULT_SECRET_PATH
|
||||
)
|
||||
|
||||
try:
|
||||
keydb_url = await load_keydb_url_from_vault()
|
||||
logger.info('KeyDB url loaded from vault')
|
||||
except Exception:
|
||||
logger.exception('Failed to load KeyDB config from vault')
|
||||
return
|
||||
|
||||
r = redis.from_url(keydb_url, decode_responses=True)
|
||||
try:
|
||||
await r.ping()
|
||||
logger.info('KeyDB connected')
|
||||
except Exception:
|
||||
logger.exception('KeyDB connection failed')
|
||||
return
|
||||
|
||||
async with async_playwright() as p:
|
||||
launch_kwargs = {'headless': True}
|
||||
if USE_CHROME_CHANNEL:
|
||||
launch_kwargs['channel'] = 'chrome'
|
||||
|
||||
browser = await p.chromium.launch(**launch_kwargs)
|
||||
context = await browser.new_context()
|
||||
|
||||
page_usdt = await context.new_page()
|
||||
page_usdc = await context.new_page()
|
||||
|
||||
logger.info('Opening pages...')
|
||||
await page_usdt.goto(USDT_URL, wait_until='domcontentloaded')
|
||||
await page_usdc.goto(USDC_URL, wait_until='domcontentloaded')
|
||||
await asyncio.sleep(2)
|
||||
logger.info('Pages opened')
|
||||
|
||||
try:
|
||||
while True:
|
||||
ts_iso = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
usdt_tradex_raw = await read_price(page_usdt)
|
||||
usdt_tradex = add_to_rate(usdt_tradex_raw, USDT_ADD_RUB)
|
||||
|
||||
usdt_freezex_raw = await read_freezex_usdt_rub()
|
||||
usdt_freezex = add_to_rate(usdt_freezex_raw, FREEZEX_ADD_RUB)
|
||||
|
||||
usdt_avg = avg_rates(usdt_tradex, usdt_freezex)
|
||||
|
||||
usdc_tradex_raw = await read_price(page_usdc)
|
||||
usdc_tradex = add_to_rate(usdc_tradex_raw, USDC_ADD_RUB)
|
||||
|
||||
status = (
|
||||
'ok'
|
||||
if usdt_avg and usdc_tradex
|
||||
else 'partial'
|
||||
if usdt_avg or usdc_tradex
|
||||
else 'failed'
|
||||
)
|
||||
logger.info(
|
||||
'tick status=%s USDT(avg)=%s USDT(tradex raw/adj)=%s/%s USDT(freezex raw/adj)=%s/%s USDC(raw/adj)=%s/%s',
|
||||
status,
|
||||
usdt_avg,
|
||||
usdt_tradex_raw,
|
||||
usdt_tradex,
|
||||
usdt_freezex_raw,
|
||||
usdt_freezex,
|
||||
usdc_tradex_raw,
|
||||
usdc_tradex,
|
||||
)
|
||||
|
||||
try:
|
||||
extra: dict[str, str] = {}
|
||||
if usdt_tradex_raw is not None:
|
||||
extra['usdt_rub_tradex_raw'] = usdt_tradex_raw
|
||||
if usdt_tradex is not None:
|
||||
extra['usdt_rub_tradex'] = usdt_tradex
|
||||
if usdt_freezex_raw is not None:
|
||||
extra['usdt_rub_freezex_raw'] = usdt_freezex_raw
|
||||
if usdt_freezex is not None:
|
||||
extra['usdt_rub_freezex'] = usdt_freezex
|
||||
if usdt_avg is not None:
|
||||
extra['usdt_rub_avg'] = usdt_avg
|
||||
|
||||
if usdc_tradex_raw is not None:
|
||||
extra['usdc_rub_tradex_raw'] = usdc_tradex_raw
|
||||
if usdc_tradex is not None:
|
||||
extra['usdc_rub_tradex'] = usdc_tradex
|
||||
|
||||
usdt_avg_ceil2 = fmt_ceil_2(usdt_avg)
|
||||
usdc_tradex_ceil2 = fmt_ceil_2(usdc_tradex)
|
||||
await write_to_redis(r, ts_iso, usdt_avg_ceil2, usdc_tradex_ceil2, extra=extra)
|
||||
await write_rate_value(r, ts_iso, usdt_avg)
|
||||
logger.info('Written to keydb')
|
||||
except Exception:
|
||||
logger.exception('KeyDB write failed')
|
||||
|
||||
await asyncio.sleep(INTERVAL_SEC)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info('Stopped by user')
|
||||
finally:
|
||||
await browser.close()
|
||||
await r.close()
|
||||
logger.info('Shutdown complete')
|
||||
|
||||
|
||||
def main() -> None:
|
||||
asyncio.run(run_loop())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
132
uv.lock
generated
Normal file
132
uv.lock
generated
Normal file
@@ -0,0 +1,132 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-parser"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "dotenv" },
|
||||
{ name = "patchright" },
|
||||
{ name = "redis" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "dotenv", specifier = ">=0.9.9" },
|
||||
{ name = "patchright", specifier = ">=1.57.2" },
|
||||
{ name = "redis", specifier = ">=7.1.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenv"
|
||||
version = "0.9.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "python-dotenv" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" },
|
||||
]
|
||||
|
||||
[[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/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/cd/7a7ca57588dac3389e97f7c9521cb6641fd8b6602faf1eaa4188384757df/greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca", size = 622363, upload-time = "2026-01-23T16:15:54.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/2f/5e0e41f33c69655300a5e54aeb637cf8ff57f1786a3aba374eacc0228c1d/greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a", size = 227156, upload-time = "2026-01-23T15:34:34.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/ab/717c58343cf02c5265b531384b248787e04d8160b8afe53d9eec053d7b44/greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1", size = 226403, upload-time = "2026-01-23T15:31:39.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/00/95df0b6a935103c0452dad2203f5be8377e551b8466a29650c4c5a5af6cc/greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e", size = 624375, upload-time = "2026-01-23T16:15:55.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/b3/c9c23a6478b3bcc91f979ce4ca50879e4d0b2bd7b9a53d8ecded719b92e2/greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946", size = 227042, upload-time = "2026-01-23T15:33:58.216Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/e7/824beda656097edee36ab15809fd063447b200cc03a7f6a24c34d520bc88/greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d", size = 226294, upload-time = "2026-01-23T15:30:52.73Z" },
|
||||
{ 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 = "patchright"
|
||||
version = "1.57.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet" },
|
||||
{ name = "pyee" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/9d/2e8ab9f09bdbfd4b8b17229f92f3b2af32fd8474a19c92d94f08c7a78dd1/patchright-1.57.2-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:eecd88992104156da183bf0e0144a43c278227a0eb9e538ae62c3f01448b86b8", size = 41963318, upload-time = "2025-12-30T15:33:40.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/a0/80ee346f4d22cca4d6e6112e162dad51d9702350438b4b644acfe2874ce2/patchright-1.57.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b16d07fc454fbd7392c29663784abf9a577b2482df3479ae487ecd8bc3948b68", size = 40753852, upload-time = "2025-12-30T15:33:43.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/84/d3f53f39855288bbe26a4ee8b5af00e1aab940af7dc971be53b3209671a9/patchright-1.57.2-py3-none-macosx_11_0_universal2.whl", hash = "sha256:af8e74a16af04dda592a8f8e89e4dd041e1ffcfd90566d17577176958fe80393", size = 41963318, upload-time = "2025-12-30T15:33:46.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/6b/214164f57f339742c3795e55cd472c7cdb82ac769d89548c6526477794f8/patchright-1.57.2-py3-none-manylinux1_x86_64.whl", hash = "sha256:5119daf606c0d21185ef4d2a6f4008db915173ed11601ebedd67abadcab0d594", size = 45953528, upload-time = "2025-12-30T15:33:49.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/5c/fb81d91376ac15d275812ab6fb8048506ee444413a97319ef640817b0984/patchright-1.57.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b58d6b2122c47dc72204847eff0ec0ea67afcb1f6573fa3f357b149b389af4ff", size = 45685195, upload-time = "2025-12-30T15:33:52.576Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/87/7a2e2fab5d25aa4fce59dfe9d69d3b18a437b19243fdf498697dc1fe0f98/patchright-1.57.2-py3-none-win32.whl", hash = "sha256:54f5f090ed5c9bb331c79c5546b9a43854ee87eb31072c308f45c76561e409b5", size = 36532138, upload-time = "2025-12-30T15:33:55.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/0f/ed5262996ea573632c4c18ab72c8eafc1eb1ee2c8b3d8470cb52f7075ca1/patchright-1.57.2-py3-none-win_amd64.whl", hash = "sha256:8865dca554dbe98b585e277f3d151e0b5fccdf3765408ec0c8631093f7828eda", size = 36532143, upload-time = "2025-12-30T15:33:58.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/31/5371fb36ec6ec3b4b1ae1784dd5254e2c7b85d0e0d34dd371933ffde2c53/patchright-1.57.2-py3-none-win_arm64.whl", hash = "sha256:830173d31c0a9eed6c03192e191dc007f30f05bc580764860cbd0001431f6f01", size = 32816218, upload-time = "2025-12-30T15:34:01.582Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyee"
|
||||
version = "13.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250, upload-time = "2025-03-17T18:53:15.955Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730, upload-time = "2025-03-17T18:53:14.532Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "7.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" },
|
||||
]
|
||||
|
||||
[[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" },
|
||||
]
|
||||
Reference in New Issue
Block a user