diff --git a/.env.example b/.env.example index f56007ee..8b2dbf96 100644 --- a/.env.example +++ b/.env.example @@ -44,7 +44,5 @@ HTTP_URL="" # Not used currently ONION_URL="XXX.onion" -RELAYS="wss://relay.damus.io,wss://relay.primal.net,wss://relay.snort.social,wss://relay.nostr.band" +RELAYS="wss://relay.damus.io,wss://relay.nostr.band" CASHU_MINTS="https://mint.minibits.cash/Bitcoin,https://mint.cubabitcoin.org" - -CURRENCY = "" diff --git a/.gitignore b/.gitignore index 4177ffa7..c21c4b2d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,10 @@ compose.override.yml # Coverage .coverage -# deployment +# Logging +logs/* +!logs/.gitkeep *.log + +# deployment +proof_backups diff --git a/Dockerfile b/Dockerfile index acf0554c..ba15bfb8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,8 @@ RUN apk add git COPY uv.lock pyproject.toml ./ -RUN uv sync +RUN uv add git+https://github.com/saschanaz/secp256k1-py.git#branch=upgrade060 +# RUN uv sync WORKDIR /app diff --git a/README.md b/README.md index 6381679d..e4d60e14 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,10 @@ The most common settings are shown below. See `.env.example` for the full list. - `REFUND_PROCESSING_INTERVAL` – Seconds between automatic refunds - `ADMIN_PASSWORD` – Password for the `/admin` dashboard +## Withdrawing Balance + +Go to `https:///admin/` (NOTE: be sure to add the '/' at the end), enter the `ADMIN_PASSWORD` you set above and withdraw your balance as a Cashu token. + ## Example Client `example.py` shows how to use the proxy with the official OpenAI client: diff --git a/compose.yml b/compose.yml index 3e574639..0b3ba57e 100644 --- a/compose.yml +++ b/compose.yml @@ -5,6 +5,7 @@ services: build: . volumes: - .:/app + - ./logs:/app/logs env_file: - .env environment: diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/pyproject.toml b/pyproject.toml index 97aada86..327ef826 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,10 +7,13 @@ requires-python = ">=3.11" dependencies = [ "fastapi[standard]>=0.115", "aiosqlite>=0.20", - "sixty-nuts>=0.1.3", "sqlmodel>=0.0.24", "httpx[socks]>=0.25.2", "greenlet>=3.2.1", + "python-json-logger>=2.0.0", + "cashu", + "secp256k1", + "marshmallow>=3.13,<4.0", ] [dependency-groups] @@ -57,3 +60,6 @@ check_untyped_defs = true disallow_untyped_calls = true disallow_incomplete_defs = true disallow_untyped_decorators = true + +[tool.uv.sources] +secp256k1 = { git = "https://github.com/saschanaz/secp256k1-py", branch = "upgrade060" } diff --git a/router/account.py b/router/account.py index e87099d8..c4389a2a 100644 --- a/router/account.py +++ b/router/account.py @@ -3,13 +3,8 @@ from typing import Annotated, NoReturn from fastapi import APIRouter, Depends, Header, HTTPException from .auth import validate_bearer_key -from .cashu import ( - credit_balance, - delete_key_if_zero_balance, - refund_balance, - wallet, -) from .db import ApiKey, AsyncSession, get_session +from .wallet import credit_balance, send_to_lnurl, send_token wallet_router = APIRouter(prefix="/v1/wallet") @@ -66,8 +61,8 @@ async def refund_wallet_endpoint( # Perform refund operation first, before modifying balance if key.refund_address: - await refund_balance(remaining_balance_msats, key, session) - result = {"recipient": key.refund_address, "msats": remaining_balance_msats} + await send_to_lnurl(remaining_balance_msats, "msat", key.refund_address) + result = {"recipient": key.refund_address, "msat": remaining_balance_msats} else: # Convert msats to sats for cashu wallet remaining_balance_sats = remaining_balance_msats // 1000 @@ -76,15 +71,13 @@ async def refund_wallet_endpoint( status_code=400, detail="Balance too small to refund (less than 1 sat)" ) - token = await wallet().send(remaining_balance_sats) + # TODO: choose currency and mint based on what user has configured + token = await send_token(remaining_balance_sats, "sat") result = {"msats": remaining_balance_msats, "recipient": None, "token": token} - # Only after successful refund, zero out the balance - key.balance = 0 - session.add(key) + await session.delete(key) await session.commit() - await delete_key_if_zero_balance(key, session) return result diff --git a/router/admin.py b/router/admin.py index 2435f4d2..856bffa9 100644 --- a/router/admin.py +++ b/router/admin.py @@ -1,16 +1,21 @@ import os from datetime import datetime, timezone -from fastapi import APIRouter, Request +from fastapi import APIRouter, HTTPException, Request from fastapi.responses import HTMLResponse +from pydantic import BaseModel from sqlmodel import select -from .cashu import wallet from .db import ApiKey, create_session +from .wallet import get_balance, send_token admin_router = APIRouter(prefix="/admin") +class WithdrawRequest(BaseModel): + amount: int + + def login_form() -> str: return """ @@ -112,7 +117,7 @@ async def dashboard(request: Request) -> str: # avoid rounding issues. total_user_balance = sum(key.balance for key in api_keys) // 1000 # Fetch balance from cashu - current_balance = await wallet().get_balance() + current_balance = await get_balance("sat") owner_balance = current_balance - total_user_balance return f""" @@ -128,7 +133,192 @@ async def dashboard(request: Request) -> str: padding: 8px; text-align: left; }} + button {{ + padding: 8px 16px; + cursor: pointer; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + margin-right: 10px; + }} + button:hover {{ + background-color: #0056b3; + }} + button:disabled {{ + background-color: #6c757d; + cursor: not-allowed; + }} + #token-result {{ + margin-top: 20px; + padding: 15px; + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 4px; + word-break: break-all; + display: none; + max-width: 100%; + }} + #token-text {{ + font-family: monospace; + font-size: 12px; + background-color: #e9ecef; + padding: 10px; + border-radius: 4px; + margin: 10px 0; + }} + .copy-btn {{ + background-color: #28a745; + padding: 4px 8px; + font-size: 12px; + }} + .copy-btn:hover {{ + background-color: #1e7e34; + }} + .refresh-btn {{ + background-color: #ffc107; + color: black; + }} + .refresh-btn:hover {{ + background-color: #e0a800; + }} + .modal {{ + display: none; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.4); + }} + .modal-content {{ + background-color: #fefefe; + margin: 15% auto; + padding: 20px; + border: 1px solid #888; + width: 300px; + border-radius: 8px; + text-align: center; + }} + .close {{ + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + cursor: pointer; + }} + .close:hover {{ + color: black; + }} + input[type="number"] {{ + width: 100%; + padding: 8px; + margin: 10px 0; + border: 1px solid #ddd; + border-radius: 4px; + }} + .warning {{ + color: #dc3545; + font-weight: bold; + margin: 10px 0; + }} +

Admin Dashboard

@@ -137,6 +327,37 @@ async def dashboard(request: Request) -> str:

The balance is calculated by subtracting the combined user balance from the total Cashu wallet balance.

Total Cashu Balance: {current_balance} sats

User Balance: {total_user_balance} sats

+ + + + + + +
+ Withdrawal Token: +
+ +

Save this token! It represents your withdrawn balance.

+
+

User's API Keys

@@ -160,3 +381,25 @@ async def admin(request: Request) -> str: if admin_cookie and admin_cookie == os.getenv("ADMIN_PASSWORD"): return await dashboard(request) return admin_auth() + + +@admin_router.post("/withdraw") +async def withdraw( + request: Request, withdraw_request: WithdrawRequest +) -> dict[str, str]: + admin_cookie = request.cookies.get("admin_password") + if not admin_cookie or admin_cookie != os.getenv("ADMIN_PASSWORD"): + raise HTTPException(status_code=403, detail="Unauthorized") + + current_balance = await get_balance("sat") + + if withdraw_request.amount <= 0: + raise HTTPException( + status_code=400, detail="Withdrawal amount must be positive" + ) + + if withdraw_request.amount > current_balance: + raise HTTPException(status_code=400, detail="Insufficient wallet balance") + + token = await send_token(withdraw_request.amount, "sat") + return {"token": token} diff --git a/router/auth.py b/router/auth.py index bca663ec..b51a069d 100644 --- a/router/auth.py +++ b/router/auth.py @@ -4,18 +4,18 @@ from typing import Optional from fastapi import HTTPException from sqlmodel import col, update -from .cashu import credit_balance from .db import ApiKey, AsyncSession -from .models import MODELS +from .logging import get_logger from .payment.cost_caculation import ( - COST_PER_REQUEST, - MODEL_BASED_PRICING, CostData, CostDataError, MaxCostData, calculate_cost, ) from .payment.helpers import get_max_cost_for_model +from .wallet import credit_balance + +logger = get_logger(__name__) # TODO: implement prepaid api key (not like it was before) # PREPAID_API_KEY = os.environ.get("PREPAID_API_KEY", None) @@ -33,7 +33,19 @@ async def validate_bearer_key( If it's a cashu key, it redeems it and stores its hash and balance. Otherwise checks if the hash of the key exists. """ + logger.debug( + "Starting bearer key validation", + extra={ + "key_preview": bearer_key[:20] + "..." + if len(bearer_key) > 20 + else bearer_key, + "has_refund_address": bool(refund_address), + "has_expiry_time": bool(key_expiry_time), + }, + ) + if not bearer_key: + logger.error("Empty bearer key provided") raise HTTPException( status_code=401, detail={ @@ -46,23 +58,108 @@ async def validate_bearer_key( ) if bearer_key.startswith("sk-"): + logger.debug( + "Processing sk- prefixed API key", + extra={"key_preview": bearer_key[:10] + "..."}, + ) + if existing_key := await session.get(ApiKey, bearer_key[3:]): + logger.info( + "Existing sk- API key found", + extra={ + "key_hash": existing_key.hashed_key[:8] + "...", + "balance": existing_key.balance, + "total_requests": existing_key.total_requests, + }, + ) + if key_expiry_time is not None: existing_key.key_expiry_time = key_expiry_time + logger.debug( + "Updated key expiry time", + extra={ + "key_hash": existing_key.hashed_key[:8] + "...", + "expiry_time": key_expiry_time, + }, + ) + if refund_address is not None: existing_key.refund_address = refund_address + logger.debug( + "Updated refund address", + extra={ + "key_hash": existing_key.hashed_key[:8] + "...", + "refund_address_preview": refund_address[:20] + "..." + if len(refund_address) > 20 + else refund_address, + }, + ) + return existing_key + else: + logger.warning( + "sk- API key not found in database", + extra={"key_preview": bearer_key[:10] + "..."}, + ) if bearer_key.startswith("cashu"): + logger.debug( + "Processing Cashu token", + extra={ + "token_preview": bearer_key[:20] + "...", + "token_type": bearer_key[:6] if len(bearer_key) >= 6 else bearer_key, + }, + ) + try: hashed_key = hashlib.sha256(bearer_key.encode()).hexdigest() + logger.debug( + "Generated token hash", extra={"hash_preview": hashed_key[:16] + "..."} + ) + if existing_key := await session.get(ApiKey, hashed_key): + logger.info( + "Existing Cashu token found", + extra={ + "key_hash": existing_key.hashed_key[:8] + "...", + "balance": existing_key.balance, + "total_requests": existing_key.total_requests, + }, + ) + if key_expiry_time is not None: existing_key.key_expiry_time = key_expiry_time + logger.debug( + "Updated key expiry time for existing Cashu key", + extra={ + "key_hash": existing_key.hashed_key[:8] + "...", + "expiry_time": key_expiry_time, + }, + ) + if refund_address is not None: existing_key.refund_address = refund_address + logger.debug( + "Updated refund address for existing Cashu key", + extra={ + "key_hash": existing_key.hashed_key[:8] + "...", + "refund_address_preview": refund_address[:20] + "..." + if len(refund_address) > 20 + else refund_address, + }, + ) + return existing_key + logger.info( + "Creating new Cashu token entry", + extra={ + "hash_preview": hashed_key[:16] + "...", + "has_refund_address": bool(refund_address), + "has_expiry_time": bool(key_expiry_time), + }, + ) + new_key = ApiKey( hashed_key=hashed_key, balance=0, @@ -71,14 +168,44 @@ async def validate_bearer_key( ) session.add(new_key) await session.flush() + + logger.debug( + "New key created, starting token redemption", + extra={"key_hash": hashed_key[:8] + "..."}, + ) + msats = await credit_balance(bearer_key, new_key, session) if msats <= 0: + logger.error( + "Token redemption returned zero or negative amount", + extra={"msats": msats, "key_hash": hashed_key[:8] + "..."}, + ) raise Exception("Token redemption failed") + await session.refresh(new_key) await session.commit() + + logger.info( + "New Cashu token successfully redeemed and stored", + extra={ + "key_hash": hashed_key[:8] + "...", + "redeemed_msats": msats, + "final_balance": new_key.balance, + }, + ) + return new_key except Exception as e: - print(f"Redemption failed: {e}") + logger.error( + "Cashu token redemption failed", + extra={ + "error": str(e), + "error_type": type(e).__name__, + "token_preview": bearer_key[:20] + "..." + if len(bearer_key) > 20 + else bearer_key, + }, + ) raise HTTPException( status_code=401, detail={ @@ -89,6 +216,17 @@ async def validate_bearer_key( } }, ) + + logger.error( + "Invalid API key format", + extra={ + "key_preview": bearer_key[:10] + "..." + if len(bearer_key) > 10 + else bearer_key, + "key_length": len(bearer_key), + }, + ) + raise HTTPException( status_code=401, detail={ @@ -101,17 +239,34 @@ async def validate_bearer_key( ) -async def pay_for_request( - key: ApiKey, - session: AsyncSession, - body: dict, -) -> None: - # Use global COST_PER_REQUEST as default, override if model-based pricing is enabled - cost_per_request = COST_PER_REQUEST - if MODEL_BASED_PRICING and MODELS: - cost_per_request = get_max_cost_for_model(model=body["model"]) +async def pay_for_request(key: ApiKey, session: AsyncSession, body: dict) -> int: + """Process payment for a request.""" + model = body["model"] + cost_per_request = get_max_cost_for_model(model=model) + + logger.info( + "Processing payment for request", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "current_balance": key.balance, + "required_cost": cost_per_request, + "model": model, + "sufficient_balance": key.balance >= cost_per_request, + }, + ) if key.balance < cost_per_request: + logger.warning( + "Insufficient balance for request", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "balance": key.balance, + "required": cost_per_request, + "shortfall": cost_per_request - key.balance, + "model": model, + }, + ) + raise HTTPException( status_code=402, detail={ @@ -123,6 +278,15 @@ async def pay_for_request( }, ) + logger.debug( + "Charging base cost for request", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "cost": cost_per_request, + "balance_before": key.balance, + }, + ) + # Charge the base cost for the request atomically to avoid race conditions stmt = ( update(ApiKey) @@ -136,7 +300,17 @@ async def pay_for_request( ) result = await session.exec(stmt) # type: ignore[call-overload] await session.commit() + if result.rowcount == 0: + logger.error( + "Concurrent request depleted balance", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "required_cost": cost_per_request, + "current_balance": key.balance, + }, + ) + # Another concurrent request spent the balance first raise HTTPException( status_code=402, @@ -148,6 +322,50 @@ async def pay_for_request( } }, ) + + await session.refresh(key) + + logger.info( + "Payment processed successfully", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "charged_amount": cost_per_request, + "new_balance": key.balance, + "total_spent": key.total_spent, + "total_requests": key.total_requests, + "model": model, + }, + ) + + return cost_per_request + + +async def revert_pay_for_request( + key: ApiKey, session: AsyncSession, cost_per_request: int +) -> None: + stmt = ( + update(ApiKey) + .where(col(ApiKey.hashed_key) == key.hashed_key) + .values( + balance=col(ApiKey.balance) + cost_per_request, + total_spent=col(ApiKey.total_spent) - cost_per_request, + total_requests=col(ApiKey.total_requests) - 1, + ) + ) + + result = await session.exec(stmt) # type: ignore[call-overload] + await session.commit() + if result.rowcount == 0: + raise HTTPException( + status_code=402, + detail={ + "error": { + "message": f"failed to revert request payment: {cost_per_request} mSats required. {key.balance} available.", + "type": "payment_error", + "code": "payment_error", + } + }, + ) await session.refresh(key) @@ -159,25 +377,81 @@ async def adjust_payment_for_tokens( This is called after the initial payment and the upstream request is complete. Returns cost data to be included in the response. """ - max_cost = get_max_cost_for_model(model=response_data["model"]) + model = response_data.get("model", "unknown") + max_cost = get_max_cost_for_model(model=model) + + logger.debug( + "Starting payment adjustment for tokens", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "model": model, + "max_cost": max_cost, + "current_balance": key.balance, + "has_usage": "usage" in response_data, + }, + ) match calculate_cost(response_data, max_cost): case MaxCostData() as cost: + logger.debug( + "Using max cost data (no token adjustment)", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "model": model, + "max_cost": cost.total_msats, + }, + ) return cost.dict() + case CostData() as cost: # If token-based pricing is enabled and base cost is 0, use token-based cost # Otherwise, token cost is additional to the base cost cost_difference = cost.total_msats - max_cost + logger.info( + "Calculated token-based cost", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "model": model, + "token_cost": cost.total_msats, + "max_cost": max_cost, + "cost_difference": cost_difference, + "input_msats": cost.input_msats, + "output_msats": cost.output_msats, + }, + ) + if cost_difference == 0: + logger.debug( + "No cost adjustment needed", + extra={"key_hash": key.hashed_key[:8] + "...", "model": model}, + ) await session.commit() return cost.dict() if cost_difference > 0: # Need to charge more + logger.info( + "Additional charge required for token usage", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "additional_charge": cost_difference, + "current_balance": key.balance, + "sufficient_balance": key.balance >= cost_difference, + "model": model, + }, + ) + if key.balance < cost_difference: - print( - f"Warning: Insufficient balance for token-based pricing adjustment: {key.hashed_key[:10]}..." + logger.warning( + "Insufficient balance for token-based pricing adjustment", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "required": cost_difference, + "available": key.balance, + "shortfall": cost_difference - key.balance, + "model": model, + }, ) await session.commit() else: @@ -192,12 +466,43 @@ async def adjust_payment_for_tokens( ) result = await session.exec(charge_stmt) # type: ignore[call-overload] await session.commit() + if result.rowcount: cost.total_msats = max_cost + cost_difference await session.refresh(key) + + logger.info( + "Additional charge applied successfully", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "charged_amount": cost_difference, + "new_balance": key.balance, + "total_cost": cost.total_msats, + "model": model, + }, + ) + else: + logger.warning( + "Failed to apply additional charge (concurrent operation)", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "attempted_charge": cost_difference, + "model": model, + }, + ) else: # Refund some of the base cost refund = abs(cost_difference) + logger.info( + "Refunding excess payment", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "refund_amount": refund, + "current_balance": key.balance, + "model": model, + }, + ) + refund_stmt = ( update(ApiKey) .where(col(ApiKey.hashed_key) == key.hashed_key) @@ -211,8 +516,30 @@ async def adjust_payment_for_tokens( cost.total_msats = max_cost - refund await session.refresh(key) + logger.info( + "Refund processed successfully", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "refunded_amount": refund, + "new_balance": key.balance, + "final_cost": cost.total_msats, + "model": model, + }, + ) + return cost.dict() + case CostDataError() as error: + logger.error( + "Cost calculation error during payment adjustment", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "model": model, + "error_message": error.message, + "error_code": error.code, + }, + ) + raise HTTPException( status_code=400, detail={ diff --git a/router/cashu.py b/router/cashu.py deleted file mode 100644 index 3934db0c..00000000 --- a/router/cashu.py +++ /dev/null @@ -1,206 +0,0 @@ -import asyncio -import os -import time -from typing import cast - -from sixty_nuts import Wallet -from sixty_nuts.mint import CurrencyUnit -from sqlmodel import col, func, select, update - -from .db import ApiKey, AsyncSession, get_session - -RECEIVE_LN_ADDRESS = os.environ["RECEIVE_LN_ADDRESS"] -MINT = os.environ.get("MINT", "https://mint.minibits.cash/Bitcoin") -MINIMUM_PAYOUT = int(os.environ.get("MINIMUM_PAYOUT", 100)) -REFUND_PROCESSING_INTERVAL = int(os.environ.get("REFUND_PROCESSING_INTERVAL", 3600)) -PAYOUT_INTERVAL = int(os.environ.get("PAYOUT_INTERVAL", 300)) # Default 5 minutes -DEV_LN_ADDRESS = "routstr@minibits.cash" -DEVS_DONATION_RATE = float(os.environ.get("DEVS_DONATION_RATE", 0.021)) # 2.1% -NSEC = os.environ["NSEC"] # Nostr private key for the wallet -CURRENCY = cast(CurrencyUnit, os.environ.get("CURRENCY", "sat")) - -wallet_instance: Wallet | None = None - - -async def init_wallet() -> None: - global wallet_instance - wallet_instance = await Wallet.create(nsec=NSEC) - - -def wallet() -> Wallet: - global wallet_instance - if wallet_instance is None: - raise ValueError("Wallet not initialized") - return wallet_instance - - -async def delete_key_if_zero_balance(key: ApiKey, session: AsyncSession) -> None: - """Delete the given API key if its balance is zero.""" - if key.balance == 0: - await session.delete(key) - await session.commit() - - -async def pay_out() -> None: - """ - Calculates the pay-out amount based on the spent balance, profit, and donation rate. - """ - try: - from .db import create_session - - async with create_session() as session: - result = await session.exec( - select(func.sum(col(ApiKey.balance))).where(ApiKey.balance > 0) - ) - balance = result.one_or_none() - if not balance: - # No balance to pay out - this is OK, not an error - return - - user_balance_sats = balance // 1000 - wallet_balance_sats = await wallet().get_balance() - - # Handle edge cases more gracefully - if wallet_balance_sats < user_balance_sats: - print( - f"Warning: Wallet balance ({wallet_balance_sats} sats) is less than user balance ({user_balance_sats} sats). Skipping payout." - ) - return - - if (revenue := wallet_balance_sats - user_balance_sats) <= MINIMUM_PAYOUT: - # Not enough revenue yet - this is OK - return - - devs_donation = int(revenue * DEVS_DONATION_RATE) - owners_draw = revenue - devs_donation - - # Send payouts - await wallet().send_to_lnurl(RECEIVE_LN_ADDRESS, owners_draw) - await wallet().send_to_lnurl(DEV_LN_ADDRESS, devs_donation) - - except Exception as e: - print(f"Error in pay_out: {e}") - - -# Periodic payout task -async def periodic_payout() -> None: - while True: - try: - await asyncio.sleep(300) # Run every 5 minutes - await pay_out() - except asyncio.CancelledError: - break - except Exception as e: - print(f"Error in periodic payout: {e}") - # Continue running even if payout fails - - -async def credit_balance(cashu_token: str, key: ApiKey, session: AsyncSession) -> int: - """Redeem a Cashu token and credit the amount to the API key balance.""" - try: - amount_sats, _ = await wallet().redeem(cashu_token) - except Exception as e: - print(f"Error in credit_balance: {e}") - # Ensure the balance cannot become negative if redeem fails - return 0 - - if amount_sats <= 0: - return 0 - - amount_msats = amount_sats * 1000 - - # Apply the balance change atomically to avoid race conditions when topping - # up the same key concurrently. - stmt = ( - update(ApiKey) - .where(col(ApiKey.hashed_key) == key.hashed_key) - .values(balance=col(ApiKey.balance) + amount_msats) - ) - await session.exec(stmt) # type: ignore[call-overload] - await session.commit() - await session.refresh(key) - - return amount_msats - - -async def check_for_refunds() -> None: - """ - Periodically checks for API keys that are eligible for refunds and processes them. - - Raises: - Exception: If an error occurs during the refund check process. - """ - # Setting REFUND_PROCESSING_INTERVAL to 0 disables it - if REFUND_PROCESSING_INTERVAL == 0: - print("Automatic refund processing is disabled.") - return - - while True: - try: - async for session in get_session(): - result = await session.exec(select(ApiKey)) - keys = result.all() - current_time = int(time.time()) - for key in keys: - if ( - key.balance > 0 - and key.refund_address - and key.key_expiry_time - and key.key_expiry_time < current_time - ): - print( - f" DEBUG Refunding key {key.hashed_key[:3] + '[...]' + key.hashed_key[-3:]}, Current Time: {current_time}, Expirary Time: {key.key_expiry_time}", - flush=True, - ) - await refund_balance(key.balance, key, session) - await delete_key_if_zero_balance(key, session) - - # Sleep for the specified interval before checking again - await asyncio.sleep(REFUND_PROCESSING_INTERVAL) - except asyncio.CancelledError: - break - except Exception as e: - print(f"Error during refund check: {e}") - - -async def refund_balance(amount_msats: int, key: ApiKey, session: AsyncSession) -> int: - if amount_msats <= 0: - amount_msats = key.balance - - # Convert msats to sats for cashu wallet - amount_sats = amount_msats // 1000 - if amount_sats == 0: - raise ValueError("Amount too small to refund (less than 1 sat)") - - # Atomically deduct the balance to avoid race conditions when multiple - # refunds are triggered concurrently. - stmt = ( - update(ApiKey) - .where(col(ApiKey.hashed_key) == key.hashed_key) - .where(col(ApiKey.balance) >= amount_msats) - .values(balance=col(ApiKey.balance) - amount_msats) - ) - result = await session.exec(stmt) # type: ignore[call-overload] - await session.commit() - if result.rowcount == 0: - raise ValueError("Insufficient balance.") - await session.refresh(key) - await delete_key_if_zero_balance(key, session) - - if key.refund_address is None: - raise ValueError("Refund address not set.") - - return await wallet().send_to_lnurl(key.refund_address, amount=amount_sats) - - -async def x_cashu_refund(key: ApiKey, session: AsyncSession) -> str: - refund_token = await wallet().send(key.balance) - await session.delete(key) - await session.commit() - return refund_token - - -async def redeem(cashu_token: str, lnurl: str) -> int: - amount_sats, _ = await wallet().redeem(cashu_token) - await wallet().send_to_lnurl(lnurl, amount=amount_sats) - return amount_sats diff --git a/router/logging.py b/router/logging.py new file mode 100644 index 00000000..5a4b019c --- /dev/null +++ b/router/logging.py @@ -0,0 +1,269 @@ +import logging.config +import logging.handlers +import os +import re +import tomllib +from datetime import datetime +from pathlib import Path +from typing import Any + +from pythonjsonlogger import jsonlogger + + +class DailyRotatingFileHandler(logging.handlers.TimedRotatingFileHandler): + """Custom TimedRotatingFileHandler that creates date-based filenames.""" + + def __init__(self, filename: str, **kwargs: Any) -> None: + """Initialize with a base filename pattern.""" + self.base_dir = os.path.dirname(filename) + self.base_name = os.path.basename(filename).replace(".log", "") + + today = datetime.now().strftime("%Y-%m-%d") + self.current_date = today + dated_filename = os.path.join(self.base_dir, f"{self.base_name}_{today}.log") + + super().__init__(dated_filename, **kwargs) + + def doRollover(self) -> None: + """Override rollover to create new date-based filename.""" + if self.stream: + self.stream.close() + + new_date = datetime.now().strftime("%Y-%m-%d") + new_filename = os.path.join(self.base_dir, f"{self.base_name}_{new_date}.log") + + self.baseFilename = new_filename + self.current_date = new_date + + # FIX ME: not sure if we need this + # self._cleanup_old_files() + + if not self.delay: + self.stream = self._open() + + def _cleanup_old_files(self) -> None: + """Remove old log files beyond backupCount.""" + if self.backupCount > 0: + log_files = [] + if os.path.exists(self.base_dir): + for file in os.listdir(self.base_dir): + if file.startswith(f"{self.base_name}_") and file.endswith(".log"): + file_path = os.path.join(self.base_dir, file) + log_files.append((file_path, os.path.getmtime(file_path))) + + log_files.sort(key=lambda x: x[1], reverse=True) + + for file_path, _ in log_files[self.backupCount :]: + try: + os.remove(file_path) + except OSError: + pass + + +def get_package_version() -> str: + """Read the package version from pyproject.toml.""" + try: + # Find project root by looking for pyproject.toml + current_path = Path(__file__).parent + while current_path != current_path.parent: + pyproject_path = current_path / "pyproject.toml" + if pyproject_path.exists(): + with open(pyproject_path, "rb") as f: + pyproject_data = tomllib.load(f) + version = pyproject_data.get("project", {}).get("version", "unknown") + return version + current_path = current_path.parent + + # Fallback: try the simple path resolution (3 levels up for router/logging/logging_config.py) + pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml" + if pyproject_path.exists(): + with open(pyproject_path, "rb") as f: + pyproject_data = tomllib.load(f) + version = pyproject_data.get("project", {}).get("version", "unknown") + return version + + return "unknown" + except Exception: + return "unknown" + + +class VersionFilter(logging.Filter): + """Filter to add package version to all log records.""" + + def __init__(self) -> None: + super().__init__() + self.version = get_package_version() + + def filter(self, record: logging.LogRecord) -> bool: + """Add version information to the log record.""" + record.version = self.version + return True + + +class SecurityFilter(logging.Filter): + """Filter to remove sensitive information from logs.""" + + SENSITIVE_KEYS = { + "authorization", + "x-cashu", + "bearer", + "token", + "key", + "secret", + "password", + "cashu_token", + "bearer_key", + "api_key", + "nsec", + "upstream_api_key", + "refund_address", + } + + def filter(self, record: logging.LogRecord) -> bool: + """Filter out sensitive information from log records.""" + try: + message = record.getMessage() + + for key in self.SENSITIVE_KEYS: + if key in message.lower(): + patterns = [ + rf"{key}[:\s=]+([a-zA-Z0-9_\-\.]+)", # key: value or key=value + rf'{key}[:\s=]+["\']([^"\']+)["\']', # key: "value" or key='value' + r"Bearer\s+([a-zA-Z0-9_\-\.]+)", # Bearer token + r"cashu[A-Z]+([a-zA-Z0-9_\-\.=/+]+)", # Cashu tokens + ] + + for pattern in patterns: + message = re.sub( + pattern, f"{key}: [REDACTED]", message, flags=re.IGNORECASE + ) + + record.msg = message + record.args = () + + except Exception: + pass + + return True + + +def get_log_level() -> str: + """Get log level from environment variable.""" + return os.environ.get("LOG_LEVEL", "INFO").upper() + + +def should_enable_console_logging() -> bool: + """Check if console logging should be enabled.""" + return os.environ.get("ENABLE_CONSOLE_LOGGING", "true").lower() in ( + "true", + "1", + "yes", + ) + + +def setup_logging() -> None: + """Configure centralized logging for the application.""" + + log_level = get_log_level() + console_enabled = should_enable_console_logging() + + # Determine which handlers to use + handlers = ["file"] + if console_enabled: + handlers.append("console") + + LOGGING_CONFIG = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "json": { + "()": jsonlogger.JsonFormatter, + "format": "%(asctime)s %(name)s %(levelname)s %(message)s %(pathname)s %(lineno)d %(version)s", + "datefmt": "%Y-%m-%d %H:%M:%S", + }, + "standard": { + "format": "%(asctime)s [%(levelname)s] %(name)s v%(version)s: %(message)s", + "datefmt": "%Y-%m-%d %H:%M:%S", + }, + }, + "filters": { + "version_filter": {"()": VersionFilter}, + "security_filter": {"()": SecurityFilter}, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "level": log_level, + "formatter": "standard", + "stream": "ext://sys.stdout", + "filters": ["version_filter", "security_filter"], + }, + "file": { + "()": DailyRotatingFileHandler, + "level": log_level, + "formatter": "json", + "filename": "logs/app.log", + "when": "midnight", # Rotate at midnight each day + "interval": 1, # Every 1 day + "backupCount": 30, # Keep 30 days of logs + "atTime": None, # Rotate at midnight (00:00) + "filters": ["version_filter", "security_filter"], + }, + }, + "loggers": { + "router": { + "level": log_level, + "handlers": handlers, + "propagate": False, + }, + "router.payment": { + "level": log_level, + "handlers": handlers, + "propagate": False, + }, + "router.cashu": { + "level": log_level, + "handlers": handlers, + "propagate": False, + }, + "router.proxy": { + "level": log_level, + "handlers": handlers, + "propagate": False, + }, + "router.auth": { + "level": log_level, + "handlers": handlers, + "propagate": False, + }, + # Suppress verbose third-party logging + "httpx": { + "level": "WARNING", + "handlers": ["console"] if console_enabled else [], + "propagate": False, + }, + "httpcore": { + "level": "WARNING", + "handlers": ["console"] if console_enabled else [], + "propagate": False, + }, + "uvicorn.access": { + "level": "WARNING", + "handlers": ["console"] if console_enabled else [], + "propagate": False, + }, + }, + "root": { + "level": log_level, + "handlers": ["console"] if console_enabled else [], + }, + } + + os.makedirs("logs", exist_ok=True) + + logging.config.dictConfig(LOGGING_CONFIG) + + +def get_logger(name: str) -> logging.Logger: + """Get a logger instance for the given module name.""" + return logging.getLogger(name) diff --git a/router/main.py b/router/main.py index b7443b94..9e1b87fa 100644 --- a/router/main.py +++ b/router/main.py @@ -8,33 +8,64 @@ from fastapi.middleware.cors import CORSMiddleware from .account import wallet_router from .admin import admin_router -from .cashu import check_for_refunds, init_wallet, periodic_payout from .db import init_db from .discovery import providers_router +from .logging import get_logger, setup_logging from .models import MODELS, models_router, update_sats_pricing from .proxy import proxy_router +from .wallet import check_for_refunds, periodic_payout + +# Initialize logging first +setup_logging() +logger = get_logger(__name__) __version__ = "0.0.1" @asynccontextmanager async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: - await init_db() - await init_wallet() - - pricing_task = asyncio.create_task(update_sats_pricing()) - refund_task = asyncio.create_task(check_for_refunds()) - payout_task = asyncio.create_task(periodic_payout()) + logger.info("Application startup initiated", extra={"version": __version__}) try: + await init_db() + logger.info("Database initialized successfully") + + logger.info("Wallet initialized successfully") + + pricing_task = asyncio.create_task(update_sats_pricing()) + refund_task = asyncio.create_task(check_for_refunds()) + payout_task = asyncio.create_task(periodic_payout()) + + logger.info( + "Background tasks started successfully", + extra={"tasks": ["pricing", "refunds", "payouts"]}, + ) + yield + + except Exception as e: + logger.error( + "Application startup failed", + extra={"error": str(e), "error_type": type(e).__name__}, + ) + raise finally: + logger.info("Application shutdown initiated") + refund_task.cancel() pricing_task.cancel() payout_task.cancel() - await asyncio.gather( - pricing_task, refund_task, payout_task, return_exceptions=True - ) + + try: + await asyncio.gather( + pricing_task, refund_task, payout_task, return_exceptions=True + ) + logger.info("Background tasks stopped successfully") + except Exception as e: + logger.error( + "Error stopping background tasks", + extra={"error": str(e), "error_type": type(e).__name__}, + ) app = FastAPI( @@ -54,9 +85,15 @@ app.add_middleware( allow_headers=["*"], ) +logger.info( + "CORS middleware configured", + extra={"allowed_origins": os.environ.get("CORS_ORIGINS", "*").split(",")}, +) + @app.get("/") async def info() -> dict: + logger.info("Info endpoint accessed") return { "name": app.title, "description": app.description, @@ -74,3 +111,8 @@ app.include_router(admin_router) app.include_router(wallet_router) app.include_router(providers_router) app.include_router(proxy_router) + +logger.info( + "Application initialized successfully", + extra={"version": __version__, "routers_count": 5}, +) diff --git a/router/models.py b/router/models.py index ef545c81..e9af0e85 100644 --- a/router/models.py +++ b/router/models.py @@ -144,6 +144,7 @@ async def update_sats_pricing() -> None: break +@models_router.get("/models") @models_router.get("/v1/models") async def models() -> dict: return {"data": MODELS} diff --git a/router/payment/cost_caculation.py b/router/payment/cost_caculation.py index fd1c64f0..2f2bf9da 100644 --- a/router/payment/cost_caculation.py +++ b/router/payment/cost_caculation.py @@ -2,7 +2,10 @@ import os from pydantic import BaseModel -from router.models import MODELS +from ..logging import get_logger +from ..models import MODELS + +logger = get_logger(__name__) COST_PER_REQUEST = ( int(os.environ.get("COST_PER_REQUEST", "1")) * 1000 @@ -15,6 +18,16 @@ COST_PER_1K_OUTPUT_TOKENS = ( ) # Convert to msats MODEL_BASED_PRICING = os.environ.get("MODEL_BASED_PRICING", "false").lower() == "true" +logger.info( + "Cost calculation initialized", + extra={ + "cost_per_request_msats": COST_PER_REQUEST, + "cost_per_1k_input_tokens_msats": COST_PER_1K_INPUT_TOKENS, + "cost_per_1k_output_tokens_msats": COST_PER_1K_OUTPUT_TOKENS, + "model_based_pricing": MODEL_BASED_PRICING, + }, +) + class CostData(BaseModel): base_msats: int @@ -35,6 +48,25 @@ class CostDataError(BaseModel): def calculate_cost( response_data: dict, max_cost: int ) -> CostData | MaxCostData | CostDataError: + """ + Calculate the cost of an API request based on token usage. + + Args: + response_data: Response data containing usage information + max_cost: Maximum cost in millisats + + Returns: + Cost data or error information + """ + logger.debug( + "Starting cost calculation", + extra={ + "max_cost_msats": max_cost, + "has_usage_data": "usage" in response_data, + "response_model": response_data.get("model", "unknown"), + }, + ) + cost_data = MaxCostData( base_msats=max_cost, input_msats=0, @@ -43,7 +75,13 @@ def calculate_cost( ) if "usage" not in response_data or response_data["usage"] is None: - print("No usage data in response, using base cost only") + logger.warning( + "No usage data in response, using base cost only", + extra={ + "max_cost_msats": max_cost, + "model": response_data.get("model", "unknown"), + }, + ) return cost_data MSATS_PER_1K_INPUT_TOKENS = COST_PER_1K_INPUT_TOKENS @@ -51,7 +89,22 @@ def calculate_cost( if MODEL_BASED_PRICING and MODELS: response_model = response_data.get("model", "") + logger.debug( + "Using model-based pricing", + extra={ + "model": response_model, + "available_models": [model.id for model in MODELS], + }, + ) + if response_model not in [model.id for model in MODELS]: + logger.error( + "Invalid model in response", + extra={ + "response_model": response_model, + "available_models": [model.id for model in MODELS], + }, + ) return CostDataError( message=f"Invalid model in response: {response_model}", code="model_not_found", @@ -59,6 +112,10 @@ def calculate_cost( model = next(model for model in MODELS if model.id == response_model) if model.sats_pricing is None: + logger.error( + "Model pricing not defined", + extra={"model": response_model, "model_id": model.id}, + ) return CostDataError( message="Model pricing not defined", code="pricing_not_found" ) @@ -66,8 +123,23 @@ def calculate_cost( MSATS_PER_1K_INPUT_TOKENS = model.sats_pricing.prompt * 1_000_000 # type: ignore MSATS_PER_1K_OUTPUT_TOKENS = model.sats_pricing.completion * 1_000_000 # type: ignore + logger.info( + "Applied model-specific pricing", + extra={ + "model": response_model, + "input_price_msats_per_1k": MSATS_PER_1K_INPUT_TOKENS, + "output_price_msats_per_1k": MSATS_PER_1K_OUTPUT_TOKENS, + }, + ) + if not (MSATS_PER_1K_OUTPUT_TOKENS and MSATS_PER_1K_INPUT_TOKENS): - # If no token pricing is configured, just return base cost + logger.warning( + "No token pricing configured, using base cost", + extra={ + "base_cost_msats": max_cost, + "model": response_data.get("model", "unknown"), + }, + ) return cost_data input_tokens = response_data.get("usage", {}).get("prompt_tokens", 0) @@ -77,6 +149,18 @@ def calculate_cost( output_msats = int(round(output_tokens / 1000 * MSATS_PER_1K_OUTPUT_TOKENS, 0)) token_based_cost = int(round(input_msats + output_msats, 0)) + logger.info( + "Calculated token-based cost", + extra={ + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "input_cost_msats": input_msats, + "output_cost_msats": output_msats, + "total_cost_msats": token_based_cost, + "model": response_data.get("model", "unknown"), + }, + ) + return CostData( base_msats=0, input_msats=input_msats, diff --git a/router/payment/helpers.py b/router/payment/helpers.py index efe4aa4e..92690723 100644 --- a/router/payment/helpers.py +++ b/router/payment/helpers.py @@ -6,76 +6,324 @@ from typing import Literal import cbor2 from fastapi import HTTPException, Response -from router.models import MODELS -from router.payment.cost_caculation import COST_PER_REQUEST +from ..logging import get_logger +from ..models import MODELS +from .cost_caculation import COST_PER_REQUEST, MODEL_BASED_PRICING + +logger = get_logger(__name__) UPSTREAM_BASE_URL = os.environ["UPSTREAM_BASE_URL"] UPSTREAM_API_KEY = os.environ.get("UPSTREAM_API_KEY", "") +logger.info( + "Payment helpers initialized", + extra={ + "upstream_base_url": UPSTREAM_BASE_URL, + "has_upstream_api_key": bool(UPSTREAM_API_KEY), + "model_based_pricing": MODEL_BASED_PRICING, + }, +) + + +def get_cost_per_request(model: str | None = None) -> int: + """Get the cost per request for a given model.""" + logger.debug( + "Calculating cost per request", + extra={ + "model": model, + "model_based_pricing": MODEL_BASED_PRICING, + "has_models": bool(MODELS), + }, + ) + + if MODEL_BASED_PRICING and MODELS and model: + cost = get_max_cost_for_model(model=model) + logger.debug( + "Using model-based cost", extra={"model": model, "cost_msats": cost} + ) + return cost + + logger.debug( + "Using default cost per request", extra={"cost_msats": COST_PER_REQUEST} + ) + return COST_PER_REQUEST + + +def check_token_balance(headers: dict, body: dict) -> Literal["sat", "msat"]: + """Check if the provided token has sufficient balance.""" + logger.debug( + "Checking token balance", + extra={ + "has_x_cashu": "x-cashu" in headers, + "has_authorization": "authorization" in headers, + "model": body.get("model", "unknown"), + }, + ) -def check_token_balance( - headers: dict, body: dict, unit: Literal["sat", "msat"] -) -> None: if x_cashu := headers.get("x-cashu", None): cashu_token = x_cashu + logger.debug( + "Using X-Cashu token", + extra={ + "token_preview": cashu_token[:20] + "..." + if len(cashu_token) > 20 + else cashu_token + }, + ) elif auth := headers.get("authorization", None): - cashu_token = auth.split(" ")[1] + cashu_token = auth.split(" ")[1] if len(auth.split(" ")) > 1 else "" + logger.debug( + "Using Authorization header token", + extra={ + "token_preview": cashu_token[:20] + "..." + if len(cashu_token) > 20 + else cashu_token + }, + ) else: + logger.error("No authentication token provided") raise HTTPException(status_code=401, detail="Unauthorized") - COST_PER_REQUEST = get_max_cost_for_model(model=body["model"]) + + # Handle empty token + if not cashu_token: + logger.error("Empty token provided") + raise HTTPException( + status_code=401, + detail={ + "error": { + "message": "API key or Cashu token required", + "type": "invalid_request_error", + "code": "missing_api_key", + } + }, + ) + + # Handle regular API keys (sk-*) + if cashu_token.startswith("sk-"): + logger.debug( + "Regular API key detected", extra={"key_preview": cashu_token[:10] + "..."} + ) + return "sat" + + cost = get_cost_per_request(model=body.get("model", None)) + if cashu_token.startswith("cashuA"): - _token = base64_token_json(cashu_token) - amount = sum(p["amount"] for t in _token["token"] for p in t["proofs"]) - unit = _token["unit"] - if unit == "msat": - pass - elif unit == "sat": - amount *= 1000 - if amount < COST_PER_REQUEST: - raise HTTPException(status_code=413, detail="Insufficient balance") + logger.debug("Processing CashuA token", extra={"required_cost_msats": cost}) + + try: + _token = base64_token_json(cashu_token) + amount = sum(p["amount"] for t in _token["token"] for p in t["proofs"]) + unit: Literal["sat", "msat"] = _token["unit"] + + if unit == "sat": + amount *= 1000 + + logger.info( + "CashuA token parsed successfully", + extra={ + "amount": amount, + "unit": _token["unit"], + "amount_msats": amount, + "required_cost_msats": cost, + "sufficient_balance": amount >= cost, + }, + ) + + if amount < cost: + logger.warning( + "Insufficient token balance", + extra={ + "amount_msats": amount, + "required_msats": cost, + "shortfall_msats": cost - amount, + "unit": unit, + }, + ) + raise HTTPException(status_code=413, detail="Insufficient balance") + + except Exception as e: + logger.error( + "Failed to parse CashuA token", + extra={ + "error": str(e), + "error_type": type(e).__name__, + "token_preview": cashu_token[:20] + "...", + }, + ) + raise HTTPException(status_code=401, detail="Invalid token format") + elif cashu_token.startswith("cashuB"): - _token = base64_token_cbor(cashu_token) - amount = sum(p["a"] for t in _token["t"] for p in t["p"]) - unit = _token["u"] - if unit == "sat": - amount *= 1000 - if amount < COST_PER_REQUEST: - raise HTTPException(status_code=413, detail="Insufficient balance") + logger.debug("Processing CashuB token", extra={"required_cost_msats": cost}) + + try: + _token = base64_token_cbor(cashu_token) + amount = sum(p["a"] for t in _token["t"] for p in t["p"]) + unit = _token["u"] + + if unit == "sat": + amount *= 1000 + + logger.info( + "CashuB token parsed successfully", + extra={ + "amount": amount, + "unit": unit, + "amount_msats": amount, + "required_cost_msats": cost, + "sufficient_balance": amount >= cost, + }, + ) + + if amount < cost: + logger.warning( + "Insufficient token balance", + extra={ + "amount_msats": amount, + "required_msats": cost, + "shortfall_msats": cost - amount, + "unit": unit, + }, + ) + raise HTTPException(status_code=413, detail="Insufficient balance") + + except Exception as e: + logger.error( + "Failed to parse CashuB token", + extra={ + "error": str(e), + "error_type": type(e).__name__, + "token_preview": cashu_token[:20] + "...", + }, + ) + raise HTTPException(status_code=401, detail="Invalid token format") + else: + logger.error( + "Unknown token format", + extra={"token_prefix": cashu_token[:10] if cashu_token else "empty"}, + ) raise HTTPException(status_code=401, detail="Unauthorized") + return unit + def base64_token_json(cashu_token: str) -> dict: - # Version 3 - JSON format - encoded = cashu_token[6:] # Remove "cashuA" - # Add correct padding – (-len) % 4 equals 0,1,2,3 - encoded += "=" * ((-len(encoded)) % 4) + """Decode a CashuA (JSON) token.""" + logger.debug("Decoding CashuA token", extra={"token_length": len(cashu_token)}) - decoded = base64.urlsafe_b64decode(encoded).decode() - token_data = json.loads(decoded) + try: + # Version 3 - JSON format + encoded = cashu_token[6:] # Remove "cashuA" + # Add correct padding – (-len) % 4 equals 0,1,2,3 + encoded += "=" * ((-len(encoded)) % 4) - return token_data + decoded = base64.urlsafe_b64decode(encoded).decode() + token_data = json.loads(decoded) + + logger.debug( + "CashuA token decoded successfully", + extra={ + "token_proofs_count": sum( + len(t.get("proofs", [])) for t in token_data.get("token", []) + ), + "unit": token_data.get("unit", "unknown"), + }, + ) + + return token_data + except Exception as e: + logger.error( + "Failed to decode CashuA token", + extra={"error": str(e), "error_type": type(e).__name__}, + ) + raise def base64_token_cbor(cashu_token: str) -> dict: - encoded = cashu_token[6:] # Remove "cashuB" - encoded += "=" * ((-len(encoded)) % 4) - decoded_bytes = base64.urlsafe_b64decode(encoded) - token_data = cbor2.loads(decoded_bytes) - return token_data + """Decode a CashuB (CBOR) token.""" + logger.debug("Decoding CashuB token", extra={"token_length": len(cashu_token)}) + + try: + encoded = cashu_token[6:] # Remove "cashuB" + encoded += "=" * ((-len(encoded)) % 4) + decoded_bytes = base64.urlsafe_b64decode(encoded) + token_data = cbor2.loads(decoded_bytes) + + logger.debug( + "CashuB token decoded successfully", + extra={ + "token_proofs_count": sum( + len(t.get("p", [])) for t in token_data.get("t", []) + ), + "unit": token_data.get("u", "unknown"), + }, + ) + + return token_data + except Exception as e: + logger.error( + "Failed to decode CashuB token", + extra={"error": str(e), "error_type": type(e).__name__}, + ) + raise def get_max_cost_for_model(model: str) -> int: - if model not in [model.id for model in MODELS]: + """Get the maximum cost for a specific model.""" + logger.debug( + "Getting max cost for model", + extra={ + "model": model, + "model_based_pricing": MODEL_BASED_PRICING, + "has_models": bool(MODELS), + }, + ) + + if not MODEL_BASED_PRICING or not MODELS: + logger.debug( + "Using default cost (no model-based pricing)", + extra={"cost_msats": COST_PER_REQUEST, "model": model}, + ) return COST_PER_REQUEST + + if model not in [model.id for model in MODELS]: + logger.warning( + "Model not found in available models", + extra={ + "requested_model": model, + "available_models": [m.id for m in MODELS], + "using_default_cost": COST_PER_REQUEST, + }, + ) + return COST_PER_REQUEST + for m in MODELS: if m.id == model: - return m.sats_pricing.max_cost * 1000 # type: ignore + max_cost = m.sats_pricing.max_cost * 1000 # type: ignore + logger.debug( + "Found model-specific max cost", + extra={"model": model, "max_cost_msats": max_cost}, + ) + return int(max_cost) + + logger.warning( + "Model pricing not found, using default", + extra={"model": model, "default_cost_msats": COST_PER_REQUEST}, + ) return COST_PER_REQUEST def create_error_response(error_type: str, message: str, status_code: int) -> Response: """Create a standardized error response.""" + logger.info( + "Creating error response", + extra={ + "error_type": error_type, + "error_message": message, + "status_code": status_code, + }, + ) + return Response( content=json.dumps( { @@ -93,20 +341,45 @@ def create_error_response(error_type: str, message: str, status_code: int) -> Re def prepare_upstream_headers(request_headers: dict) -> dict: """Prepare headers for upstream request, removing sensitive/problematic ones.""" + logger.debug( + "Preparing upstream headers", + extra={ + "original_headers_count": len(request_headers), + "has_upstream_api_key": bool(UPSTREAM_API_KEY), + }, + ) + headers = dict(request_headers) + # Remove headers that shouldn't be forwarded - headers.pop("host", None) - headers.pop("content-length", None) - headers.pop("refund-lnurl", None) - headers.pop("key-expiry-time", None) - headers.pop("x-cashu", None) + removed_headers = [] + for header in [ + "host", + "content-length", + "refund-lnurl", + "key-expiry-time", + "x-cashu", + ]: + if headers.pop(header, None) is not None: + removed_headers.append(header) # Handle authorization if UPSTREAM_API_KEY: headers["Authorization"] = f"Bearer {UPSTREAM_API_KEY}" - headers.pop("authorization", None) + if headers.pop("authorization", None) is not None: + removed_headers.append("authorization (replaced with upstream key)") else: - headers.pop("Authorization", None) - headers.pop("authorization", None) + for auth_header in ["Authorization", "authorization"]: + if headers.pop(auth_header, None) is not None: + removed_headers.append(auth_header) + + logger.debug( + "Headers prepared for upstream", + extra={ + "final_headers_count": len(headers), + "removed_headers": removed_headers, + "added_upstream_auth": bool(UPSTREAM_API_KEY), + }, + ) return headers diff --git a/router/payment/x_cashu.py b/router/payment/x_cashu.py index f3cb5b47..30ce674c 100644 --- a/router/payment/x_cashu.py +++ b/router/payment/x_cashu.py @@ -1,43 +1,83 @@ import json import traceback -from typing import AsyncGenerator, Literal, cast +from typing import AsyncGenerator import httpx from fastapi import BackgroundTasks, HTTPException, Request from fastapi.responses import Response, StreamingResponse -from router.cashu import wallet -from router.payment.cost_caculation import ( - CostData, - CostDataError, - MaxCostData, - calculate_cost, -) -from router.payment.helpers import ( +from ..logging import get_logger +from ..wallet import CurrencyUnit, recieve_token, send_token +from .cost_caculation import CostData, CostDataError, MaxCostData, calculate_cost +from .helpers import ( UPSTREAM_BASE_URL, create_error_response, get_max_cost_for_model, prepare_upstream_headers, ) +logger = get_logger(__name__) + async def x_cashu_handler( request: Request, x_cashu_token: str, path: str ) -> Response | StreamingResponse: - headers = dict(request.headers) - amount, _ = await redeem_token(x_cashu_token) - headers = prepare_upstream_headers(dict(request.headers)) - return await forward_to_upstream(request, path, headers, amount) + """Handle X-Cashu token payment requests.""" + logger.info( + "Processing X-Cashu payment request", + extra={ + "path": path, + "method": request.method, + "token_preview": x_cashu_token[:20] + "..." + if len(x_cashu_token) > 20 + else x_cashu_token, + }, + ) + + try: + headers = dict(request.headers) + amount, unit, mint = await recieve_token(x_cashu_token) + headers = prepare_upstream_headers(dict(request.headers)) + + logger.info( + "X-Cashu token redeemed successfully", + extra={"amount": amount, "unit": unit, "path": path, "mint": mint}, + ) + + return await forward_to_upstream(request, path, headers, amount, unit) + except Exception as e: + logger.error( + "X-Cashu payment request failed", + extra={ + "error": str(e), + "error_type": type(e).__name__, + "path": path, + "method": request.method, + }, + ) + raise async def forward_to_upstream( - request: Request, path: str, headers: dict, amount: int + request: Request, path: str, headers: dict, amount: int, unit: CurrencyUnit ) -> Response | StreamingResponse: """Forward request to upstream and handle the response.""" if path.startswith("v1/"): path = path.replace("v1/", "") url = f"{UPSTREAM_BASE_URL}/{path}" + + logger.debug( + "Forwarding request to upstream", + extra={ + "url": url, + "method": request.method, + "path": path, + "amount": amount, + "unit": unit, + }, + ) + async with httpx.AsyncClient( transport=httpx.AsyncHTTPTransport(retries=1), timeout=None, @@ -54,8 +94,64 @@ async def forward_to_upstream( stream=True, ) + logger.debug( + "Received upstream response", + extra={ + "status_code": response.status_code, + "path": path, + "response_headers": dict(response.headers), + }, + ) + + if response.status_code != 200: + logger.warning( + "Upstream request failed, processing refund", + extra={ + "status_code": response.status_code, + "path": path, + "amount": amount, + "unit": unit, + }, + ) + + refund_token = await send_refund(amount - 60, unit) + + logger.info( + "Refund processed for failed upstream request", + extra={ + "status_code": response.status_code, + "refund_amount": amount, + "unit": unit, + "refund_token_preview": refund_token[:20] + "..." + if len(refund_token) > 20 + else refund_token, + }, + ) + + error_response = Response( + content=json.dumps( + { + "error": { + "message": "Error forwarding request to upstream", + "type": "upstream_error", + "code": response.status_code, + "refund_token": refund_token, + } + } + ), + status_code=response.status_code, + media_type="application/json", + ) + error_response.headers["X-Cashu"] = refund_token + return error_response + if path.endswith("chat/completions"): - result = await handle_x_cashu_chat_completion(response, amount) + logger.debug( + "Processing chat completion response", + extra={"path": path, "amount": amount, "unit": unit}, + ) + + result = await handle_x_cashu_chat_completion(response, amount, unit) background_tasks = BackgroundTasks() background_tasks.add_task(response.aclose) result.background = background_tasks @@ -65,6 +161,11 @@ async def forward_to_upstream( background_tasks.add_task(response.aclose) background_tasks.add_task(client.aclose) + logger.debug( + "Streaming non-chat response", + extra={"path": path, "status_code": response.status_code}, + ) + return StreamingResponse( response.aiter_bytes(), status_code=response.status_code, @@ -73,11 +174,17 @@ async def forward_to_upstream( ) except Exception as exc: tb = traceback.format_exc() - print( - f"Unexpected error: {exc}\n" - f"Request details: method={request.method}, url={url}, headers={headers}, " - f"path={path}, query_params={dict(request.query_params)}\n" - f"Traceback:\n{tb}" + logger.error( + "Unexpected error in upstream forwarding", + extra={ + "error": str(exc), + "error_type": type(exc).__name__, + "method": request.method, + "url": url, + "path": path, + "query_params": dict(request.query_params), + "traceback": tb, + }, ) return create_error_response( "internal_error", "An unexpected server error occurred", 500 @@ -85,23 +192,46 @@ async def forward_to_upstream( async def handle_x_cashu_chat_completion( - response: httpx.Response, amount: int + response: httpx.Response, amount: int, unit: CurrencyUnit ) -> StreamingResponse | Response: """Handle both streaming and non-streaming chat completion responses with token-based pricing.""" + logger.debug( + "Handling chat completion response", + extra={"amount": amount, "unit": unit, "status_code": response.status_code}, + ) + try: content = await response.aread() content_str = content.decode("utf-8") if isinstance(content, bytes) else content is_streaming = content_str.startswith("data:") or "data:" in content_str + logger.debug( + "Chat completion response analysis", + extra={ + "is_streaming": is_streaming, + "content_length": len(content_str), + "amount": amount, + "unit": unit, + }, + ) + if is_streaming: - print("Detected streaming response, processing SSE format") - return await handle_streaming_response(content_str, response, amount) + return await handle_streaming_response(content_str, response, amount, unit) else: - print("Detected non-streaming response, processing as JSON") - return await handle_non_streaming_response(content_str, response, amount) + return await handle_non_streaming_response( + content_str, response, amount, unit + ) except Exception as e: - print(f"Error processing chat completion response: {e}") + logger.error( + "Error processing chat completion response", + extra={ + "error": str(e), + "error_type": type(e).__name__, + "amount": amount, + "unit": unit, + }, + ) # Return the original response if we can't process it return StreamingResponse( response.aiter_bytes(), @@ -111,9 +241,25 @@ async def handle_x_cashu_chat_completion( async def handle_streaming_response( - content_str: str, response: httpx.Response, amount: int + content_str: str, response: httpx.Response, amount: int, unit: CurrencyUnit ) -> StreamingResponse: """Handle Server-Sent Events (SSE) streaming response.""" + logger.debug( + "Processing streaming response", + extra={ + "amount": amount, + "unit": unit, + "content_lines": len(content_str.strip().split("\n")), + }, + ) + + # Initialize response headers early so they can be modified during processing + response_headers = dict(response.headers) + if "transfer-encoding" in response_headers: + del response_headers["transfer-encoding"] + if "content-encoding" in response_headers: + del response_headers["content-encoding"] + # For streaming responses, we'll extract the final usage data # and calculate cost based on that usage_data = None @@ -134,27 +280,69 @@ async def handle_streaming_response( except json.JSONDecodeError: continue - print(f"usage: {usage_data}") + response_headers = dict(response.headers) # If we found usage data, calculate cost and refund if usage_data and model: + logger.debug( + "Found usage data in streaming response", + extra={ + "model": model, + "usage_data": usage_data, + "amount": amount, + "unit": unit, + }, + ) + response_data = {"usage": usage_data, "model": model} try: cost_data = await get_cost(response_data) - print(f"Refunded {cost_data} msats") if cost_data: refund_amount = amount - cost_data.total_msats if refund_amount > 0: - refund_token = await send_refund(refund_amount) - response.headers["X-Cashu"] = refund_token - print(f"Refunded {refund_amount} msats") - except Exception as e: - print(f"Error calculating cost for streaming response: {e}") + logger.info( + "Processing refund for streaming response", + extra={ + "original_amount": amount, + "cost_msats": cost_data.total_msats, + "refund_amount": refund_amount, + "unit": unit, + "model": model, + }, + ) - response_headers = dict(response.headers) - if "transfer-encoding" in response_headers: - del response_headers["transfer-encoding"] - if "content-encoding" in response_headers: - del response_headers["content-encoding"] + refund_token = await send_refund(refund_amount, unit) + response_headers["X-Cashu"] = refund_token + + logger.info( + "Refund processed for streaming response", + extra={ + "refund_amount": refund_amount, + "unit": unit, + "refund_token_preview": refund_token[:20] + "..." + if len(refund_token) > 20 + else refund_token, + }, + ) + else: + logger.debug( + "No refund needed for streaming response", + extra={ + "amount": amount, + "cost_msats": cost_data.total_msats, + "model": model, + }, + ) + except Exception as e: + logger.error( + "Error calculating cost for streaming response", + extra={ + "error": str(e), + "error_type": type(e).__name__, + "model": model, + "amount": amount, + "unit": unit, + }, + ) async def generate() -> AsyncGenerator[bytes, None]: for line in lines: @@ -169,15 +357,28 @@ async def handle_streaming_response( async def handle_non_streaming_response( - content_str: str, response: httpx.Response, amount: int + content_str: str, response: httpx.Response, amount: int, unit: CurrencyUnit ) -> Response: """Handle regular JSON response.""" + logger.debug( + "Processing non-streaming response", + extra={"amount": amount, "unit": unit, "content_length": len(content_str)}, + ) + try: response_json = json.loads(content_str) cost_data = await get_cost(response_json) if not cost_data: + logger.error( + "Failed to calculate cost for response", + extra={ + "amount": amount, + "unit": unit, + "response_model": response_json.get("model", "unknown"), + }, + ) return Response( content=json.dumps( { @@ -199,11 +400,32 @@ async def handle_non_streaming_response( del response_headers["content-encoding"] refund_amount = amount - cost_data.total_msats - print("refund: ", refund_amount) + + logger.info( + "Processing non-streaming response cost calculation", + extra={ + "original_amount": amount, + "cost_msats": cost_data.total_msats, + "refund_amount": refund_amount, + "unit": unit, + "model": response_json.get("model", "unknown"), + }, + ) + if refund_amount > 0: - refund_token = await send_refund(refund_amount) - response.headers["X-Cashu"] = refund_token - print(f"Refunded {refund_amount} msats") + refund_token = await send_refund(refund_amount, unit) + response_headers["X-Cashu"] = refund_token + + logger.info( + "Refund processed for non-streaming response", + extra={ + "refund_amount": refund_amount, + "unit": unit, + "refund_token_preview": refund_token[:20] + "..." + if len(refund_token) > 20 + else refund_token, + }, + ) return Response( content=content_str, @@ -212,8 +434,32 @@ async def handle_non_streaming_response( media_type="application/json", ) except json.JSONDecodeError as e: - response.headers["X-Cashu"] = await wallet().send(amount - 60) - print(f"Failed to parse JSON from upstream response: {e}") + logger.error( + "Failed to parse JSON from upstream response", + extra={ + "error": str(e), + "content_preview": content_str[:200] + "..." + if len(content_str) > 200 + else content_str, + "amount": amount, + "unit": unit, + }, + ) + + # Emergency refund with small deduction for processing + emergency_refund = amount + refund_token = await send_token(emergency_refund, unit=unit) + response.headers["X-Cashu"] = refund_token + + logger.warning( + "Emergency refund issued due to JSON parse error", + extra={ + "original_amount": amount, + "refund_amount": emergency_refund, + "deduction": 60, + }, + ) + # Return original content if JSON parsing fails return Response( content=content_str, @@ -229,14 +475,41 @@ async def get_cost(response_data: dict) -> MaxCostData | CostData | None: This is called after the initial payment and the upstream request is complete. Returns cost data to be included in the response. """ - max_cost = get_max_cost_for_model(model=response_data["model"]) + model = response_data.get("model", "unknown") + logger.debug( + "Calculating cost for response", + extra={"model": model, "has_usage": "usage" in response_data}, + ) + + max_cost = get_max_cost_for_model(model=model) match calculate_cost(response_data, max_cost): case MaxCostData() as cost: + logger.debug( + "Using max cost pricing", + extra={"model": model, "max_cost_msats": cost.total_msats}, + ) return cost case CostData() as cost: + logger.debug( + "Using token-based pricing", + extra={ + "model": model, + "total_cost_msats": cost.total_msats, + "input_msats": cost.input_msats, + "output_msats": cost.output_msats, + }, + ) return cost case CostDataError() as error: + logger.error( + "Cost calculation error", + extra={ + "model": model, + "error_message": error.message, + "error_code": error.code, + }, + ) raise HTTPException( status_code=400, detail={ @@ -249,34 +522,70 @@ async def get_cost(response_data: dict) -> MaxCostData | CostData | None: ) -async def redeem_token(x_cashu_token: str) -> tuple[int, Literal["sat", "msat"]]: - try: - result = await wallet().redeem(x_cashu_token) - return cast(tuple[int, Literal["sat", "msat"]], result) - except Exception as e: - raise HTTPException( - status_code=401, - detail={ - "error": { - "message": f"Invalid or expired Cashu key: {str(e)}", - "type": "invalid_request_error", - "code": "invalid_api_key", - } - }, - ) +async def send_refund(amount: int, unit: CurrencyUnit, mint: str | None = None) -> str: + """Send a refund using Cashu tokens.""" + logger.debug( + "Creating refund token", extra={"amount": amount, "unit": unit, "mint": mint} + ) + max_retries = 3 + last_exception = None -async def send_refund(amount: int) -> str: - try: - return await wallet().send(amount) - except Exception as e: - raise HTTPException( - status_code=401, - detail={ - "error": { - "message": f"failed to create refund: {str(e)}", - "type": "invalid_request_error", - "code": "send_token_failed", - } - }, - ) + for attempt in range(max_retries): + try: + refund_token = await send_token(amount, unit=unit, mint_url=mint) + + logger.info( + "Refund token created successfully", + extra={ + "amount": amount, + "unit": unit, + "mint": mint, + "attempt": attempt + 1, + "token_preview": refund_token[:20] + "..." + if len(refund_token) > 20 + else refund_token, + }, + ) + + return refund_token + except Exception as e: + last_exception = e + if attempt < max_retries - 1: + logger.warning( + "Refund token creation failed, retrying", + extra={ + "error": str(e), + "error_type": type(e).__name__, + "attempt": attempt + 1, + "max_retries": max_retries, + "amount": amount, + "unit": unit, + "mint": mint, + }, + ) + else: + logger.error( + "Failed to create refund token after all retries", + extra={ + "error": str(e), + "error_type": type(e).__name__, + "attempt": attempt + 1, + "max_retries": max_retries, + "amount": amount, + "unit": unit, + "mint": mint, + }, + ) + + # If we get here, all retries failed + raise HTTPException( + status_code=401, + detail={ + "error": { + "message": f"failed to create refund after {max_retries} attempts: {str(last_exception)}", + "type": "invalid_request_error", + "code": "send_token_failed", + } + }, + ) diff --git a/router/price.py b/router/price.py index ab584122..a14557a6 100644 --- a/router/price.py +++ b/router/price.py @@ -1,57 +1,128 @@ import asyncio -import logging import os import httpx +from .logging import get_logger + +logger = get_logger(__name__) + # artifical spread to cover conversion fees EXCHANGE_FEE = float(os.environ.get("EXCHANGE_FEE", "1.005")) # 0.5% default +logger.info("Price module initialized", extra={"exchange_fee": EXCHANGE_FEE}) + async def kraken_btc_usd(client: httpx.AsyncClient) -> float | None: + """Fetch BTC/USD price from Kraken API.""" api = "https://api.kraken.com/0/public/Ticker?pair=XBTUSD" try: - return float((await client.get(api)).json()["result"]["XXBTZUSD"]["c"][0]) + logger.debug("Fetching BTC price from Kraken") + response = await client.get(api) + price_data = response.json() + price = float(price_data["result"]["XXBTZUSD"]["c"][0]) + + return price except (httpx.RequestError, KeyError) as e: - logging.warning(f"Kraken API error: {e}") + logger.warning( + "Kraken API error", + extra={ + "error": str(e), + "error_type": type(e).__name__, + "exchange": "kraken", + }, + ) return None async def coinbase_btc_usd(client: httpx.AsyncClient) -> float | None: + """Fetch BTC/USD price from Coinbase API.""" api = "https://api.coinbase.com/v2/prices/BTC-USD/spot" try: - return float((await client.get(api)).json()["data"]["amount"]) + logger.debug("Fetching BTC price from Coinbase") + response = await client.get(api) + price_data = response.json() + price = float(price_data["data"]["amount"]) + + return price except (httpx.RequestError, KeyError) as e: - logging.warning(f"Coinbase API error: {e}") + logger.warning( + "Coinbase API error", + extra={ + "error": str(e), + "error_type": type(e).__name__, + "exchange": "coinbase", + }, + ) return None async def binance_btc_usdt(client: httpx.AsyncClient) -> float | None: + """Fetch BTC/USDT price from Binance API.""" api = "https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT" try: - return float((await client.get(api)).json()["price"]) + logger.debug("Fetching BTC price from Binance") + response = await client.get(api) + price_data = response.json() + price = float(price_data["price"]) + + return price except (httpx.RequestError, KeyError) as e: - logging.warning(f"Binance API error: {e}") + logger.warning( + "Binance API error", + extra={ + "error": str(e), + "error_type": type(e).__name__, + "exchange": "binance", + }, + ) return None async def btc_usd_ask_price() -> float: - async with httpx.AsyncClient() as client: - return ( - max( - [ - price - for price in await asyncio.gather( - kraken_btc_usd(client), - coinbase_btc_usd(client), - binance_btc_usdt(client), - ) - if price is not None - ] + """Get the highest BTC/USD price from multiple exchanges with fee adjustment.""" + logger.debug("Starting BTC price aggregation from multiple exchanges") + + async with httpx.AsyncClient(timeout=30.0) as client: + try: + prices = await asyncio.gather( + kraken_btc_usd(client), + coinbase_btc_usd(client), + binance_btc_usdt(client), ) - * EXCHANGE_FEE - ) + + valid_prices = [price for price in prices if price is not None] + + if not valid_prices: + logger.error("No valid BTC prices obtained from any exchange") + raise ValueError("Unable to fetch BTC price from any exchange") + + max_price = max(valid_prices) + final_price = max_price * EXCHANGE_FEE + + return final_price + + except Exception as e: + logger.error( + "Error in BTC price aggregation", + extra={"error": str(e), "error_type": type(e).__name__}, + ) + raise async def sats_usd_ask_price() -> float: - return (await btc_usd_ask_price()) / 100_000_000 + """Get the USD price per satoshi.""" + logger.debug("Calculating satoshi price from BTC price") + + try: + btc_price = await btc_usd_ask_price() + sats_price = btc_price / 100_000_000 + + return sats_price + + except Exception as e: + logger.error( + "Error calculating satoshi price", + extra={"error": str(e), "error_type": type(e).__name__}, + ) + raise diff --git a/router/proxy.py b/router/proxy.py index 29239a69..2b643cb9 100644 --- a/router/proxy.py +++ b/router/proxy.py @@ -7,18 +7,23 @@ import httpx from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request from fastapi.responses import Response, StreamingResponse -from router.payment.helpers import ( +from .auth import ( + adjust_payment_for_tokens, + pay_for_request, + revert_pay_for_request, + validate_bearer_key, +) +from .db import ApiKey, AsyncSession, create_session, get_session +from .logging import get_logger +from .payment.helpers import ( UPSTREAM_BASE_URL, check_token_balance, create_error_response, prepare_upstream_headers, ) -from router.payment.x_cashu import x_cashu_handler - -from .auth import adjust_payment_for_tokens, pay_for_request, validate_bearer_key -from .cashu import x_cashu_refund -from .db import ApiKey, AsyncSession, create_session, get_session +from .payment.x_cashu import x_cashu_handler +logger = get_logger(__name__) proxy_router = APIRouter() @@ -26,6 +31,14 @@ async def handle_streaming_chat_completion( response: httpx.Response, key: ApiKey, session: AsyncSession ) -> StreamingResponse: """Handle streaming chat completion responses with token-based pricing.""" + logger.info( + "Processing streaming chat completion", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "key_balance": key.balance, + "response_status": response.status_code, + }, + ) async def stream_with_cost() -> AsyncGenerator[bytes, None]: # Store all chunks to analyze @@ -38,6 +51,14 @@ async def handle_streaming_chat_completion( # Pass through each chunk to client yield chunk + logger.debug( + "Streaming completed, analyzing usage data", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "chunks_count": len(stored_chunks), + }, + ) + # Process stored chunks to find usage data # Start from the end and work backwards for i in range(len(stored_chunks) - 1, -1, -1): @@ -63,6 +84,15 @@ async def handle_streaming_chat_completion( and data["usage"] is not None and isinstance(data["usage"], dict) ): + logger.info( + "Found usage data in streaming response", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "usage_data": data["usage"], + "model": data.get("model", "unknown"), + }, + ) + # Found usage data, calculate cost # Create a new session for this operation async with create_session() as new_session: @@ -71,18 +101,43 @@ async def handle_streaming_chat_completion( key.__class__, key.hashed_key ) if fresh_key: - cost_data = await adjust_payment_for_tokens( - fresh_key, data, new_session - ) - # Format as SSE and yield - cost_json = json.dumps({"cost": cost_data}) - yield f"data: {cost_json}\n\n".encode() + try: + cost_data = await adjust_payment_for_tokens( + fresh_key, data, new_session + ) + logger.info( + "Token adjustment completed for streaming", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "cost_data": cost_data, + "balance_after_adjustment": fresh_key.balance, + }, + ) + # Format as SSE and yield + cost_json = json.dumps({"cost": cost_data}) + yield f"data: {cost_json}\n\n".encode() + except Exception as cost_error: + logger.error( + "Error adjusting payment for streaming tokens", + extra={ + "error": str(cost_error), + "error_type": type(cost_error).__name__, + "key_hash": key.hashed_key[:8] + "...", + }, + ) break except json.JSONDecodeError: continue except Exception as e: - print(f"Error processing streaming response for cost: {e}") + logger.error( + "Error processing streaming response chunk", + extra={ + "error": str(e), + "error_type": type(e).__name__, + "key_hash": key.hashed_key[:8] + "...", + }, + ) return StreamingResponse( stream_with_cost(), @@ -95,21 +150,58 @@ async def handle_non_streaming_chat_completion( response: httpx.Response, key: ApiKey, session: AsyncSession ) -> Response: """Handle non-streaming chat completion responses with token-based pricing.""" + logger.info( + "Processing non-streaming chat completion", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "key_balance": key.balance, + "response_status": response.status_code, + }, + ) + try: content = await response.aread() response_json = json.loads(content) + + logger.debug( + "Parsed response JSON", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "model": response_json.get("model", "unknown"), + "has_usage": "usage" in response_json, + }, + ) + cost_data = await adjust_payment_for_tokens(key, response_json, session) response_json["cost"] = cost_data - response_headers = dict(response.headers) + logger.info( + "Token adjustment completed for non-streaming", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "cost_data": cost_data, + "model": response_json.get("model", "unknown"), + "balance_after_adjustment": key.balance, + }, + ) - # Remove Transfer-Encoding header to avoid conflict with Content-Length header in common nginx setups - if "transfer-encoding" in response_headers: - del response_headers["transfer-encoding"] + # Keep only standard headers that are safe to pass through + allowed_headers = { + "content-type", + "cache-control", + "date", + "vary", + "access-control-allow-origin", + "access-control-allow-methods", + "access-control-allow-headers", + "access-control-allow-credentials", + "access-control-expose-headers", + "access-control-max-age", + } - # Remove Content-Encoding header since we're sending uncompressed JSON - if "content-encoding" in response_headers: - del response_headers["content-encoding"] + response_headers = { + k: v for k, v in response.headers.items() if k.lower() in allowed_headers + } return Response( content=json.dumps(response_json).encode(), @@ -118,10 +210,26 @@ async def handle_non_streaming_chat_completion( media_type="application/json", ) except json.JSONDecodeError as e: - print(f"Failed to parse JSON from upstream response: {e}") + logger.error( + "Failed to parse JSON from upstream response", + extra={ + "error": str(e), + "key_hash": key.hashed_key[:8] + "...", + "content_preview": content[:200].decode(errors="ignore") + if content + else "empty", + }, + ) raise except Exception as e: - print(f"Error adjusting payment for tokens: {e}") + logger.error( + "Error processing non-streaming chat completion", + extra={ + "error": str(e), + "error_type": type(e).__name__, + "key_hash": key.hashed_key[:8] + "...", + }, + ) raise @@ -138,6 +246,19 @@ async def forward_to_upstream( path = path.replace("v1/", "") url = f"{UPSTREAM_BASE_URL}/{path}" + + logger.info( + "Forwarding request to upstream", + extra={ + "url": url, + "method": request.method, + "path": path, + "key_hash": key.hashed_key[:8] + "...", + "key_balance": key.balance, + "has_request_body": request_body is not None, + }, + ) + client = httpx.AsyncClient( transport=httpx.AsyncHTTPTransport(retries=1), timeout=None, # No timeout - requests can take as long as needed @@ -168,11 +289,52 @@ async def forward_to_upstream( stream=True, ) + logger.info( + "Received upstream response", + extra={ + "status_code": response.status_code, + "path": path, + "key_hash": key.hashed_key[:8] + "...", + "content_type": response.headers.get("content-type", "unknown"), + }, + ) + # For chat completions, we need to handle token-based pricing if path.endswith("chat/completions"): + # Check if client requested streaming + client_wants_streaming = False + if request_body: + try: + request_data = json.loads(request_body) + client_wants_streaming = request_data.get("stream", False) + logger.debug( + "Chat completion request analysis", + extra={ + "client_wants_streaming": client_wants_streaming, + "model": request_data.get("model", "unknown"), + "key_hash": key.hashed_key[:8] + "...", + }, + ) + except json.JSONDecodeError: + logger.warning( + "Failed to parse request body JSON for streaming detection" + ) + # Handle both streaming and non-streaming responses content_type = response.headers.get("content-type", "") - is_streaming = "text/event-stream" in content_type + upstream_is_streaming = "text/event-stream" in content_type + is_streaming = client_wants_streaming and upstream_is_streaming + + logger.debug( + "Response type analysis", + extra={ + "is_streaming": is_streaming, + "client_wants_streaming": client_wants_streaming, + "upstream_is_streaming": upstream_is_streaming, + "content_type": content_type, + "key_hash": key.hashed_key[:8] + "...", + }, + ) if is_streaming and response.status_code == 200: # Process streaming response and extract cost from the last chunk @@ -183,7 +345,7 @@ async def forward_to_upstream( result.background = background_tasks return result - elif response.status_code == 200 and "application/json" in content_type: + elif response.status_code == 200: # Handle non-streaming response try: return await handle_non_streaming_chat_completion( @@ -198,6 +360,15 @@ async def forward_to_upstream( background_tasks.add_task(response.aclose) background_tasks.add_task(client.aclose) + logger.debug( + "Streaming non-chat response", + extra={ + "path": path, + "status_code": response.status_code, + "key_hash": key.hashed_key[:8] + "...", + }, + ) + return StreamingResponse( response.aiter_bytes(), status_code=response.status_code, @@ -209,10 +380,18 @@ async def forward_to_upstream( await client.aclose() error_type = type(exc).__name__ error_details = str(exc) - print( - f"Error forwarding request to upstream: {error_type}: {error_details}\n" - f"Request details: method={request.method}, url={url}, headers={headers}, " - f"path={path}, query_params={dict(request.query_params)}" + + logger.error( + "HTTP request error to upstream", + extra={ + "error_type": error_type, + "error_details": error_details, + "method": request.method, + "url": url, + "path": path, + "query_params": dict(request.query_params), + "key_hash": key.hashed_key[:8] + "...", + }, ) # Provide more specific error messages based on the error type @@ -229,15 +408,22 @@ async def forward_to_upstream( except Exception as exc: await client.aclose() - import traceback - tb = traceback.format_exc() - print( - f"Unexpected error: {exc}\n" - f"Request details: method={request.method}, url={url}, headers={headers}, " - f"path={path}, query_params={dict(request.query_params)}\n" - f"Traceback:\n{tb}" + + logger.error( + "Unexpected error in upstream forwarding", + extra={ + "error": str(exc), + "error_type": type(exc).__name__, + "method": request.method, + "url": url, + "path": path, + "query_params": dict(request.query_params), + "key_hash": key.hashed_key[:8] + "...", + "traceback": tb, + }, ) + return create_error_response( "internal_error", "An unexpected server error occurred", 500 ) @@ -247,6 +433,17 @@ async def forward_to_upstream( async def proxy( request: Request, path: str, session: AsyncSession = Depends(get_session) ) -> Response | StreamingResponse: + """Main proxy endpoint handler.""" + logger.info( + "Received proxy request", + extra={ + "method": request.method, + "path": path, + "client_host": request.client.host if request.client else "unknown", + "user_agent": request.headers.get("user-agent", "unknown")[:100], + }, + ) + request_body = await request.body() headers = dict(request.headers) @@ -255,7 +452,25 @@ async def proxy( if request_body: try: request_body_dict = json.loads(request_body) - except json.JSONDecodeError: + logger.debug( + "Request body parsed", + extra={ + "path": path, + "body_keys": list(request_body_dict.keys()), + "model": request_body_dict.get("model", "not_specified"), + }, + ) + except json.JSONDecodeError as e: + logger.error( + "Invalid JSON in request body", + extra={ + "error": str(e), + "path": path, + "body_preview": request_body[:200].decode(errors="ignore") + if request_body + else "empty", + }, + ) return Response( content=json.dumps( {"error": {"type": "invalid_request_error", "code": "invalid_json"}} @@ -264,31 +479,92 @@ async def proxy( media_type="application/json", ) + # Check token balance for all requests to get currency unit + try: + unit = check_token_balance(headers, request_body_dict) + logger.debug( + "Token balance check completed", extra={"path": path, "unit": unit} + ) + except HTTPException as e: + logger.warning( + "Token balance check failed", + extra={"path": path, "status_code": e.status_code, "detail": str(e.detail)}, + ) + raise + # Handle authentication if x_cashu := headers.get("x-cashu", None): - # Check token balance before authentication for cashu tokens - if request_body_dict: - check_token_balance(headers, request_body_dict, "msat") + logger.info( + "Processing X-Cashu payment", + extra={ + "path": path, + "token_preview": x_cashu[:20] + "..." if len(x_cashu) > 20 else x_cashu, + }, + ) return await x_cashu_handler(request, x_cashu, path) elif auth := headers.get("authorization", None): + logger.debug( + "Processing bearer token authentication", + extra={ + "path": path, + "token_preview": auth[:20] + "..." if len(auth) > 20 else auth, + }, + ) key = await get_bearer_token_key(headers, path, session, auth) else: if request.method not in ["GET"]: + logger.warning( + "Unauthorized request - no authentication provided", + extra={"method": request.method, "path": path}, + ) return Response( content=json.dumps({"detail": "Unauthorized"}), status_code=401, media_type="application/json", ) + logger.debug("Processing unauthenticated GET request", extra={"path": path}) # Prepare headers for upstream headers = prepare_upstream_headers(dict(request.headers)) return await forward_get_to_upstream(request, path, headers) + cost_per_request = 0 # Only pay for request if we have request body data (for completions endpoints) if request_body_dict: - await pay_for_request(key, session, request_body_dict) + logger.info( + "Processing payment for request", + extra={ + "path": path, + "key_hash": key.hashed_key[:8] + "...", + "key_balance_before": key.balance, + "model": request_body_dict.get("model", "unknown"), + }, + ) + + try: + await pay_for_request(key, session, request_body_dict) + logger.info( + "Payment processed successfully", + extra={ + "path": path, + "key_hash": key.hashed_key[:8] + "...", + "key_balance_after": key.balance, + "model": request_body_dict.get("model", "unknown"), + }, + ) + except Exception as e: + logger.error( + "Payment processing failed", + extra={ + "error": str(e), + "error_type": type(e).__name__, + "path": path, + "key_hash": key.hashed_key[:8] + "...", + }, + ) + raise # Prepare headers for upstream headers = prepare_upstream_headers(dict(request.headers)) @@ -298,28 +574,17 @@ async def proxy( request, path, headers, request_body, key, session ) - if response.status_code != 200 and key.refund_address == "X-CASHU": - refund_token = await x_cashu_refund(key, session) - response = Response( - content=json.dumps( - { - "error": { - "message": "Error forwarding request to upstream", - "type": "upstream_error", - "code": response.status_code, - "refund_token": refund_token, - } - } - ), - status_code=response.status_code, - media_type="application/json", + if response.status_code != 200: + await revert_pay_for_request(key, session, cost_per_request) + logger.warning( + "Upstream request failed, revert payment", + extra={ + "status_code": response.status_code, + "path": path, + "key_hash": key.hashed_key[:8] + "...", + "key_balance": key.balance, + }, ) - response.headers["X-Cashu"] = refund_token - return response - - if key.refund_address == "X-CASHU": - refund_token = await x_cashu_refund(key, session) - response.headers["X-Cashu"] = refund_token return response @@ -332,16 +597,40 @@ async def get_bearer_token_key( refund_address = headers.get("Refund-LNURL", None) key_expiry_time = headers.get("Key-Expiry-Time", None) + logger.debug( + "Processing bearer token", + extra={ + "path": path, + "has_refund_address": bool(refund_address), + "has_expiry_time": bool(key_expiry_time), + "bearer_key_preview": bearer_key[:20] + "..." + if len(bearer_key) > 20 + else bearer_key, + }, + ) + # Validate key_expiry_time header if key_expiry_time: try: key_expiry_time = int(key_expiry_time) # type: ignore + logger.debug( + "Key expiry time validated", + extra={"expiry_time": key_expiry_time, "path": path}, + ) except ValueError: + logger.error( + "Invalid Key-Expiry-Time header", + extra={"key_expiry_time": key_expiry_time, "path": path}, + ) raise HTTPException( status_code=400, detail="Invalid Key-Expiry-Time: must be a valid Unix timestamp", ) if not refund_address: + logger.error( + "Missing Refund-LNURL header with Key-Expiry-Time", + extra={"path": path, "expiry_time": key_expiry_time}, + ) raise HTTPException( status_code=400, detail="Error: Refund-LNURL header required when using Key-Expiry-Time", @@ -349,12 +638,35 @@ async def get_bearer_token_key( else: key_expiry_time = None - return await validate_bearer_key( - bearer_key, - session, - refund_address, - key_expiry_time, # type: ignore - ) + try: + key = await validate_bearer_key( + bearer_key, + session, + refund_address, + key_expiry_time, # type: ignore + ) + logger.info( + "Bearer token validated successfully", + extra={ + "path": path, + "key_hash": key.hashed_key[:8] + "...", + "key_balance": key.balance, + }, + ) + return key + except Exception as e: + logger.error( + "Bearer token validation failed", + extra={ + "error": str(e), + "error_type": type(e).__name__, + "path": path, + "bearer_key_preview": bearer_key[:20] + "..." + if len(bearer_key) > 20 + else bearer_key, + }, + ) + raise async def forward_get_to_upstream( @@ -368,6 +680,11 @@ async def forward_get_to_upstream( url = f"{UPSTREAM_BASE_URL}/{path}" + logger.info( + "Forwarding GET request to upstream", + extra={"url": url, "method": request.method, "path": path}, + ) + async with httpx.AsyncClient( transport=httpx.AsyncHTTPTransport(retries=1), timeout=None, @@ -383,6 +700,11 @@ async def forward_get_to_upstream( ), ) + logger.info( + "GET request forwarded successfully", + extra={"path": path, "status_code": response.status_code}, + ) + return StreamingResponse( response.aiter_bytes(), status_code=response.status_code, @@ -390,11 +712,17 @@ async def forward_get_to_upstream( ) except Exception as exc: tb = traceback.format_exc() - print( - f"Unexpected error: {exc}\n" - f"Request details: method={request.method}, url={url}, headers={headers}, " - f"path={path}, query_params={dict(request.query_params)}\n" - f"Traceback:\n{tb}" + logger.error( + "Error forwarding GET request", + extra={ + "error": str(exc), + "error_type": type(exc).__name__, + "method": request.method, + "url": url, + "path": path, + "query_params": dict(request.query_params), + "traceback": tb, + }, ) return create_error_response( "internal_error", "An unexpected server error occurred", 500 diff --git a/router/wallet.py b/router/wallet.py new file mode 100644 index 00000000..1856efbb --- /dev/null +++ b/router/wallet.py @@ -0,0 +1,190 @@ +import os +from typing import Literal + +from cashu.core.base import Token +from cashu.wallet.helpers import deserialize_token_from_string, receive, send +from cashu.wallet.wallet import Wallet + +from .db import ApiKey, AsyncSession +from .logging import get_logger + +# from .cashu import ( +# credit_balance, +# delete_key_if_zero_balance, +# refund_balance, +# wallet, +# ) +# from .cashu import ( +# check_for_refunds, +# init_wallet, +# periodic_payout, +# ) + +logger = get_logger(__name__) + +CurrencyUnit = Literal["sat", "msat"] + +TRUSTED_MINTS = os.environ["CASHU_MINTS"].split(",") +PRIMARY_MINT_URL = TRUSTED_MINTS[0] + + +async def get_balance(unit: CurrencyUnit) -> int: + wallet = await Wallet.with_db( + PRIMARY_MINT_URL, + # DATABASE_URL, + db=".wallet", + load_all_keysets=True, + unit=unit, + ) + await wallet.load_proofs() + return wallet.available_balance.amount + + +async def recieve_token( + token: str, +) -> tuple[int, CurrencyUnit, str]: # amount, unit, mint_url + # trusted_mints = os.environ["CASHU_MINTS"].split(",") + token_obj = deserialize_token_from_string(token) + wallet = await Wallet.with_db( + token_obj.mint, + db=".wallet", + load_all_keysets=True, + unit=token_obj.unit, + ) + if token_obj.mint in TRUSTED_MINTS and token_obj.mint != PRIMARY_MINT_URL: + return await swap_to_primary_mint(token_obj, wallet) + elif token_obj.mint not in TRUSTED_MINTS: + raise ValueError("Mint URL is not supported by this proxy") + await receive(wallet, token_obj) + return token_obj.amount, token_obj.unit, token_obj.mint + + +async def send_token( + amount: int, unit: CurrencyUnit, mint_url: str | None = None +) -> str: + wallet = await Wallet.with_db( + mint_url or PRIMARY_MINT_URL, + db=".wallet", + load_all_keysets=True, + unit=unit, + ) + balance, token = await send(wallet, amount=amount, lock="", legacy=False) + return token + + +async def swap_to_primary_mint( + token_obj: Token, wallet: Wallet +) -> tuple[int, CurrencyUnit, str]: + print(f"swap_to_primary_mint, token_obj: {token_obj}") + if token_obj.unit == "sat": + amount_msat = token_obj.amount * 1000 + elif token_obj.unit == "msat": + amount_msat = token_obj.amount + else: + raise ValueError("Invalid unit") + estimated_fee_sat = max(amount_msat // 1000 * 0.01, 2) + amount_msat_after_fee = amount_msat - estimated_fee_sat * 1000 + mint_quote = await wallet.mint_quote(amount_msat_after_fee, "sat") + melt_quote = await wallet.melt_quote(mint_quote.request, amount_msat_after_fee) + _ = await wallet.melt( + proofs=token_obj.proofs, + invoice=mint_quote.request, + fee_reserve=melt_quote.fee_reserve, + quote_id=melt_quote.quote, + ) + + _ = await wallet.mint(token_obj.amount, mint_quote.quote) + + return token_obj.amount, "sat", PRIMARY_MINT_URL + + +# insert initial token state here to reduce db calls +# async def create_refund_token( +# amount: int, unit: CurrencyUnit, mint_url: str | None = None +# ) -> str: +# wallet = await Wallet.with_db( +# mint_url, DATABASE_URL, load_all_keysets=True, unit=unit +# ) +# if wallet.balance_per_minturl(unit=unit)[mint_url] < amount: +# raise ValueError("Wallet has no balance") +# if mint_url is None: +# mint_url = wallet.mint_urls[0] +# return await wallet._make_token(amount, unit=unit, mint_url=mint_url) + + +# async def redeem_token(token: str) -> Token: +# token_obj = deserialize_token_from_string(token) +# wallet = await Wallet.with_db( +# token_obj.mint, +# DATABASE_URL, +# load_all_keysets=True, +# unit=token_obj.unit, +# ) +# return await redeem_universal(wallet, token_obj) + + +async def credit_balance(cashu_token: str, key: ApiKey, session: AsyncSession) -> int: + amount, unit, mint_url = await recieve_token(cashu_token) + if unit == "sat": + amount = amount * 1000 + if mint_url != PRIMARY_MINT_URL: + raise ValueError("Mint URL is not supported by this proxy") + key.balance += amount + session.add(key) + await session.commit() + logger.info( + "Cashu token successfully redeemed and stored", + extra={"amount": amount, "unit": unit, "mint_url": mint_url}, + ) + return amount + + +async def send_to_lnurl(amount: int, unit: CurrencyUnit, lnurl: str) -> dict[str, int]: + raise NotImplementedError + + +async def check_for_refunds() -> None: + print("check_for_refunds, temp not implemented") + + +async def init_wallet() -> None: + balance = await get_balance("sat") + print(f"init_wallet, balance: {balance}") + + +async def periodic_payout() -> None: + print("periodic_payout, temp not implemented") + + +# class Proof: +# """ +# Represents an ecash bill +# """ + + +# def redeem_to_proofs(self, token: str) -> list[Proof]: +# raise NotImplementedError + + +# class Payment: +# """ +# Stores all cashu payment related data +# """ + +# def __init__(self, token: str) -> None: +# self.initial_token = token +# amount, unit, mint_url = self.parse_token(token) +# self.amount = amount +# self.unit = unit +# self.mint_url = mint_url + +# self.claimed_proofs = redeem_to_proofs(token) + +# def parse_token(self, token: str) -> tuple[int, CurrencyUnit, str]: +# raise NotImplementedError + +# def refund_full(self) -> None: +# raise NotImplementedError + +# def refund_partial(self, amount: int) -> None: +# raise NotImplementedError diff --git a/tests/conftest.py b/tests/conftest.py index ba1bf03b..d69640c2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ import asyncio import os from typing import AsyncGenerator, Generator -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import patch import pytest import pytest_asyncio @@ -14,14 +14,14 @@ from sqlmodel.ext.asyncio.session import AsyncSession # Save original environment variables ORIGINAL_ENV = os.environ.copy() -# Set test environment variables before importing the app +# Set test environment variables BEFORE importing the app TEST_ENV = { "UPSTREAM_BASE_URL": "https://api.example.com", "UPSTREAM_API_KEY": "test-upstream-key", "NAME": "TestRoutstrNode", "DESCRIPTION": "Test Node", "NPUB": "npub1test", - "MINT": "https://test.mint.com", + "CASHU_MINTS": "https://test.mint.com", "HTTP_URL": "http://test.example.com", "ONION_URL": "http://test.onion", "CORS_ORIGINS": "*", @@ -36,28 +36,9 @@ TEST_ENV = { # Apply test environment os.environ.update(TEST_ENV) -# Mock the Wallet class from sixty_nuts before importing the app -with patch("sixty_nuts.Wallet") as mock_wallet_class: - # Create a mock wallet instance - mock_wallet = AsyncMock() - mock_wallet.__aenter__ = AsyncMock(return_value=mock_wallet) - mock_wallet.__aexit__ = AsyncMock(return_value=None) - - # Mock wallet state - mock_state = MagicMock() - mock_state.balance = 1000 # Balance in sats - mock_wallet.fetch_wallet_state = AsyncMock(return_value=mock_state) - - # Mock other wallet methods - mock_wallet.send_to_lnurl = AsyncMock(return_value=100) - mock_wallet.redeem = AsyncMock(return_value=(1, "msat")) - mock_wallet.send = AsyncMock(return_value="cashu:token123") - - # Make the Wallet class return our mock when instantiated - mock_wallet_class.return_value = mock_wallet - - from router.db import get_session - from router.main import app +# Now import modules that depend on environment variables +from router.db import get_session # noqa: E402 +from router.main import app # noqa: E402 @pytest.fixture(scope="session") @@ -98,28 +79,9 @@ async def test_session(test_engine: AsyncEngine) -> AsyncGenerator[AsyncSession, def test_client() -> Generator[TestClient, None, None]: """Create a test client for the FastAPI app.""" with patch.dict(os.environ, TEST_ENV, clear=True): - with patch("sixty_nuts.Wallet") as mock_wallet_class: - # Create a mock wallet instance - mock_wallet = AsyncMock() - mock_wallet.__aenter__ = AsyncMock(return_value=mock_wallet) - mock_wallet.__aexit__ = AsyncMock(return_value=None) - - # Mock wallet state - mock_state = MagicMock() - mock_state.balance = 1000 # Balance in sats - mock_wallet.fetch_wallet_state = AsyncMock(return_value=mock_state) - - # Mock other wallet methods - mock_wallet.send_to_lnurl = AsyncMock(return_value=100) - mock_wallet.redeem = AsyncMock(return_value=(1, "msat")) - mock_wallet.send = AsyncMock(return_value="cashu:token123") - - # Make the Wallet class return our mock when instantiated - mock_wallet_class.return_value = mock_wallet - - with patch("router.models.update_sats_pricing") as mock_update: - mock_update.return_value = None - yield TestClient(app) + with patch("router.models.update_sats_pricing") as mock_update: + mock_update.return_value = None + yield TestClient(app) @pytest_asyncio.fixture @@ -133,32 +95,14 @@ async def async_client(test_session: AsyncSession) -> AsyncGenerator[AsyncClient # Mock startup tasks with patch.dict(os.environ, TEST_ENV, clear=True): - with patch("sixty_nuts.Wallet") as mock_wallet_class: - # Create a mock wallet instance - mock_wallet = AsyncMock() - mock_wallet.__aenter__ = AsyncMock(return_value=mock_wallet) - mock_wallet.__aexit__ = AsyncMock(return_value=None) + with patch("router.models.update_sats_pricing") as mock_update: + mock_update.return_value = None - # Mock wallet state - mock_state = MagicMock() - mock_state.balance = 1000 # Balance in sats - mock_wallet.fetch_wallet_state = AsyncMock(return_value=mock_state) - - # Mock other wallet methods - mock_wallet.send_to_lnurl = AsyncMock(return_value=100) - mock_wallet.redeem = AsyncMock(return_value=(1, "msat")) - mock_wallet.send = AsyncMock(return_value="cashuAoken123") - - # Make the Wallet class return our mock when instantiated - mock_wallet_class.return_value = mock_wallet - - with patch("router.models.update_sats_pricing") as mock_update: - mock_update.return_value = None - - async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" - ) as client: - yield client + async with AsyncClient( + transport=ASGITransport(app=app), # type: ignore + base_url="http://test", + ) as client: + yield client app.dependency_overrides.clear() diff --git a/tests/test_proxy.py b/tests/test_proxy.py deleted file mode 100644 index af2df006..00000000 --- a/tests/test_proxy.py +++ /dev/null @@ -1,353 +0,0 @@ -import json -import os -import uuid -from typing import AsyncGenerator -from unittest.mock import AsyncMock, patch - -import pytest -import pytest_asyncio -from httpx import AsyncClient - -from router.db import ApiKey, AsyncSession - - -@pytest_asyncio.fixture -async def api_key_with_balance(test_session: AsyncSession) -> ApiKey: - """Create an API key with sufficient balance.""" - unique_id = str(uuid.uuid4())[:8] - key = ApiKey( - hashed_key=f"test-hashed-key-{unique_id}", - balance=10000000, # 10,000 sats in msats - refund_address=None, - total_spent=0, - total_requests=0, - ) - test_session.add(key) - await test_session.commit() - await test_session.refresh(key) - return key - - -@pytest.mark.asyncio -async def test_proxy_requires_authentication(async_client: AsyncClient) -> None: - """Test that proxy endpoints require authentication.""" - response = await async_client.post("/v1/chat/completions") - - assert response.status_code == 401 - assert response.json()["detail"] == "Unauthorized" - - -@pytest.mark.asyncio -async def test_proxy_empty_bearer_token(async_client: AsyncClient) -> None: - """Test that proxy endpoints return structured error for empty bearer token.""" - response = await async_client.post( - "/v1/chat/completions", headers={"Authorization": "Bearer "} - ) - - assert response.status_code == 401 - assert ( - "API key or Cashu token required" - in response.json()["detail"]["error"]["message"] - ) - - -@pytest.mark.asyncio -async def test_proxy_with_insufficient_balance( - async_client: AsyncClient, test_session: AsyncSession -) -> None: - """Test proxy request with insufficient balance.""" - # Create key with minimal balance - unique_id = str(uuid.uuid4())[:8] - key = ApiKey( - hashed_key=f"low-balance-key-{unique_id}", - balance=100, # Only 0.1 sats - refund_address=None, - total_spent=0, - total_requests=0, - ) - test_session.add(key) - await test_session.commit() - - # Mock the models.json check - with patch("os.path.exists", return_value=False): - response = await async_client.post( - "/v1/chat/completions", - headers={"Authorization": f"Bearer sk-{key.hashed_key}"}, - json={"model": "gpt-4", "messages": [{"role": "user", "content": "Hello"}]}, - ) - - assert response.status_code == 402 - assert "Insufficient balance" in response.json()["detail"]["error"]["message"] - - -@pytest.mark.asyncio -async def test_proxy_invalid_json_body( - async_client: AsyncClient, api_key_with_balance: ApiKey -) -> None: - """Test proxy request with invalid JSON body.""" - response = await async_client.post( - "/v1/chat/completions", - headers={ - "Authorization": f"Bearer sk-{api_key_with_balance.hashed_key}", - "Content-Type": "application/json", - }, - content=b'{"invalid": json",}', # Invalid JSON - ) - - assert response.status_code == 400 - error_data = response.json() - assert error_data["error"]["type"] == "invalid_request_error" - assert error_data["error"]["code"] == "invalid_json" - - -@pytest.mark.asyncio -async def test_proxy_successful_request_mock( - async_client: AsyncClient, api_key_with_balance: ApiKey, test_session: AsyncSession -) -> None: - """Test successful proxy request with mocked upstream.""" - mock_response_data = { - "id": "chatcmpl-123", - "object": "chat.completion", - "created": 1677652288, - "model": "gpt-4", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": "Hello! How can I help you?", - }, - "finish_reason": "stop", - } - ], - "usage": {"prompt_tokens": 9, "completion_tokens": 10, "total_tokens": 19}, - } - - with patch("httpx.AsyncClient") as mock_client_class: - mock_client = AsyncMock() - mock_client_class.return_value = mock_client - - # Create a mock response - mock_response = AsyncMock() - mock_response.status_code = 200 - mock_response.headers = {"content-type": "application/json"} - mock_response.aread = AsyncMock( - return_value=json.dumps(mock_response_data).encode() - ) - mock_response.aiter_bytes = AsyncMock() - mock_response.aclose = AsyncMock() - - mock_client.send = AsyncMock(return_value=mock_response) - mock_client.build_request = AsyncMock() - mock_client.aclose = AsyncMock() - - # Also mock the models.json check and pay_out - with patch("os.path.exists", return_value=False): - with patch("router.cashu.pay_out") as mock_payout: - mock_payout.return_value = None - - response = await async_client.post( - "/v1/chat/completions", - headers={ - "Authorization": f"Bearer sk-{api_key_with_balance.hashed_key}" - }, - json={ - "model": "gpt-4", - "messages": [{"role": "user", "content": "Hello"}], - }, - ) - - assert response.status_code == 200 - response_json = response.json() - - # Verify the response includes the original data plus cost - assert response_json["id"] == "chatcmpl-123" - assert "cost" in response_json - assert response_json["cost"]["total_msats"] >= 0 - - # Verify balance was deducted - await test_session.refresh(api_key_with_balance) - assert api_key_with_balance.balance < 10000000 - assert api_key_with_balance.total_requests == 1 - - -@pytest.mark.asyncio -async def test_proxy_streaming_response( - async_client: AsyncClient, api_key_with_balance: ApiKey -) -> None: - """Test proxy request with streaming response.""" - # Mock SSE stream chunks - stream_chunks = [ - b'data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-4","choices":[{"delta":{"content":"Hello"},"index":0}]}\n\n', - b'data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-4","choices":[{"delta":{"content":" there!"},"index":0}]}\n\n', - b'data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-4","choices":[{"delta":{},"index":0,"finish_reason":"stop"}],"usage":{"prompt_tokens":9,"completion_tokens":3,"total_tokens":12}}\n\n', - b"data: [DONE]\n\n", - ] - - async def mock_aiter_bytes() -> AsyncGenerator[bytes, None]: - for chunk in stream_chunks: - yield chunk - - with patch("httpx.AsyncClient") as mock_client_class: - mock_client = AsyncMock() - mock_client_class.return_value = mock_client - - mock_response = AsyncMock() - mock_response.status_code = 200 - mock_response.headers = {"content-type": "text/event-stream"} - mock_response.aiter_bytes = lambda: mock_aiter_bytes() - mock_response.aclose = AsyncMock() - - mock_client.send = AsyncMock(return_value=mock_response) - mock_client.build_request = AsyncMock() - mock_client.aclose = AsyncMock() - - with patch("os.path.exists", return_value=False): - with patch("router.cashu.pay_out") as mock_payout: - mock_payout.return_value = None - - response = await async_client.post( - "/v1/chat/completions", - headers={ - "Authorization": f"Bearer sk-{api_key_with_balance.hashed_key}" - }, - json={ - "model": "gpt-4", - "messages": [{"role": "user", "content": "Hello"}], - "stream": True, - }, - ) - - assert response.status_code == 200 - assert response.headers["content-type"] == "text/event-stream" - - -@pytest.mark.asyncio -async def test_proxy_handles_upstream_errors( - async_client: AsyncClient, api_key_with_balance: ApiKey -) -> None: - """Test proxy handles upstream connection errors gracefully.""" - with patch("httpx.AsyncClient") as mock_client_class: - mock_client = AsyncMock() - mock_client_class.return_value = mock_client - - # Simulate connection error - mock_client.send.side_effect = Exception("Connection refused") - mock_client.build_request = AsyncMock() - mock_client.aclose = AsyncMock() - - with patch("os.path.exists", return_value=False): - response = await async_client.post( - "/v1/chat/completions", - headers={ - "Authorization": f"Bearer sk-{api_key_with_balance.hashed_key}" - }, - json={ - "model": "gpt-4", - "messages": [{"role": "user", "content": "Hello"}], - }, - ) - - assert response.status_code == 500 - error_data = response.json() - assert error_data["error"]["type"] == "internal_error" - assert error_data["error"]["message"] == "An unexpected server error occurred" - - -@pytest.mark.asyncio -async def test_proxy_with_model_based_pricing( - async_client: AsyncClient, test_session: AsyncSession -) -> None: - """Test proxy with model-based pricing enabled.""" - # Create API key with sufficient balance - unique_id = str(uuid.uuid4())[:8] - key = ApiKey( - hashed_key=f"model-pricing-key-{unique_id}", - balance=10000000, # 10,000 sats - refund_address=None, - total_spent=0, - total_requests=0, - ) - test_session.add(key) - await test_session.commit() - - with patch.dict(os.environ, {"MODEL_BASED_PRICING": "true"}): - with patch("os.path.exists", return_value=True): - # Mock a model with pricing - from router.models import MODELS, Architecture, Model, Pricing, TopProvider - - test_model = Model( - id="gpt-4", - name="GPT-4", - created=1680000000, - description="Test model", - context_length=8192, - architecture=Architecture( - modality="text", - input_modalities=["text"], - output_modalities=["text"], - tokenizer="cl100k_base", - instruct_type="none", - ), - pricing=Pricing( - prompt=0.03, - completion=0.06, - request=0.001, - image=0.0, - web_search=0.0, - internal_reasoning=0.0, - ), - sats_pricing=Pricing( - prompt=300, # 300 sats per 1k tokens - completion=600, - request=10, - image=0.0, - web_search=0.0, - internal_reasoning=0.0, - max_cost=5000, # 5000 sats max - ), - top_provider=TopProvider( - context_length=8192, max_completion_tokens=4096, is_moderated=False - ), - ) - - # Temporarily replace models - original_models = MODELS[:] - MODELS.clear() - MODELS.append(test_model) - - # Mock the upstream HTTP client - with patch("httpx.AsyncClient") as mock_client_class: - mock_client = AsyncMock() - mock_client_class.return_value = mock_client - - # Create a mock response - mock_response = AsyncMock() - mock_response.status_code = 200 - mock_response.headers = {"content-type": "application/json"} - mock_response.aread = AsyncMock( - return_value=b'{"id": "test", "model": "gpt-4"}' - ) - mock_response.aiter_bytes = AsyncMock() - mock_response.aclose = AsyncMock() - - mock_client.send = AsyncMock(return_value=mock_response) - mock_client.build_request = AsyncMock() - mock_client.aclose = AsyncMock() - - try: - response = await async_client.post( - "/v1/chat/completions", - headers={"Authorization": f"Bearer sk-{key.hashed_key}"}, - json={ - "model": "gpt-4", - "messages": [{"role": "user", "content": "Hello"}], - }, - ) - - # Should succeed because balance (10,000 sats) > max_cost (5000 sats) - assert response.status_code == 200 - - finally: - MODELS.clear() - MODELS.extend(original_models) diff --git a/uv.lock b/uv.lock index 80b9d727..b1fb8dc5 100644 --- a/uv.lock +++ b/uv.lock @@ -28,6 +28,65 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, ] +[[package]] +name = "asn1crypto" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", size = 121080 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045 }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, +] + +[[package]] +name = "asyncpg" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/0e/f5d708add0d0b97446c402db7e8dd4c4183c13edaabe8a8500b411e7b495/asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a", size = 674506 }, + { url = "https://files.pythonhosted.org/packages/6a/a0/67ec9a75cb24a1d99f97b8437c8d56da40e6f6bd23b04e2f4ea5d5ad82ac/asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed", size = 645922 }, + { url = "https://files.pythonhosted.org/packages/5c/d9/a7584f24174bd86ff1053b14bb841f9e714380c672f61c906eb01d8ec433/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a", size = 3079565 }, + { url = "https://files.pythonhosted.org/packages/a0/d7/a4c0f9660e333114bdb04d1a9ac70db690dd4ae003f34f691139a5cbdae3/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956", size = 3109962 }, + { url = "https://files.pythonhosted.org/packages/3c/21/199fd16b5a981b1575923cbb5d9cf916fdc936b377e0423099f209e7e73d/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056", size = 3064791 }, + { url = "https://files.pythonhosted.org/packages/77/52/0004809b3427534a0c9139c08c87b515f1c77a8376a50ae29f001e53962f/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454", size = 3188696 }, + { url = "https://files.pythonhosted.org/packages/52/cb/fbad941cd466117be58b774a3f1cc9ecc659af625f028b163b1e646a55fe/asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d", size = 567358 }, + { url = "https://files.pythonhosted.org/packages/3c/0a/0a32307cf166d50e1ad120d9b81a33a948a1a5463ebfa5a96cc5606c0863/asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f", size = 629375 }, + { url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162 }, + { url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025 }, + { url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243 }, + { url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059 }, + { url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596 }, + { url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632 }, + { url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186 }, + { url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064 }, + { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373 }, + { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745 }, + { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103 }, + { url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471 }, + { url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253 }, + { url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720 }, + { url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404 }, + { url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623 }, +] + +[[package]] +name = "base58" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/45/8ae61209bb9015f516102fa559a2914178da1d5868428bd86a1b4421141d/base58-2.1.1.tar.gz", hash = "sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c", size = 6528 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/45/ec96b29162a402fc4c1c5512d114d7b3787b9d1c2ec241d9568b4816ee23/base58-2.1.1-py3-none-any.whl", hash = "sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2", size = 5621 }, +] + [[package]] name = "bech32" version = "1.2.0" @@ -37,6 +96,145 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/41/7022a226e5a6ac7091a95ba36bad057012ab7330b9894ad4e14e31d0b858/bech32-1.2.0-py3-none-any.whl", hash = "sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981", size = 4587 }, ] +[[package]] +name = "bip32" +version = "4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coincurve" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/37/b69968b1b6eaea1fedb8efdb1862d86e92b6f68e182f39c764f894984db5/bip32-4.0.tar.gz", hash = "sha256:8035588f252f569bb414bc60df151ae431fc1c6789a19488a32890532ef3a2fc", size = 21662 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/bd/dcf1650776a241c10a2bc6826b6e23ff63bf55373bb053b716c69c463758/bip32-4.0-py3-none-any.whl", hash = "sha256:9728b38336129c00e1f870bbb3e328c9632d51c1bddeef4011fd3115cb3aeff9", size = 12898 }, +] + +[[package]] +name = "bitstring" +version = "3.1.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/b1/80d58eeb21c9d4ca739770558d61f6adacb13aa4908f4f55e0974cbd25ee/bitstring-3.1.9.tar.gz", hash = "sha256:a5848a3f63111785224dca8bb4c0a75b62ecdef56a042c8d6be74b16f7e860e7", size = 198509 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/fa/ac153ef3c9668a093f33386edf7a20122962e9142b1105fbe2a4a4262785/bitstring-3.1.9-py3-none-any.whl", hash = "sha256:0de167daa6a00c9386255a7cac931b45e6e24e0ad7ea64f1f92a64ac23ad4578", size = 38388 }, +] + +[[package]] +name = "bolt11" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "base58" }, + { name = "bech32" }, + { name = "bitstring" }, + { name = "click" }, + { name = "coincurve" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/39/4b151129bac9a5a7bce390531659de760b065c679d6a47b20f9fa034f4e1/bolt11-2.1.1.tar.gz", hash = "sha256:4e903d77208bfc4de8fc7e183a0689ea54afe874c91d62524d3b8c09492fa7ea", size = 13872 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/9e/78e59887cbf94116bdc890af7726ae264d55df14f1c777724c656e8a35fe/bolt11-2.1.1-py3-none-any.whl", hash = "sha256:fd4edb9e73e27bf5e017f47c97f7c6827b523fcf9cab152b123961ca78323e2d", size = 17102 }, +] + +[[package]] +name = "brotli" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/12/ad41e7fadd5db55459c4c401842b47f7fee51068f86dd2894dd0dcfc2d2a/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", size = 873068 }, + { url = "https://files.pythonhosted.org/packages/95/4e/5afab7b2b4b61a84e9c75b17814198ce515343a44e2ed4488fac314cd0a9/Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", size = 446244 }, + { url = "https://files.pythonhosted.org/packages/9d/e6/f305eb61fb9a8580c525478a4a34c5ae1a9bcb12c3aee619114940bc513d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", size = 2906500 }, + { url = "https://files.pythonhosted.org/packages/3e/4f/af6846cfbc1550a3024e5d3775ede1e00474c40882c7bf5b37a43ca35e91/Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", size = 2943950 }, + { url = "https://files.pythonhosted.org/packages/b3/e7/ca2993c7682d8629b62630ebf0d1f3bb3d579e667ce8e7ca03a0a0576a2d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", size = 2918527 }, + { url = "https://files.pythonhosted.org/packages/b3/96/da98e7bedc4c51104d29cc61e5f449a502dd3dbc211944546a4cc65500d3/Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", size = 2845489 }, + { url = "https://files.pythonhosted.org/packages/e8/ef/ccbc16947d6ce943a7f57e1a40596c75859eeb6d279c6994eddd69615265/Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", size = 2914080 }, + { url = "https://files.pythonhosted.org/packages/80/d6/0bd38d758d1afa62a5524172f0b18626bb2392d717ff94806f741fcd5ee9/Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", size = 2813051 }, + { url = "https://files.pythonhosted.org/packages/14/56/48859dd5d129d7519e001f06dcfbb6e2cf6db92b2702c0c2ce7d97e086c1/Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", size = 2938172 }, + { url = "https://files.pythonhosted.org/packages/3d/77/a236d5f8cd9e9f4348da5acc75ab032ab1ab2c03cc8f430d24eea2672888/Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", size = 2933023 }, + { url = "https://files.pythonhosted.org/packages/f1/87/3b283efc0f5cb35f7f84c0c240b1e1a1003a5e47141a4881bf87c86d0ce2/Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", size = 2935871 }, + { url = "https://files.pythonhosted.org/packages/f3/eb/2be4cc3e2141dc1a43ad4ca1875a72088229de38c68e842746b342667b2a/Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", size = 2847784 }, + { url = "https://files.pythonhosted.org/packages/66/13/b58ddebfd35edde572ccefe6890cf7c493f0c319aad2a5badee134b4d8ec/Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", size = 3034905 }, + { url = "https://files.pythonhosted.org/packages/84/9c/bc96b6c7db824998a49ed3b38e441a2cae9234da6fa11f6ed17e8cf4f147/Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", size = 2929467 }, + { url = "https://files.pythonhosted.org/packages/e7/71/8f161dee223c7ff7fea9d44893fba953ce97cf2c3c33f78ba260a91bcff5/Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", size = 333169 }, + { url = "https://files.pythonhosted.org/packages/02/8a/fece0ee1057643cb2a5bbf59682de13f1725f8482b2c057d4e799d7ade75/Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", size = 357253 }, + { url = "https://files.pythonhosted.org/packages/5c/d0/5373ae13b93fe00095a58efcbce837fd470ca39f703a235d2a999baadfbc/Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", size = 815693 }, + { url = "https://files.pythonhosted.org/packages/8e/48/f6e1cdf86751300c288c1459724bfa6917a80e30dbfc326f92cea5d3683a/Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", size = 422489 }, + { url = "https://files.pythonhosted.org/packages/06/88/564958cedce636d0f1bed313381dfc4b4e3d3f6015a63dae6146e1b8c65c/Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", size = 873081 }, + { url = "https://files.pythonhosted.org/packages/58/79/b7026a8bb65da9a6bb7d14329fd2bd48d2b7f86d7329d5cc8ddc6a90526f/Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", size = 446244 }, + { url = "https://files.pythonhosted.org/packages/e5/18/c18c32ecea41b6c0004e15606e274006366fe19436b6adccc1ae7b2e50c2/Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", size = 2906505 }, + { url = "https://files.pythonhosted.org/packages/08/c8/69ec0496b1ada7569b62d85893d928e865df29b90736558d6c98c2031208/Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", size = 2944152 }, + { url = "https://files.pythonhosted.org/packages/ab/fb/0517cea182219d6768113a38167ef6d4eb157a033178cc938033a552ed6d/Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", size = 2919252 }, + { url = "https://files.pythonhosted.org/packages/c7/53/73a3431662e33ae61a5c80b1b9d2d18f58dfa910ae8dd696e57d39f1a2f5/Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", size = 2845955 }, + { url = "https://files.pythonhosted.org/packages/55/ac/bd280708d9c5ebdbf9de01459e625a3e3803cce0784f47d633562cf40e83/Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", size = 2914304 }, + { url = "https://files.pythonhosted.org/packages/76/58/5c391b41ecfc4527d2cc3350719b02e87cb424ef8ba2023fb662f9bf743c/Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", size = 2814452 }, + { url = "https://files.pythonhosted.org/packages/c7/4e/91b8256dfe99c407f174924b65a01f5305e303f486cc7a2e8a5d43c8bec3/Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", size = 2938751 }, + { url = "https://files.pythonhosted.org/packages/5a/a6/e2a39a5d3b412938362bbbeba5af904092bf3f95b867b4a3eb856104074e/Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", size = 2933757 }, + { url = "https://files.pythonhosted.org/packages/13/f0/358354786280a509482e0e77c1a5459e439766597d280f28cb097642fc26/Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", size = 2936146 }, + { url = "https://files.pythonhosted.org/packages/80/f7/daf538c1060d3a88266b80ecc1d1c98b79553b3f117a485653f17070ea2a/Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", size = 2848055 }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0eaa0585c4077d3c2d1edf322d8e97aabf317941d3a72d7b3ad8bce004b0/Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", size = 3035102 }, + { url = "https://files.pythonhosted.org/packages/d8/63/1c1585b2aa554fe6dbce30f0c18bdbc877fa9a1bf5ff17677d9cca0ac122/Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", size = 2930029 }, + { url = "https://files.pythonhosted.org/packages/5f/3b/4e3fd1893eb3bbfef8e5a80d4508bec17a57bb92d586c85c12d28666bb13/Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", size = 333276 }, + { url = "https://files.pythonhosted.org/packages/3d/d5/942051b45a9e883b5b6e98c041698b1eb2012d25e5948c58d6bf85b1bb43/Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", size = 357255 }, + { url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681 }, + { url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475 }, + { url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173 }, + { url = "https://files.pythonhosted.org/packages/ea/1d/e6ca79c96ff5b641df6097d299347507d39a9604bde8915e76bf026d6c77/Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", size = 2943803 }, + { url = "https://files.pythonhosted.org/packages/ac/a3/d98d2472e0130b7dd3acdbb7f390d478123dbf62b7d32bda5c830a96116d/Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", size = 2918946 }, + { url = "https://files.pythonhosted.org/packages/c4/a5/c69e6d272aee3e1423ed005d8915a7eaa0384c7de503da987f2d224d0721/Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", size = 2845707 }, + { url = "https://files.pythonhosted.org/packages/58/9f/4149d38b52725afa39067350696c09526de0125ebfbaab5acc5af28b42ea/Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", size = 2936231 }, + { url = "https://files.pythonhosted.org/packages/5a/5a/145de884285611838a16bebfdb060c231c52b8f84dfbe52b852a15780386/Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", size = 2848157 }, + { url = "https://files.pythonhosted.org/packages/50/ae/408b6bfb8525dadebd3b3dd5b19d631da4f7d46420321db44cd99dcf2f2c/Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", size = 3035122 }, + { url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206 }, + { url = "https://files.pythonhosted.org/packages/c2/f0/a61d9262cd01351df22e57ad7c34f66794709acab13f34be2675f45bf89d/Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", size = 333804 }, + { url = "https://files.pythonhosted.org/packages/7e/c1/ec214e9c94000d1c1974ec67ced1c970c148aa6b8d8373066123fc3dbf06/Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", size = 358517 }, +] + +[[package]] +name = "cashu" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiosqlite" }, + { name = "asyncpg" }, + { name = "bech32" }, + { name = "bip32" }, + { name = "bitstring" }, + { name = "bolt11" }, + { name = "brotli" }, + { name = "cbor2" }, + { name = "click" }, + { name = "cryptography" }, + { name = "ecdsa" }, + { name = "environs" }, + { name = "fastapi" }, + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "grpcio-tools" }, + { name = "h11" }, + { name = "httpx", extra = ["socks"] }, + { name = "importlib-metadata" }, + { name = "jinja2" }, + { name = "loguru" }, + { name = "mnemonic" }, + { name = "mypy-protobuf" }, + { name = "pycryptodomex" }, + { name = "pydantic" }, + { name = "pyjwt" }, + { name = "redis" }, + { name = "secp256k1" }, + { name = "setuptools" }, + { name = "slowapi" }, + { name = "sqlalchemy", extra = ["asyncio"] }, + { name = "types-protobuf" }, + { name = "uvicorn" }, + { name = "websocket-client" }, + { name = "websockets" }, + { name = "wheel" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/84/b60d5a007b48d8b3bc795f3bbe6f47a56cd42e7af828c0082e94eb9ce949/cashu-0.17.0.tar.gz", hash = "sha256:a37ce0630b5e1a2b938ae10e4a252f4ed089224d7a186fe2f4548d5f2be17831", size = 6992332 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/07/f4564f42b4b579d4a476b4280f082f3e861f684945842630b73ee0aa0309/cashu-0.17.0-py3-none-any.whl", hash = "sha256:23d64de4d076dc5cbab149d11b3e662b2a7adb7aaf8416e4c445e7acf216b37d", size = 7062992 }, +] + [[package]] name = "cbor2" version = "5.6.5" @@ -135,40 +333,34 @@ wheels = [ [[package]] name = "coincurve" -version = "21.0.0" +version = "20.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/a2/f2a38eb05b747ed3e54e1be33be339d4a14c1f5cc6a6e2b342b5e8160d51/coincurve-21.0.0.tar.gz", hash = "sha256:8b37ce4265a82bebf0e796e21a769e56fdbf8420411ccbe3fafee4ed75b6a6e5", size = 128986 } +dependencies = [ + { name = "asn1crypto" }, + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/4c/9e5e51e6c12cec6444c86697992f9c6ccffa19f84d042ff939c8b89206ff/coincurve-20.0.0.tar.gz", hash = "sha256:872419e404300302e938849b6b92a196fabdad651060b559dc310e52f8392829", size = 122865 } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/5a/9aaa096d830b5d1386335759e73038a5352f8cd670efed55d242f92d0bce/coincurve-21.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:65ec42cab9c60d587fb6275c71f0ebc580625c377a894c4818fb2a2b583a184b", size = 1390936 }, - { url = "https://files.pythonhosted.org/packages/8a/e4/37dd30ed171432e32c075a03237915c0e69a5a524a807f380d910b276a2a/coincurve-21.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5828cd08eab928db899238874d1aab12fa1236f30fe095a3b7e26a5fc81df0a3", size = 1384762 }, - { url = "https://files.pythonhosted.org/packages/09/fd/78870f4babed4981feb9b97b3189aec0f01a1a24be8a1ac04807dc68aa0d/coincurve-21.0.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54de1cac75182de9f71ce41415faafcaf788303e21cbd0188064e268d61625e5", size = 1597025 }, - { url = "https://files.pythonhosted.org/packages/9d/fb/b4850f8afc941655ef4c1204b50f9e21f841c6a64aa83a559277ca305cbd/coincurve-21.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07cda058d9394bea30d57a92fdc18ee3ca6b5bc8ef776a479a2ffec917105836", size = 1603987 }, - { url = "https://files.pythonhosted.org/packages/9d/b7/df41dbcec3f70e383fa024949ce8956ff3b2a1b9eac330fba18c2115eece/coincurve-21.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9070804d7c71badfe4f0bf19b728cfe7c70c12e733938ead6b1db37920b745c0", size = 1604762 }, - { url = "https://files.pythonhosted.org/packages/70/84/1b2437fc22590073eefb3da0418648b2d5b768951ef851822be8c164b998/coincurve-21.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:669ab5db393637824b226de058bb7ea0cb9a0236e1842d7b22f74d4a8a1f1ff1", size = 1637469 }, - { url = "https://files.pythonhosted.org/packages/9c/4b/893763b3964b3044071a450fdada4c5024dc16f7644258a7bd06cf41e2ba/coincurve-21.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3bcd538af097b3914ec3cb654262e72e224f95f2e9c1eb7fbd75d843ae4e528e", size = 1601177 }, - { url = "https://files.pythonhosted.org/packages/77/45/d2f42159cb461f5b070ff848244f1b83f3ea9ec3a3435368f9be33e4e276/coincurve-21.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45b6a5e6b5536e1f46f729829d99ce1f8f847308d339e8880fe7fa1646935c10", size = 1635597 }, - { url = "https://files.pythonhosted.org/packages/9a/7c/528cff0aa17acd6c64b10c4bd8bb0adb6c96420be4e170916150537f36f6/coincurve-21.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:87597cf30dfc05fa74218810776efacf8816813ab9fa6ea1490f94e9f8b15e77", size = 1328626 }, - { url = "https://files.pythonhosted.org/packages/cb/91/845b00da05b132e7bb3f3d1c4c301c195b39a9dc8f9962295ff340a27f18/coincurve-21.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:b992d1b1dac85d7f542d9acbcf245667438839484d7f2b032fd032256bcd778e", size = 1325365 }, - { url = "https://files.pythonhosted.org/packages/f3/61/a2d9e109f99b6f5e65e653ac998b0944c5b82c568ac142fcbb381a4803be/coincurve-21.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f60ad56113f08e8c540bb89f4f35f44d434311433195ffff22893ccfa335070c", size = 1391948 }, - { url = "https://files.pythonhosted.org/packages/24/5a/2da75ee00a722ef1fa068ada3bc34c564595ead86fef573434e2f0cb0a5c/coincurve-21.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1cb1cd19fb0be22e68ecb60ad950b41f18b9b02eebeffaac9391dc31f74f08f2", size = 1384958 }, - { url = "https://files.pythonhosted.org/packages/dc/50/6bf0bf7e8a9a9dd419ecc1e479dcb9fbfe657029276ad703806a25a2bef2/coincurve-21.0.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05d7e255a697b3475d7ae7640d3bdef3d5bc98ce9ce08dd387f780696606c33b", size = 1606576 }, - { url = "https://files.pythonhosted.org/packages/bd/ab/9e89908fdd09ad522938085587aaa821b022f4def16c286c5580cfc85811/coincurve-21.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a366c314df7217e3357bb8c7d2cda540b0bce180705f7a0ce2d1d9e28f62ad4", size = 1613642 }, - { url = "https://files.pythonhosted.org/packages/b7/75/050b6fd08978de85a7b480f0f220ab6a30967c0910119f3096a8dd40befc/coincurve-21.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b04778b75339c6e46deb9ae3bcfc2250fbe48d1324153e4310fc4996e135715", size = 1616974 }, - { url = "https://files.pythonhosted.org/packages/d7/62/2740ba0cafebf45708633635fecadcbe582d7a3ed1ce8b4637921feceaf8/coincurve-21.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8efcbdcd50cc219989a2662e6c6552f455efc000a15dd6ab3ebf4f9b187f41a3", size = 1644133 }, - { url = "https://files.pythonhosted.org/packages/94/14/1f27c3048c4084fa85ef65f42a4ca631f2b184336e6d9446fecec20e0a7f/coincurve-21.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6df44b4e3b7acdc1453ade52a52e3f8a5b53ecdd5a06bd200f1ec4b4e250f7d9", size = 1619918 }, - { url = "https://files.pythonhosted.org/packages/ca/22/7ec3ec4c8e7764daa25767d6674cb5741ea2d9b39ff758e9918d22a4b49b/coincurve-21.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bcc0831f07cb75b91c35c13b1362e7b9dc76c376b27d01ff577bec52005e22a8", size = 1645797 }, - { url = "https://files.pythonhosted.org/packages/fb/60/87982b7499943ab12605df7b14f6001fff331aca0881b260682461e2309d/coincurve-21.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:5dd7b66b83b143f3ad3861a68fc0279167a0bae44fe3931547400b7a200e90b1", size = 1329255 }, - { url = "https://files.pythonhosted.org/packages/62/c0/65b60b371579570931daca8a3f67debfc1482908b8ed03432297274a27da/coincurve-21.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:78dbe439e8cb22389956a4f2f2312813b4bd0531a0b691d4f8e868c7b366555d", size = 1325973 }, - { url = "https://files.pythonhosted.org/packages/b3/40/cce55adaec37a588eb24b67da8eb68926546458e12ed2c4c2a21deb93d4c/coincurve-21.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9df5ceb5de603b9caf270629996710cf5ed1d43346887bc3895a11258644b65b", size = 1391762 }, - { url = "https://files.pythonhosted.org/packages/ca/7a/628a30281d246ce98aea56592e0c8e79b03a93ee8b85d688db3388130c2d/coincurve-21.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:154467858d23c48f9e5ab380433bc2625027b50617400e2984cc16f5799ab601", size = 1384921 }, - { url = "https://files.pythonhosted.org/packages/61/cc/719c5da31e6ba07e438abcf962f7a365eb69a06a0621ca4f2a484f344e09/coincurve-21.0.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f57f07c44d14d939bed289cdeaba4acb986bba9f729a796b6a341eab1661eedc", size = 1606559 }, - { url = "https://files.pythonhosted.org/packages/b2/ee/dd14237013d732e7fc3248c0c33a1d36b88b5378dfa3e624a50a23fb6f19/coincurve-21.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fb03e3a388a93d31ed56a442bdec7983ea404490e21e12af76fb1dbf097082a", size = 1613684 }, - { url = "https://files.pythonhosted.org/packages/f0/05/eaa7f36a03376ced1c19e0cb563341cc83fe48f5734b2effe8f16d0ee0ab/coincurve-21.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d09ba4fd9d26b00b06645fcd768c5ad44832a1fa847ebe8fb44970d3204c3cb7", size = 1617001 }, - { url = "https://files.pythonhosted.org/packages/39/32/fc75f1dd914ac95eb2704425c7ca1a9f509f982e15d05e0ca895b9e6ea9c/coincurve-21.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1a1e7ee73bc1b3bcf14c7b0d1f44e6485785d3b53ef7b16173c36d3cefa57f93", size = 1643924 }, - { url = "https://files.pythonhosted.org/packages/1a/4b/8c6e65b5755e26fc02077803879747615c1c327047328d1784bccb4ff4c3/coincurve-21.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ad05952b6edc593a874df61f1bc79db99d716ec48ba4302d699e14a419fe6f51", size = 1619964 }, - { url = "https://files.pythonhosted.org/packages/64/bc/d0a743305ff9fa26e72b4c77b534d5958ec8030b3772555a7172a0c134e5/coincurve-21.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d2bf350ced38b73db9efa1ff8fd16a67a1cb35abb2dda50d89661b531f03fd3", size = 1645526 }, - { url = "https://files.pythonhosted.org/packages/9d/44/ab082e2dc8c9a45774f1bb9961f58b43c0882b866f5c469ead932d45a35d/coincurve-21.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:54d9500c56d5499375e579c3917472ffcf804c3584dd79052a79974280985c74", size = 1329285 }, - { url = "https://files.pythonhosted.org/packages/f3/94/407f6fc811310f15b1fc7255f436f6a9040854213beeb10093f56b5b7fd3/coincurve-21.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:773917f075ec4b94a7a742637d303a3a082616a115c36568eb6c873a8d950d18", size = 1326027 }, + { url = "https://files.pythonhosted.org/packages/24/a7/d60a41b3f0a546854c9b7ca65ab99a5fdf1c9e158ae264a580de8f23fd1c/coincurve-20.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44087d1126d43925bf9a2391ce5601bf30ce0dba4466c239172dc43226696018", size = 1255635 }, + { url = "https://files.pythonhosted.org/packages/b7/4a/727fab66c0fbecfd7beeb38467910bd3652a77df649565e30160a9d2bae2/coincurve-20.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ccf0ba38b0f307a9b3ce28933f6c71dc12ef3a0985712ca09f48591afd597c8", size = 1255536 }, + { url = "https://files.pythonhosted.org/packages/0f/8b/25d4ae5bb60665023e6d71681fada88ee95b5010dae6fc0b44d8b23b8df1/coincurve-20.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:566bc5986debdf8572b6be824fd4de03d533c49f3de778e29f69017ae3fe82d8", size = 1191928 }, + { url = "https://files.pythonhosted.org/packages/0d/86/8c32c512fa27bfe7cfe70329fd43ebac23c0c8cec202cf6e4f52854e7ce3/coincurve-20.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4d70283168e146f025005c15406086513d5d35e89a60cf4326025930d45013a", size = 1194365 }, + { url = "https://files.pythonhosted.org/packages/fe/74/fefbe512f54df7d02a7ea4821b87cf199a91b3565cdf0c94448b3f6b1af1/coincurve-20.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:763c6122dd7d5e7a81c86414ce360dbe9a2d4afa1ca6c853ee03d63820b3d0c5", size = 1204658 }, + { url = "https://files.pythonhosted.org/packages/09/68/05b29f881f628ce8e8468f5f7420f6c4d7c129f43964e81d15bf388ae67a/coincurve-20.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f00c361c356bcea386d47a191bb8ac60429f4b51c188966a201bfecaf306ff7f", size = 1215301 }, + { url = "https://files.pythonhosted.org/packages/ee/5d/d91549cf5a163797b0724dc2dcd551b908b6beddb6598b37743df7f6f3ec/coincurve-20.0.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4af57bdadd2e64d117dd0b33cfefe76e90c7a6c496a7b034fc65fd01ec249b15", size = 1204505 }, + { url = "https://files.pythonhosted.org/packages/37/0f/898022e08760fb57d281f3695576e859b0f8a8ac629670223d9066c3f60d/coincurve-20.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a26437b7cbde13fb6e09261610b788ca2a0ca2195c62030afd1e1e0d1a62e035", size = 1209305 }, + { url = "https://files.pythonhosted.org/packages/57/b9/643567d3f680ddf8d1bf10a56112ae7755296500d8eaaef498be637a8533/coincurve-20.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ed51f8bba35e6c7676ad65539c3dbc35acf014fc402101fa24f6b0a15a74ab9e", size = 1198932 }, + { url = "https://files.pythonhosted.org/packages/b3/3a/898f5c12469b292042608dd0702bcb0420ec32bac6b1ca2a0dd790f922bd/coincurve-20.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:594b840fc25d74118407edbbbc754b815f1bba9759dbf4f67f1c2b78396df2d3", size = 1193318 }, + { url = "https://files.pythonhosted.org/packages/8f/24/e1bf259dd57186fbdc7cec51909db320884162cfad5ec72cbaa63573ff9d/coincurve-20.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4df4416a6c0370d777aa725a25b14b04e45aa228da1251c258ff91444643f688", size = 1255671 }, + { url = "https://files.pythonhosted.org/packages/0a/c5/1817f87d1cd5ff50d8537fe60fb96f66b76dd02da885d970952e6189a801/coincurve-20.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1ccc3e4db55abf3fc0e604a187fdb05f0702bc5952e503d9a75f4ae6eeb4cb3a", size = 1255565 }, + { url = "https://files.pythonhosted.org/packages/90/9f/35e15f993717ed1dcc4c26d9771f073a1054af26808a0f421783bb4cd7e0/coincurve-20.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac8335b1658a2ef5b3eb66d52647742fe8c6f413ad5b9d5310d7ea6d8060d40f", size = 1191953 }, + { url = "https://files.pythonhosted.org/packages/4a/3d/6a9bc32e69b738b5e05f5027bace1da6722352a4a447e495d3c03a601d99/coincurve-20.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ac025e485a0229fd5394e0bf6b4a75f8a4f6cee0dcf6f0b01a2ef05c5210ff", size = 1194425 }, + { url = "https://files.pythonhosted.org/packages/1a/a6/15424973dc47fc7c87e3c0f8859f6f1b1032582ee9f1b85fdd5d1e33d630/coincurve-20.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e46e3f1c21b3330857bcb1a3a5b942f645c8bce912a8a2b252216f34acfe4195", size = 1204678 }, + { url = "https://files.pythonhosted.org/packages/6a/e7/71ddb4d66c11c4ad13e729362f8852e048ae452eba3dfcf57751842bb292/coincurve-20.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:df9ff9b17a1d27271bf476cf3fa92df4c151663b11a55d8cea838b8f88d83624", size = 1215395 }, + { url = "https://files.pythonhosted.org/packages/b9/7d/03e0a19cfff1d86f5d019afc69cfbff02caada701ed5a4a50abc63d4261c/coincurve-20.0.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4155759f071375699282e03b3d95fb473ee05c022641c077533e0d906311e57a", size = 1204552 }, + { url = "https://files.pythonhosted.org/packages/07/cd/e9bd4ca7d931653a35c74194da04191a9aecc54b8f48a554cd538dc810e4/coincurve-20.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0530b9dd02fc6f6c2916716974b79bdab874227f560c422801ade290e3fc5013", size = 1209392 }, + { url = "https://files.pythonhosted.org/packages/99/54/260053f14f74b99b645084231e1c76994134ded49407a3bba23a8ffc0ff6/coincurve-20.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:eacf9c0ce8739c84549a89c083b1f3526c8780b84517ee75d6b43d276e55f8a0", size = 1198932 }, + { url = "https://files.pythonhosted.org/packages/b4/b5/c465e09345dd38b9415f5d47ae7683b3f461db02fcc03e699b6b5687ab2b/coincurve-20.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:52a67bfddbd6224dfa42085c88ad176559801b57d6a8bd30d92ee040de88b7b3", size = 1193324 }, ] [[package]] @@ -241,43 +433,43 @@ toml = [ [[package]] name = "cryptography" -version = "45.0.3" +version = "43.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/1f/9fa001e74a1993a9cadd2333bb889e50c66327b8594ac538ab8a04f915b7/cryptography-45.0.3.tar.gz", hash = "sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899", size = 744738 } +sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989 } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/b2/2345dc595998caa6f68adf84e8f8b50d18e9fc4638d32b22ea8daedd4b7a/cryptography-45.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71", size = 7056239 }, - { url = "https://files.pythonhosted.org/packages/71/3d/ac361649a0bfffc105e2298b720d8b862330a767dab27c06adc2ddbef96a/cryptography-45.0.3-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b", size = 4205541 }, - { url = "https://files.pythonhosted.org/packages/70/3e/c02a043750494d5c445f769e9c9f67e550d65060e0bfce52d91c1362693d/cryptography-45.0.3-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f", size = 4433275 }, - { url = "https://files.pythonhosted.org/packages/40/7a/9af0bfd48784e80eef3eb6fd6fde96fe706b4fc156751ce1b2b965dada70/cryptography-45.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942", size = 4209173 }, - { url = "https://files.pythonhosted.org/packages/31/5f/d6f8753c8708912df52e67969e80ef70b8e8897306cd9eb8b98201f8c184/cryptography-45.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9", size = 3898150 }, - { url = "https://files.pythonhosted.org/packages/8b/50/f256ab79c671fb066e47336706dc398c3b1e125f952e07d54ce82cf4011a/cryptography-45.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56", size = 4466473 }, - { url = "https://files.pythonhosted.org/packages/62/e7/312428336bb2df0848d0768ab5a062e11a32d18139447a76dfc19ada8eed/cryptography-45.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca", size = 4211890 }, - { url = "https://files.pythonhosted.org/packages/e7/53/8a130e22c1e432b3c14896ec5eb7ac01fb53c6737e1d705df7e0efb647c6/cryptography-45.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1", size = 4466300 }, - { url = "https://files.pythonhosted.org/packages/ba/75/6bb6579688ef805fd16a053005fce93944cdade465fc92ef32bbc5c40681/cryptography-45.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578", size = 4332483 }, - { url = "https://files.pythonhosted.org/packages/2f/11/2538f4e1ce05c6c4f81f43c1ef2bd6de7ae5e24ee284460ff6c77e42ca77/cryptography-45.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497", size = 4573714 }, - { url = "https://files.pythonhosted.org/packages/f5/bb/e86e9cf07f73a98d84a4084e8fd420b0e82330a901d9cac8149f994c3417/cryptography-45.0.3-cp311-abi3-win32.whl", hash = "sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710", size = 2934752 }, - { url = "https://files.pythonhosted.org/packages/c7/75/063bc9ddc3d1c73e959054f1fc091b79572e716ef74d6caaa56e945b4af9/cryptography-45.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490", size = 3412465 }, - { url = "https://files.pythonhosted.org/packages/71/9b/04ead6015229a9396890d7654ee35ef630860fb42dc9ff9ec27f72157952/cryptography-45.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06", size = 7031892 }, - { url = "https://files.pythonhosted.org/packages/46/c7/c7d05d0e133a09fc677b8a87953815c522697bdf025e5cac13ba419e7240/cryptography-45.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57", size = 4196181 }, - { url = "https://files.pythonhosted.org/packages/08/7a/6ad3aa796b18a683657cef930a986fac0045417e2dc428fd336cfc45ba52/cryptography-45.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716", size = 4423370 }, - { url = "https://files.pythonhosted.org/packages/4f/58/ec1461bfcb393525f597ac6a10a63938d18775b7803324072974b41a926b/cryptography-45.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8", size = 4197839 }, - { url = "https://files.pythonhosted.org/packages/d4/3d/5185b117c32ad4f40846f579369a80e710d6146c2baa8ce09d01612750db/cryptography-45.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc", size = 3886324 }, - { url = "https://files.pythonhosted.org/packages/67/85/caba91a57d291a2ad46e74016d1f83ac294f08128b26e2a81e9b4f2d2555/cryptography-45.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342", size = 4450447 }, - { url = "https://files.pythonhosted.org/packages/ae/d1/164e3c9d559133a38279215c712b8ba38e77735d3412f37711b9f8f6f7e0/cryptography-45.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b", size = 4200576 }, - { url = "https://files.pythonhosted.org/packages/71/7a/e002d5ce624ed46dfc32abe1deff32190f3ac47ede911789ee936f5a4255/cryptography-45.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782", size = 4450308 }, - { url = "https://files.pythonhosted.org/packages/87/ad/3fbff9c28cf09b0a71e98af57d74f3662dea4a174b12acc493de00ea3f28/cryptography-45.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65", size = 4325125 }, - { url = "https://files.pythonhosted.org/packages/f5/b4/51417d0cc01802304c1984d76e9592f15e4801abd44ef7ba657060520bf0/cryptography-45.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b", size = 4560038 }, - { url = "https://files.pythonhosted.org/packages/80/38/d572f6482d45789a7202fb87d052deb7a7b136bf17473ebff33536727a2c/cryptography-45.0.3-cp37-abi3-win32.whl", hash = "sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab", size = 2924070 }, - { url = "https://files.pythonhosted.org/packages/91/5a/61f39c0ff4443651cc64e626fa97ad3099249152039952be8f344d6b0c86/cryptography-45.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2", size = 3395005 }, - { url = "https://files.pythonhosted.org/packages/e7/d4/58a246342093a66af8935d6aa59f790cbb4731adae3937b538d054bdc2f9/cryptography-45.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:edd6d51869beb7f0d472e902ef231a9b7689508e83880ea16ca3311a00bf5ce7", size = 3589802 }, - { url = "https://files.pythonhosted.org/packages/96/61/751ebea58c87b5be533c429f01996050a72c7283b59eee250275746632ea/cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:555e5e2d3a53b4fabeca32835878b2818b3f23966a4efb0d566689777c5a12c8", size = 4146964 }, - { url = "https://files.pythonhosted.org/packages/8d/01/28c90601b199964de383da0b740b5156f5d71a1da25e7194fdf793d373ef/cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:25286aacb947286620a31f78f2ed1a32cded7be5d8b729ba3fb2c988457639e4", size = 4388103 }, - { url = "https://files.pythonhosted.org/packages/3d/ec/cd892180b9e42897446ef35c62442f5b8b039c3d63a05f618aa87ec9ebb5/cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:050ce5209d5072472971e6efbfc8ec5a8f9a841de5a4db0ebd9c2e392cb81972", size = 4150031 }, - { url = "https://files.pythonhosted.org/packages/db/d4/22628c2dedd99289960a682439c6d3aa248dff5215123ead94ac2d82f3f5/cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dc10ec1e9f21f33420cc05214989544727e776286c1c16697178978327b95c9c", size = 4387389 }, - { url = "https://files.pythonhosted.org/packages/39/ec/ba3961abbf8ecb79a3586a4ff0ee08c9d7a9938b4312fb2ae9b63f48a8ba/cryptography-45.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9eda14f049d7f09c2e8fb411dda17dd6b16a3c76a1de5e249188a32aeb92de19", size = 3337432 }, + { url = "https://files.pythonhosted.org/packages/1f/f3/01fdf26701a26f4b4dbc337a26883ad5bccaa6f1bbbdd29cd89e22f18a1c/cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", size = 6225303 }, + { url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905 }, + { url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271 }, + { url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606 }, + { url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484 }, + { url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131 }, + { url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647 }, + { url = "https://files.pythonhosted.org/packages/56/48/7b6b190f1462818b324e674fa20d1d5ef3e24f2328675b9b16189cbf0b3c/cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", size = 2623873 }, + { url = "https://files.pythonhosted.org/packages/eb/b1/0ebff61a004f7f89e7b65ca95f2f2375679d43d0290672f7713ee3162aff/cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", size = 3068039 }, + { url = "https://files.pythonhosted.org/packages/30/d5/c8b32c047e2e81dd172138f772e81d852c51f0f2ad2ae8a24f1122e9e9a7/cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", size = 6222984 }, + { url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968 }, + { url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754 }, + { url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458 }, + { url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220 }, + { url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898 }, + { url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592 }, + { url = "https://files.pythonhosted.org/packages/81/1e/ffcc41b3cebd64ca90b28fd58141c5f68c83d48563c88333ab660e002cd3/cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", size = 2623145 }, + { url = "https://files.pythonhosted.org/packages/87/5c/3dab83cc4aba1f4b0e733e3f0c3e7d4386440d660ba5b1e3ff995feb734d/cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", size = 3068026 }, +] + +[[package]] +name = "deprecated" +version = "1.2.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 }, ] [[package]] @@ -298,6 +490,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 }, ] +[[package]] +name = "ecdsa" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/1f/924e3caae75f471eae4b26bd13b698f6af2c44279f67af317439c2f4c46a/ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61", size = 201793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607 }, +] + [[package]] name = "email-validator" version = "2.2.0" @@ -311,6 +515,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521 }, ] +[[package]] +name = "environs" +version = "9.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/e3/c3c6c76f3dbe3e019e9a451b35bf9f44690026a5bb1232f7b77097b72ff5/environs-9.5.0.tar.gz", hash = "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9", size = 20795 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/5e/f0f217dc393372681bfe05c50f06a212e78d0a3fee907a74ab451ec1dcdb/environs-9.5.0-py2.py3-none-any.whl", hash = "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124", size = 12548 }, +] + [[package]] name = "fastapi" version = "0.115.12" @@ -354,6 +571,18 @@ standard = [ { name = "uvicorn", extra = ["standard"] }, ] +[[package]] +name = "googleapis-common-protos" +version = "1.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530 }, +] + [[package]] name = "greenlet" version = "3.2.1" @@ -397,6 +626,87 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/e6/f9d759788518a6248684e3afeb3691f3ab0276d769b6217a1533362298c8/greenlet-3.2.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d6668caf15f181c1b82fb6406f3911696975cc4c37d782e19cb7ba499e556189", size = 269897 }, ] +[[package]] +name = "grpcio" +version = "1.74.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/b4/35feb8f7cab7239c5b94bd2db71abb3d6adb5f335ad8f131abb6060840b6/grpcio-1.74.0.tar.gz", hash = "sha256:80d1f4fbb35b0742d3e3d3bb654b7381cd5f015f8497279a1e9c21ba623e01b1", size = 12756048 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/77/b2f06db9f240a5abeddd23a0e49eae2b6ac54d85f0e5267784ce02269c3b/grpcio-1.74.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:69e1a8180868a2576f02356565f16635b99088da7df3d45aaa7e24e73a054e31", size = 5487368 }, + { url = "https://files.pythonhosted.org/packages/48/99/0ac8678a819c28d9a370a663007581744a9f2a844e32f0fa95e1ddda5b9e/grpcio-1.74.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8efe72fde5500f47aca1ef59495cb59c885afe04ac89dd11d810f2de87d935d4", size = 10999804 }, + { url = "https://files.pythonhosted.org/packages/45/c6/a2d586300d9e14ad72e8dc211c7aecb45fe9846a51e558c5bca0c9102c7f/grpcio-1.74.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:a8f0302f9ac4e9923f98d8e243939a6fb627cd048f5cd38595c97e38020dffce", size = 5987667 }, + { url = "https://files.pythonhosted.org/packages/c9/57/5f338bf56a7f22584e68d669632e521f0de460bb3749d54533fc3d0fca4f/grpcio-1.74.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f609a39f62a6f6f05c7512746798282546358a37ea93c1fcbadf8b2fed162e3", size = 6655612 }, + { url = "https://files.pythonhosted.org/packages/82/ea/a4820c4c44c8b35b1903a6c72a5bdccec92d0840cf5c858c498c66786ba5/grpcio-1.74.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c98e0b7434a7fa4e3e63f250456eaef52499fba5ae661c58cc5b5477d11e7182", size = 6219544 }, + { url = "https://files.pythonhosted.org/packages/a4/17/0537630a921365928f5abb6d14c79ba4dcb3e662e0dbeede8af4138d9dcf/grpcio-1.74.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:662456c4513e298db6d7bd9c3b8df6f75f8752f0ba01fb653e252ed4a59b5a5d", size = 6334863 }, + { url = "https://files.pythonhosted.org/packages/e2/a6/85ca6cb9af3f13e1320d0a806658dca432ff88149d5972df1f7b51e87127/grpcio-1.74.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3d14e3c4d65e19d8430a4e28ceb71ace4728776fd6c3ce34016947474479683f", size = 7019320 }, + { url = "https://files.pythonhosted.org/packages/4f/a7/fe2beab970a1e25d2eff108b3cf4f7d9a53c185106377a3d1989216eba45/grpcio-1.74.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bf949792cee20d2078323a9b02bacbbae002b9e3b9e2433f2741c15bdeba1c4", size = 6514228 }, + { url = "https://files.pythonhosted.org/packages/6a/c2/2f9c945c8a248cebc3ccda1b7a1bf1775b9d7d59e444dbb18c0014e23da6/grpcio-1.74.0-cp311-cp311-win32.whl", hash = "sha256:55b453812fa7c7ce2f5c88be3018fb4a490519b6ce80788d5913f3f9d7da8c7b", size = 3817216 }, + { url = "https://files.pythonhosted.org/packages/ff/d1/a9cf9c94b55becda2199299a12b9feef0c79946b0d9d34c989de6d12d05d/grpcio-1.74.0-cp311-cp311-win_amd64.whl", hash = "sha256:86ad489db097141a907c559988c29718719aa3e13370d40e20506f11b4de0d11", size = 4495380 }, + { url = "https://files.pythonhosted.org/packages/4c/5d/e504d5d5c4469823504f65687d6c8fb97b7f7bf0b34873b7598f1df24630/grpcio-1.74.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:8533e6e9c5bd630ca98062e3a1326249e6ada07d05acf191a77bc33f8948f3d8", size = 5445551 }, + { url = "https://files.pythonhosted.org/packages/43/01/730e37056f96f2f6ce9f17999af1556df62ee8dab7fa48bceeaab5fd3008/grpcio-1.74.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:2918948864fec2a11721d91568effffbe0a02b23ecd57f281391d986847982f6", size = 10979810 }, + { url = "https://files.pythonhosted.org/packages/79/3d/09fd100473ea5c47083889ca47ffd356576173ec134312f6aa0e13111dee/grpcio-1.74.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:60d2d48b0580e70d2e1954d0d19fa3c2e60dd7cbed826aca104fff518310d1c5", size = 5941946 }, + { url = "https://files.pythonhosted.org/packages/8a/99/12d2cca0a63c874c6d3d195629dcd85cdf5d6f98a30d8db44271f8a97b93/grpcio-1.74.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3601274bc0523f6dc07666c0e01682c94472402ac2fd1226fd96e079863bfa49", size = 6621763 }, + { url = "https://files.pythonhosted.org/packages/9d/2c/930b0e7a2f1029bbc193443c7bc4dc2a46fedb0203c8793dcd97081f1520/grpcio-1.74.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:176d60a5168d7948539def20b2a3adcce67d72454d9ae05969a2e73f3a0feee7", size = 6180664 }, + { url = "https://files.pythonhosted.org/packages/db/d5/ff8a2442180ad0867717e670f5ec42bfd8d38b92158ad6bcd864e6d4b1ed/grpcio-1.74.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e759f9e8bc908aaae0412642afe5416c9f983a80499448fcc7fab8692ae044c3", size = 6301083 }, + { url = "https://files.pythonhosted.org/packages/b0/ba/b361d390451a37ca118e4ec7dccec690422e05bc85fba2ec72b06cefec9f/grpcio-1.74.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9e7c4389771855a92934b2846bd807fc25a3dfa820fd912fe6bd8136026b2707", size = 6994132 }, + { url = "https://files.pythonhosted.org/packages/3b/0c/3a5fa47d2437a44ced74141795ac0251bbddeae74bf81df3447edd767d27/grpcio-1.74.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cce634b10aeab37010449124814b05a62fb5f18928ca878f1bf4750d1f0c815b", size = 6489616 }, + { url = "https://files.pythonhosted.org/packages/ae/95/ab64703b436d99dc5217228babc76047d60e9ad14df129e307b5fec81fd0/grpcio-1.74.0-cp312-cp312-win32.whl", hash = "sha256:885912559974df35d92219e2dc98f51a16a48395f37b92865ad45186f294096c", size = 3807083 }, + { url = "https://files.pythonhosted.org/packages/84/59/900aa2445891fc47a33f7d2f76e00ca5d6ae6584b20d19af9c06fa09bf9a/grpcio-1.74.0-cp312-cp312-win_amd64.whl", hash = "sha256:42f8fee287427b94be63d916c90399ed310ed10aadbf9e2e5538b3e497d269bc", size = 4490123 }, + { url = "https://files.pythonhosted.org/packages/d4/d8/1004a5f468715221450e66b051c839c2ce9a985aa3ee427422061fcbb6aa/grpcio-1.74.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:2bc2d7d8d184e2362b53905cb1708c84cb16354771c04b490485fa07ce3a1d89", size = 5449488 }, + { url = "https://files.pythonhosted.org/packages/94/0e/33731a03f63740d7743dced423846c831d8e6da808fcd02821a4416df7fa/grpcio-1.74.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c14e803037e572c177ba54a3e090d6eb12efd795d49327c5ee2b3bddb836bf01", size = 10974059 }, + { url = "https://files.pythonhosted.org/packages/0d/c6/3d2c14d87771a421205bdca991467cfe473ee4c6a1231c1ede5248c62ab8/grpcio-1.74.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f6ec94f0e50eb8fa1744a731088b966427575e40c2944a980049798b127a687e", size = 5945647 }, + { url = "https://files.pythonhosted.org/packages/c5/83/5a354c8aaff58594eef7fffebae41a0f8995a6258bbc6809b800c33d4c13/grpcio-1.74.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:566b9395b90cc3d0d0c6404bc8572c7c18786ede549cdb540ae27b58afe0fb91", size = 6626101 }, + { url = "https://files.pythonhosted.org/packages/3f/ca/4fdc7bf59bf6994aa45cbd4ef1055cd65e2884de6113dbd49f75498ddb08/grpcio-1.74.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1ea6176d7dfd5b941ea01c2ec34de9531ba494d541fe2057c904e601879f249", size = 6182562 }, + { url = "https://files.pythonhosted.org/packages/fd/48/2869e5b2c1922583686f7ae674937986807c2f676d08be70d0a541316270/grpcio-1.74.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:64229c1e9cea079420527fa8ac45d80fc1e8d3f94deaa35643c381fa8d98f362", size = 6303425 }, + { url = "https://files.pythonhosted.org/packages/a6/0e/bac93147b9a164f759497bc6913e74af1cb632c733c7af62c0336782bd38/grpcio-1.74.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:0f87bddd6e27fc776aacf7ebfec367b6d49cad0455123951e4488ea99d9b9b8f", size = 6996533 }, + { url = "https://files.pythonhosted.org/packages/84/35/9f6b2503c1fd86d068b46818bbd7329db26a87cdd8c01e0d1a9abea1104c/grpcio-1.74.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3b03d8f2a07f0fea8c8f74deb59f8352b770e3900d143b3d1475effcb08eec20", size = 6491489 }, + { url = "https://files.pythonhosted.org/packages/75/33/a04e99be2a82c4cbc4039eb3a76f6c3632932b9d5d295221389d10ac9ca7/grpcio-1.74.0-cp313-cp313-win32.whl", hash = "sha256:b6a73b2ba83e663b2480a90b82fdae6a7aa6427f62bf43b29912c0cfd1aa2bfa", size = 3805811 }, + { url = "https://files.pythonhosted.org/packages/34/80/de3eb55eb581815342d097214bed4c59e806b05f1b3110df03b2280d6dfd/grpcio-1.74.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd3c71aeee838299c5887230b8a1822795325ddfea635edd82954c1eaa831e24", size = 4489214 }, +] + +[[package]] +name = "grpcio-tools" +version = "1.74.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/c8/bca79cb8c14bb63027831039919c801db9f593c7504c09433934f5dff6a4/grpcio_tools-1.74.0.tar.gz", hash = "sha256:88ab9eb18b6ac1b4872add6b394073bd8d44eee7c32e4dc60a022e25ffaffb95", size = 5390007 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/50/7bafe168b4b3494e7b96d4838b0d35eab62e5c74bf9c91e8f14233c94f60/grpcio_tools-1.74.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:9d9e28fbbab9b9e923c3d286949e8ff81ebbb402458698f0a2b1183b539779db", size = 2545457 }, + { url = "https://files.pythonhosted.org/packages/8b/1c/8a0eb4e101f2fe8edc12851ddfccf4f2498d5f23d444ea73d09c94202b46/grpcio_tools-1.74.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:41040eb1b5d1e582687f6f19cf2efc4c191b6eab56b16f6fba50ac085c5ca4dd", size = 5842973 }, + { url = "https://files.pythonhosted.org/packages/bb/f2/eb1bac2dd6397f5ca271e6cb2566b61d4a4bf8df07db0988bc55200f254d/grpcio_tools-1.74.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:1fdc013118e4e9054b6e1a64d16a0d4a17a4071042e674ada8673406ddb26e59", size = 2515918 }, + { url = "https://files.pythonhosted.org/packages/6b/fe/d270fd30ccd04d5faa9c3f2796ce56a0597eddf327a0fc746ccbb273cdd9/grpcio_tools-1.74.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f037414c527a2c4a3af15451d9e58d7856d0a62b3f6dd3f5b969ecba82f5e843", size = 2904944 }, + { url = "https://files.pythonhosted.org/packages/91/9f/3adb6e1ae826d9097745f4ad38a84c8c2edb4d768871222c95aa541f8e54/grpcio_tools-1.74.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536f53a6a8d1ba1c469d085066cfa0dd3bb51f07013b71857bc3ad1eabe3ab49", size = 2656300 }, + { url = "https://files.pythonhosted.org/packages/3f/15/e532439218674c9e451e7f965a0a6bcd53344c4178c62dc1acd66ed93797/grpcio_tools-1.74.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1e23ff54dea7f6e9543dcebd2c0f4b7c9af39812966c05e1c5289477cb2bf2f7", size = 3051857 }, + { url = "https://files.pythonhosted.org/packages/ca/06/a63aeb1a16ab1508f2ed349faafb4e2e1fb2b048168a033e7392adab14c7/grpcio_tools-1.74.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:76072dee9fa99b33eb0c334a16e70d694df762df705c7a2481f702af33d81a28", size = 3501682 }, + { url = "https://files.pythonhosted.org/packages/47/1f/81da8c39874d9152fba5fa2bf3b6708c29ea3621fde30667509b9124ef06/grpcio_tools-1.74.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bdf91eb722f2990085b1342c277e212ec392e37bd493a2a21d9eb9238f28c3e", size = 3125364 }, + { url = "https://files.pythonhosted.org/packages/a3/64/a23256ecd34ceebe8aac8adedd4f65ed240572662899acb779cfcf5e0277/grpcio_tools-1.74.0-cp311-cp311-win32.whl", hash = "sha256:a036cd2a4223901e7a9f6a9b394326a9352a4ad70bdd3f1d893f1b231fcfdf7e", size = 993385 }, + { url = "https://files.pythonhosted.org/packages/dc/b8/a0d7359d93f0a2bbaf3b0d43eb8fa3e9f315e03ef4a4ebe05b4315a64644/grpcio_tools-1.74.0-cp311-cp311-win_amd64.whl", hash = "sha256:d1fdf245178158a92a2dc78e3545b6d13b6c917d9b80931fc85cfb3e9534a07d", size = 1157908 }, + { url = "https://files.pythonhosted.org/packages/5e/9c/08a4018e19c937af14bfa052ad3d7826a1687da984992d31d15139c7c8d3/grpcio_tools-1.74.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:61d84f6050d7170712600f7ee1dac8849f5dc0bfe0044dd71132ee1e7aa2b373", size = 2546097 }, + { url = "https://files.pythonhosted.org/packages/0a/7b/b2985b1b8aa295d745b2e105c99401ad674fcdc2f5a9c8eb3ec0f57ad397/grpcio_tools-1.74.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f0129a62711dbc1f1efd51d069d2ce0631d69e033bf3a046606c623acf935e08", size = 5839819 }, + { url = "https://files.pythonhosted.org/packages/de/40/de0fe696d50732c8b1f0f9271b05a3082f2a91e77e28d70dd3ffc1e4aaa5/grpcio_tools-1.74.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:5ec661f3bb41f0d2a30125ea382f4d5c874bf4f26d4d8e3839bb7e3b3c037b3e", size = 2517611 }, + { url = "https://files.pythonhosted.org/packages/a0/6d/949d3b339c3ff3c631168b355ce7be937f10feb894fdabe66c48ebd82394/grpcio_tools-1.74.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7970a9cf3002bec2eff5a449ac7398b77e5d171cbb534c47258c72409d0aea74", size = 2905274 }, + { url = "https://files.pythonhosted.org/packages/06/6b/f9b2e7b15c147ad6164e9ac7b20ee208435ca3243bcc97feb1ab74dcb902/grpcio_tools-1.74.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f56d67b04790f84e216353341c6b298f1aeb591e1797fe955f606516c640936", size = 2656414 }, + { url = "https://files.pythonhosted.org/packages/bd/de/621dde431314f49668c25b26a12f624c3da8748ac29df9db7d0a2596e575/grpcio_tools-1.74.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3d0c33cc984d21525f190cb1af479f8da46370df5f2ced1a4e50769ababd0c0", size = 3052690 }, + { url = "https://files.pythonhosted.org/packages/40/82/d43c9484174feea5a153371a011e06eabe508b97519a1e9a338b7ebdf43b/grpcio_tools-1.74.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:88e535c1cf349e57e371529ea9918f811c5eff88161f322bbc06d6222bad6d50", size = 3501214 }, + { url = "https://files.pythonhosted.org/packages/30/fc/195b90e4571f6c70665a25c7b748e13c2087025660d6d5aead9093f28b18/grpcio_tools-1.74.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c3cf9401ce72bc49582c2d80e0a2ee0e573e1c3c998c8bc5f739db8845e8e148", size = 3125689 }, + { url = "https://files.pythonhosted.org/packages/cb/81/fe8980e5fb768090ffc531902ec1b7e5bf1d92108ecf8b7305405b297475/grpcio_tools-1.74.0-cp312-cp312-win32.whl", hash = "sha256:b63e250da44b15c67b9a34c5c30c81059bde528fc8af092d7f43194469f7c719", size = 993069 }, + { url = "https://files.pythonhosted.org/packages/63/a9/7b081924d655787d56d2b409f703f0bf457b3dac10a67ad04dc7338e9aae/grpcio_tools-1.74.0-cp312-cp312-win_amd64.whl", hash = "sha256:519d7cae085ae6695a8031bb990bf7766a922332b0a531e51342abc5431b78b5", size = 1157502 }, + { url = "https://files.pythonhosted.org/packages/2f/65/307a72cf4bfa553a25e284bd1f27b94a53816ac01ddf432c398117b91b2a/grpcio_tools-1.74.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e2e22460355adbd0f25fdd7ed8b9ae53afb3875b9d5f34cdf1cf12559418245e", size = 2545750 }, + { url = "https://files.pythonhosted.org/packages/5b/8e/9b2217c15baadc7cfca3eba9f980e147452ca82f41767490f619edea3489/grpcio_tools-1.74.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:0cab5a2c6ae75b555fee8a1a9a9b575205171e1de392fe2d4139a29e67d8f5bb", size = 5838169 }, + { url = "https://files.pythonhosted.org/packages/ea/42/a6a158b7e91c0a358cddf3f9088b004c2bfa42d1f96154b9b8eb17e16d73/grpcio_tools-1.74.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:9b18afca48b55832402a716ea4634ef2b68927a8a17ddf4038f51812299255c9", size = 2517140 }, + { url = "https://files.pythonhosted.org/packages/05/db/d4576a07b2d1211822a070f76a99a9f4f4cb63496a02964ce77c88df8a28/grpcio_tools-1.74.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85f442a9e89e276bf89a0c9c76ea71647a927d967759333c1fa40300c27f7bd", size = 2905214 }, + { url = "https://files.pythonhosted.org/packages/77/dc/3713e75751f862d8c84f823ba935d486c0aac0b6f789fa61fbde04ad5019/grpcio_tools-1.74.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:051ce925b0b99ae2daf61b3cba19962b8655cc2a72758ce4081b89272206f5a3", size = 2656245 }, + { url = "https://files.pythonhosted.org/packages/bd/e4/01f9e8e0401d8e11a70ae8aff6899eb8c16536f69a0a9ffb25873588721c/grpcio_tools-1.74.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:98c7b8eb0de6984cd7fa7335ce3383b3bb9a1559edc238c811df88008d5d3593", size = 3052327 }, + { url = "https://files.pythonhosted.org/packages/28/c2/264b4e705375a834c9c7462847ae435c0be1644f03a705d3d7464af07bd5/grpcio_tools-1.74.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f8f7d17b7573b9a2a6b4183fa4a56a2ab17370c8d0541e1424cf0c9c6f863434", size = 3500706 }, + { url = "https://files.pythonhosted.org/packages/ee/c0/cc034cec5871a1918e7888e8ce700e06fab5bbb328f998a2f2750cd603b5/grpcio_tools-1.74.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:db08b91ea0cd66dc4b1b929100e7aa84c9c10c51573c8282ec1ba05b41f887ef", size = 3125098 }, + { url = "https://files.pythonhosted.org/packages/69/55/5792b681af82b3ff1e50ce0ccfbb6d52fc68a13932ed3da57e58d7dfb67b/grpcio_tools-1.74.0-cp313-cp313-win32.whl", hash = "sha256:4b6c5efb331ae9e5f614437f4a5938459a8a5a1ab3dfe133d2bbdeaba39b894d", size = 992431 }, + { url = "https://files.pythonhosted.org/packages/94/9f/626f0fe6bfc1c6917785c6a5ee2eb8c07b5a30771e4bf4cff3c1ab5b431b/grpcio_tools-1.74.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8324cd67f61f7900d227b36913ee5f0302ba3ba8777c8bc705afa8174098d28", size = 1157064 }, +] + [[package]] name = "h11" version = "0.14.0" @@ -450,17 +760,18 @@ wheels = [ [[package]] name = "httpx" -version = "0.28.1" +version = "0.25.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, + { name = "sniffio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +sdist = { url = "https://files.pythonhosted.org/packages/8c/23/911d93a022979d3ea295f659fbe7edb07b3f4561a477e83b3a6d0e0c914e/httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8", size = 123889 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, + { url = "https://files.pythonhosted.org/packages/a2/65/6940eeb21dcb2953778a6895281c179efd9100463ff08cb6232bb6480da7/httpx-0.25.2-py3-none-any.whl", hash = "sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118", size = 74980 }, ] [package.optional-dependencies] @@ -477,6 +788,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] +[[package]] +name = "importlib-metadata" +version = "6.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/eb/58c2ab27ee628ad801f56d4017fe62afab0293116f6d0b08f1d5bd46e06f/importlib_metadata-6.11.0.tar.gz", hash = "sha256:1231cf92d825c9e03cfc4da076a16de6422c863558229ea0b22b675657463443", size = 54593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/9b/ecce94952ab5ea74c31dcf9ccf78ccd484eebebef06019bf8cb579ab4519/importlib_metadata-6.11.0-py3-none-any.whl", hash = "sha256:f0afba6205ad8f8947c7d338b5342d5db2afbfd82f9cbef7879a9539cc12eb9b", size = 23427 }, +] + [[package]] name = "iniconfig" version = "2.1.0" @@ -545,6 +868,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/47/3729f00f35a696e68da15d64eb9283c330e776f3b5789bac7f2c0c4df209/jiter-0.9.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6f7838bc467ab7e8ef9f387bd6de195c43bad82a569c1699cb822f6609dd4cdf", size = 206867 }, ] +[[package]] +name = "limits" +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/32/95d4908a730213a5db40462b0e20c1b93a688b33eade8c4981bbf0ca08de/limits-5.4.0.tar.gz", hash = "sha256:27ebf55118e3c9045f0dbc476f4559b26d42f4b043db670afb8963f36cf07fd9", size = 95423 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/aa/b84c06700735332017bc095182756ee9fb71db650d89b50b6d63549c6fcd/limits-5.4.0-py3-none-any.whl", hash = "sha256:1afb03c0624cf004085532aa9524953f2565cf8b0a914e48dda89d172c13ceb7", size = 60950 }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 }, +] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -605,6 +955,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, ] +[[package]] +name = "marshmallow" +version = "3.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878 }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -614,6 +976,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] +[[package]] +name = "mnemonic" +version = "0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/8d/d4dc2b2bddfeb57cab4404a41749b577f578f71140ab754da9afa8f5c599/mnemonic-0.20.tar.gz", hash = "sha256:7c6fb5639d779388027a77944680aee4870f0fcd09b1e42a5525ee2ce4c625f6", size = 67596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/95/3e07c33ffb26f5823b45a1c30db8acea44763198c2bd393e07e884f3295f/mnemonic-0.20-py3-none-any.whl", hash = "sha256:acd2168872d0379e7a10873bb3e12bf6c91b35de758135c4fbd1015ef18fafc5", size = 62028 }, +] + [[package]] name = "mypy" version = "1.15.0" @@ -654,6 +1025,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, ] +[[package]] +name = "mypy-protobuf" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, + { name = "types-protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/6f/282d64d66bf48ce60e38a6560753f784e0f88ab245ac2fb5e93f701a36cd/mypy-protobuf-3.6.0.tar.gz", hash = "sha256:02f242eb3409f66889f2b1a3aa58356ec4d909cdd0f93115622e9e70366eca3c", size = 24445 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/73/d6b999782ae22f16971cc05378b3b33f6a89ede3b9619e8366aa23484bca/mypy_protobuf-3.6.0-py3-none-any.whl", hash = "sha256:56176e4d569070e7350ea620262478b49b7efceba4103d468448f1d21492fd6c", size = 16434 }, +] + [[package]] name = "openai" version = "1.77.0" @@ -691,6 +1075,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, ] +[[package]] +name = "protobuf" +version = "6.31.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603 }, + { url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283 }, + { url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604 }, + { url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115 }, + { url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070 }, + { url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724 }, +] + [[package]] name = "pycparser" version = "2.22" @@ -700,6 +1098,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, ] +[[package]] +name = "pycryptodomex" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/85/e24bf90972a30b0fcd16c73009add1d7d7cd9140c2498a68252028899e41/pycryptodomex-3.23.0.tar.gz", hash = "sha256:71909758f010c82bc99b0abf4ea12012c98962fbf0583c2164f8b84533c2e4da", size = 4922157 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/00/10edb04777069a42490a38c137099d4b17ba6e36a4e6e28bdc7470e9e853/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7b37e08e3871efe2187bc1fd9320cc81d87caf19816c648f24443483005ff886", size = 2498764 }, + { url = "https://files.pythonhosted.org/packages/6b/3f/2872a9c2d3a27eac094f9ceaa5a8a483b774ae69018040ea3240d5b11154/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:91979028227543010d7b2ba2471cf1d1e398b3f183cb105ac584df0c36dac28d", size = 1643012 }, + { url = "https://files.pythonhosted.org/packages/70/af/774c2e2b4f6570fbf6a4972161adbb183aeeaa1863bde31e8706f123bf92/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8962204c47464d5c1c4038abeadd4514a133b28748bcd9fa5b6d62e3cec6fa", size = 2187643 }, + { url = "https://files.pythonhosted.org/packages/de/a3/71065b24cb889d537954cedc3ae5466af00a2cabcff8e29b73be047e9a19/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a33986a0066860f7fcf7c7bd2bc804fa90e434183645595ae7b33d01f3c91ed8", size = 2273762 }, + { url = "https://files.pythonhosted.org/packages/c9/0b/ff6f43b7fbef4d302c8b981fe58467b8871902cdc3eb28896b52421422cc/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7947ab8d589e3178da3d7cdeabe14f841b391e17046954f2fbcd941705762b5", size = 2313012 }, + { url = "https://files.pythonhosted.org/packages/02/de/9d4772c0506ab6da10b41159493657105d3f8bb5c53615d19452afc6b315/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c25e30a20e1b426e1f0fa00131c516f16e474204eee1139d1603e132acffc314", size = 2186856 }, + { url = "https://files.pythonhosted.org/packages/28/ad/8b30efcd6341707a234e5eba5493700a17852ca1ac7a75daa7945fcf6427/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:da4fa650cef02db88c2b98acc5434461e027dce0ae8c22dd5a69013eaf510006", size = 2347523 }, + { url = "https://files.pythonhosted.org/packages/0f/02/16868e9f655b7670dbb0ac4f2844145cbc42251f916fc35c414ad2359849/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58b851b9effd0d072d4ca2e4542bf2a4abcf13c82a29fd2c93ce27ee2a2e9462", size = 2272825 }, + { url = "https://files.pythonhosted.org/packages/ca/18/4ca89ac737230b52ac8ffaca42f9c6f1fd07c81a6cd821e91af79db60632/pycryptodomex-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:a9d446e844f08299236780f2efa9898c818fe7e02f17263866b8550c7d5fb328", size = 1772078 }, + { url = "https://files.pythonhosted.org/packages/73/34/13e01c322db027682e00986873eca803f11c56ade9ba5bbf3225841ea2d4/pycryptodomex-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bc65bdd9fc8de7a35a74cab1c898cab391a4add33a8fe740bda00f5976ca4708", size = 1803656 }, + { url = "https://files.pythonhosted.org/packages/54/68/9504c8796b1805d58f4425002bcca20f12880e6fa4dc2fc9a668705c7a08/pycryptodomex-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c885da45e70139464f082018ac527fdaad26f1657a99ee13eecdce0f0ca24ab4", size = 1707172 }, + { url = "https://files.pythonhosted.org/packages/dd/9c/1a8f35daa39784ed8adf93a694e7e5dc15c23c741bbda06e1d45f8979e9e/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:06698f957fe1ab229a99ba2defeeae1c09af185baa909a31a5d1f9d42b1aaed6", size = 2499240 }, + { url = "https://files.pythonhosted.org/packages/7a/62/f5221a191a97157d240cf6643747558759126c76ee92f29a3f4aee3197a5/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2c2537863eccef2d41061e82a881dcabb04944c5c06c5aa7110b577cc487545", size = 1644042 }, + { url = "https://files.pythonhosted.org/packages/8c/fd/5a054543c8988d4ed7b612721d7e78a4b9bf36bc3c5ad45ef45c22d0060e/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587", size = 2186227 }, + { url = "https://files.pythonhosted.org/packages/c8/a9/8862616a85cf450d2822dbd4fff1fcaba90877907a6ff5bc2672cafe42f8/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f489c4765093fb60e2edafdf223397bc716491b2b69fe74367b70d6999257a5c", size = 2272578 }, + { url = "https://files.pythonhosted.org/packages/46/9f/bda9c49a7c1842820de674ab36c79f4fbeeee03f8ff0e4f3546c3889076b/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdc69d0d3d989a1029df0eed67cc5e8e5d968f3724f4519bd03e0ec68df7543c", size = 2312166 }, + { url = "https://files.pythonhosted.org/packages/03/cc/870b9bf8ca92866ca0186534801cf8d20554ad2a76ca959538041b7a7cf4/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bbcb1dd0f646484939e142462d9e532482bc74475cecf9c4903d4e1cd21f003", size = 2185467 }, + { url = "https://files.pythonhosted.org/packages/96/e3/ce9348236d8e669fea5dd82a90e86be48b9c341210f44e25443162aba187/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:8a4fcd42ccb04c31268d1efeecfccfd1249612b4de6374205376b8f280321744", size = 2346104 }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e869bcee87beb89040263c416a8a50204f7f7a83ac11897646c9e71e0daf/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:55ccbe27f049743a4caf4f4221b166560d3438d0b1e5ab929e07ae1702a4d6fd", size = 2271038 }, + { url = "https://files.pythonhosted.org/packages/8d/67/09ee8500dd22614af5fbaa51a4aee6e342b5fa8aecf0a6cb9cbf52fa6d45/pycryptodomex-3.23.0-cp37-abi3-win32.whl", hash = "sha256:189afbc87f0b9f158386bf051f720e20fa6145975f1e76369303d0f31d1a8d7c", size = 1771969 }, + { url = "https://files.pythonhosted.org/packages/69/96/11f36f71a865dd6df03716d33bd07a67e9d20f6b8d39820470b766af323c/pycryptodomex-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:52e5ca58c3a0b0bd5e100a9fbc8015059b05cffc6c66ce9d98b4b45e023443b9", size = 1803124 }, + { url = "https://files.pythonhosted.org/packages/f9/93/45c1cdcbeb182ccd2e144c693eaa097763b08b38cded279f0053ed53c553/pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51", size = 1707161 }, +] + [[package]] name = "pydantic" version = "1.10.22" @@ -742,6 +1170,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, ] +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, +] + [[package]] name = "pytest" version = "8.3.5" @@ -791,6 +1228,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, ] +[[package]] +name = "python-json-logger" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/de/d3144a0bceede957f961e975f3752760fbe390d57fbe194baf709d8f1f7b/python_json_logger-3.3.0.tar.gz", hash = "sha256:12b7e74b17775e7d565129296105bbe3910842d9d0eb083fc83a6a617aa8df84", size = 16642 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/20/0f2523b9e50a8052bc6a8b732dfc8568abbdc42010aef03a2d750bdab3b2/python_json_logger-3.3.0-py3-none-any.whl", hash = "sha256:dd980fae8cffb24c13caf6e158d3d61c0d6d22342f932cb6e9deedab3d35eec7", size = 15163 }, +] + [[package]] name = "python-multipart" version = "0.0.20" @@ -835,6 +1281,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, ] +[[package]] +name = "redis" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, + { name = "pyjwt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/cf/128b1b6d7086200c9f387bd4be9b2572a30b90745ef078bd8b235042dc9f/redis-5.3.1.tar.gz", hash = "sha256:ca49577a531ea64039b5a36db3d6cd1a0c7a60c34124d46924a45b956e8cf14c", size = 4626200 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/26/5c5fa0e83c3621db835cfc1f1d789b37e7fa99ed54423b5f519beb931aa7/redis-5.3.1-py3-none-any.whl", hash = "sha256:dc1909bd24669cc31b5f67a039700b16ec30571096c5f1f0d9d2324bff31af97", size = 272833 }, +] + [[package]] name = "rich" version = "14.0.0" @@ -868,10 +1327,13 @@ version = "0.0.1" source = { virtual = "." } dependencies = [ { name = "aiosqlite" }, + { name = "cashu" }, { name = "fastapi", extra = ["standard"] }, { name = "greenlet" }, { name = "httpx", extra = ["socks"] }, - { name = "sixty-nuts" }, + { name = "marshmallow" }, + { name = "python-json-logger" }, + { name = "secp256k1" }, { name = "sqlmodel" }, ] @@ -889,10 +1351,13 @@ dev = [ [package.metadata] requires-dist = [ { name = "aiosqlite", specifier = ">=0.20" }, + { name = "cashu" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115" }, { name = "greenlet", specifier = ">=3.2.1" }, { name = "httpx", extras = ["socks"], specifier = ">=0.25.2" }, - { name = "sixty-nuts", specifier = ">=0.1.3" }, + { name = "marshmallow", specifier = ">=3.13,<4.0" }, + { name = "python-json-logger", specifier = ">=2.0.0" }, + { name = "secp256k1", git = "https://github.com/saschanaz/secp256k1-py?branch=upgrade060" }, { name = "sqlmodel", specifier = ">=0.0.24" }, ] @@ -932,6 +1397,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cd/be/f6b790d6ae98f1f32c645f8540d5c96248b72343b0a56fab3a07f2941897/ruff-0.11.8-py3-none-win_arm64.whl", hash = "sha256:304432e4c4a792e3da85b7699feb3426a0908ab98bf29df22a31b0cdd098fac2", size = 10713129 }, ] +[[package]] +name = "secp256k1" +version = "0.14.0" +source = { git = "https://github.com/saschanaz/secp256k1-py?branch=upgrade060#7d70a8ec7ca2db050d292c3759e49e75e21ac533" } +dependencies = [ + { name = "cffi" }, +] + +[[package]] +name = "setuptools" +version = "75.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/df/ec5ad16b0ec305081c372bd0550fd638fa96e472cd5a03049c344076ea76/setuptools-75.9.1.tar.gz", hash = "sha256:b6eca2c3070cdc82f71b4cb4bb2946bc0760a210d11362278cf1ff394e6ea32c", size = 1345088 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/28/19ad82a0549d73ec6feffa6711eacf9246035a9426b8a8b528440c9959d2/setuptools-75.9.1-py3-none-any.whl", hash = "sha256:0a6f876d62f4d978ca1a11ab4daf728d1357731f978543ff18ecdbf9fd071f73", size = 1231632 }, +] + [[package]] name = "shellingham" version = "1.5.4" @@ -942,21 +1424,24 @@ wheels = [ ] [[package]] -name = "sixty-nuts" -version = "0.1.3" +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "slowapi" +version = "0.1.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "bech32" }, - { name = "cbor2" }, - { name = "coincurve" }, - { name = "cryptography" }, - { name = "httpx" }, - { name = "typer" }, - { name = "websockets" }, + { name = "limits" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/a5/04c6b7c839fa641300911c4d10d59ac4a4c28ae23ec34b67e23fb9b4c212/sixty_nuts-0.1.3.tar.gz", hash = "sha256:b3bf69b916ba6aca03f0452d51829895f274a58f19f45c83cd32e9d8cc36dd53", size = 165092 } +sdist = { url = "https://files.pythonhosted.org/packages/a0/99/adfc7f94ca024736f061257d39118e1542bade7a52e86415a4c4ae92d8ff/slowapi-0.1.9.tar.gz", hash = "sha256:639192d0f1ca01b1c6d95bf6c71d794c3a9ee189855337b4821f7f457dddad77", size = 14028 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/7e/4a9be2b8252dddc96783b5a53c4a973012e2c19c375059c0a541ef58da54/sixty_nuts-0.1.3-py3-none-any.whl", hash = "sha256:47373e4533fcb79631d15d09b4225d4a56ace275c8d3474fabe953dcbc693f40", size = 89514 }, + { url = "https://files.pythonhosted.org/packages/2b/bb/f71c4b7d7e7eb3fc1e8c0458a8979b912f40b58002b9fbf37729b8cb464b/slowapi-0.1.9-py3-none-any.whl", hash = "sha256:cfad116cfb84ad9d763ee155c1e5c5cbf00b0d47399a769b227865f5df576e36", size = 14670 }, ] [[package]] @@ -1014,6 +1499,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/7c/5fc8e802e7506fe8b55a03a2e1dab156eae205c91bee46305755e086d2e2/sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a", size = 1903894 }, ] +[package.optional-dependencies] +asyncio = [ + { name = "greenlet" }, +] + [[package]] name = "sqlmodel" version = "0.0.24" @@ -1105,6 +1595,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317 }, ] +[[package]] +name = "types-protobuf" +version = "5.29.1.20250403" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/6d/62a2e73b966c77609560800004dd49a926920dd4976a9fdd86cf998e7048/types_protobuf-5.29.1.20250403.tar.gz", hash = "sha256:7ff44f15022119c9d7558ce16e78b2d485bf7040b4fadced4dd069bb5faf77a2", size = 59413 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e3/b74dcc2797b21b39d5a4f08a8b08e20369b4ca250d718df7af41a60dd9f0/types_protobuf-5.29.1.20250403-py3-none-any.whl", hash = "sha256:c71de04106a2d54e5b2173d0a422058fae0ef2d058d70cf369fb797bf61ffa59", size = 73874 }, +] + [[package]] name = "typing-extensions" version = "4.13.2" @@ -1214,43 +1713,180 @@ wheels = [ ] [[package]] -name = "websockets" -version = "15.0.1" +name = "websocket-client" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423 }, - { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082 }, - { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330 }, - { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878 }, - { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883 }, - { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252 }, - { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521 }, - { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958 }, - { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918 }, - { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388 }, - { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828 }, - { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 }, - { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 }, - { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 }, - { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 }, - { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 }, - { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 }, - { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 }, - { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 }, - { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 }, - { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 }, - { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 }, - { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 }, - { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 }, - { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 }, - { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 }, - { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 }, - { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 }, - { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 }, - { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 }, - { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 }, - { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 }, - { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826 }, +] + +[[package]] +name = "websockets" +version = "12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/62/7a7874b7285413c954a4cca3c11fd851f11b2fe5b4ae2d9bee4f6d9bdb10/websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b", size = 104994 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/73/9c1e168a2e7fdf26841dc98f5f5502e91dea47428da7690a08101f616169/websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4", size = 124047 }, + { url = "https://files.pythonhosted.org/packages/e4/2d/9a683359ad2ed11b2303a7a94800db19c61d33fa3bde271df09e99936022/websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f", size = 121282 }, + { url = "https://files.pythonhosted.org/packages/95/aa/75fa3b893142d6d98a48cb461169bd268141f2da8bfca97392d6462a02eb/websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3", size = 121325 }, + { url = "https://files.pythonhosted.org/packages/6e/a4/51a25e591d645df71ee0dc3a2c880b28e5514c00ce752f98a40a87abcd1e/websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c", size = 131502 }, + { url = "https://files.pythonhosted.org/packages/cd/ea/0ceeea4f5b87398fe2d9f5bcecfa00a1bcd542e2bfcac2f2e5dd612c4e9e/websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45", size = 130491 }, + { url = "https://files.pythonhosted.org/packages/e3/05/f52a60b66d9faf07a4f7d71dc056bffafe36a7e98c4eb5b78f04fe6e4e85/websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04", size = 130872 }, + { url = "https://files.pythonhosted.org/packages/ac/4e/c7361b2d7b964c40fea924d64881145164961fcd6c90b88b7e3ab2c4f431/websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447", size = 136318 }, + { url = "https://files.pythonhosted.org/packages/0a/31/337bf35ae5faeaf364c9cddec66681cdf51dc4414ee7a20f92a18e57880f/websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca", size = 135594 }, + { url = "https://files.pythonhosted.org/packages/95/aa/1ac767825c96f9d7e43c4c95683757d4ef28cf11fa47a69aca42428d3e3a/websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53", size = 136191 }, + { url = "https://files.pythonhosted.org/packages/28/4b/344ec5cfeb6bc417da097f8253607c3aed11d9a305fb58346f506bf556d8/websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402", size = 124453 }, + { url = "https://files.pythonhosted.org/packages/d1/40/6b169cd1957476374f51f4486a3e85003149e62a14e6b78a958c2222337a/websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b", size = 124971 }, + { url = "https://files.pythonhosted.org/packages/a9/6d/23cc898647c8a614a0d9ca703695dd04322fb5135096a20c2684b7c852b6/websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df", size = 124061 }, + { url = "https://files.pythonhosted.org/packages/39/34/364f30fdf1a375e4002a26ee3061138d1571dfda6421126127d379d13930/websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc", size = 121296 }, + { url = "https://files.pythonhosted.org/packages/2e/00/96ae1c9dcb3bc316ef683f2febd8c97dde9f254dc36c3afc65c7645f734c/websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b", size = 121326 }, + { url = "https://files.pythonhosted.org/packages/af/f1/bba1e64430685dd456c1a1fd6b0c791ae33104967b928aefeff261761e8d/websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb", size = 131807 }, + { url = "https://files.pythonhosted.org/packages/62/3b/98ee269712f37d892b93852ce07b3e6d7653160ca4c0d4f8c8663f8021f8/websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92", size = 130751 }, + { url = "https://files.pythonhosted.org/packages/f1/00/d6f01ca2b191f8b0808e4132ccd2e7691f0453cbd7d0f72330eb97453c3a/websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed", size = 131176 }, + { url = "https://files.pythonhosted.org/packages/af/9c/703ff3cd8109dcdee6152bae055d852ebaa7750117760ded697ab836cbcf/websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5", size = 136246 }, + { url = "https://files.pythonhosted.org/packages/0b/a5/1a38fb85a456b9dc874ec984f3ff34f6550eafd17a3da28753cd3c1628e8/websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2", size = 135466 }, + { url = "https://files.pythonhosted.org/packages/3c/98/1261f289dff7e65a38d59d2f591de6ed0a2580b729aebddec033c4d10881/websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113", size = 136083 }, + { url = "https://files.pythonhosted.org/packages/a9/1c/f68769fba63ccb9c13fe0a25b616bd5aebeef1c7ddebc2ccc32462fb784d/websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d", size = 124460 }, + { url = "https://files.pythonhosted.org/packages/20/52/8915f51f9aaef4e4361c89dd6cf69f72a0159f14e0d25026c81b6ad22525/websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f", size = 124985 }, + { url = "https://files.pythonhosted.org/packages/79/4d/9cc401e7b07e80532ebc8c8e993f42541534da9e9249c59ee0139dcb0352/websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e", size = 118370 }, +] + +[[package]] +name = "wheel" +version = "0.41.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/d0/0b4c18a0b85c20233b0c3bc33f792aefd7f12a5832b4da77419949ff6fd9/wheel-0.41.3.tar.gz", hash = "sha256:4d4987ce51a49370ea65c0bfd2234e8ce80a12780820d9dc462597a6e60d0841", size = 98880 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/7f/4c07234086edbce4a0a446209dc0cb08a19bb206a3ea53b2f56a403f983b/wheel-0.41.3-py3-none-any.whl", hash = "sha256:488609bc63a29322326e05560731bf7bfea8e48ad646e1f5e40d366607de0942", size = 65801 }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 }, +] + +[[package]] +name = "wrapt" +version = "1.17.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308 }, + { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488 }, + { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776 }, + { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776 }, + { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420 }, + { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199 }, + { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307 }, + { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025 }, + { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879 }, + { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419 }, + { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773 }, + { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799 }, + { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821 }, + { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919 }, + { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721 }, + { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899 }, + { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222 }, + { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707 }, + { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685 }, + { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567 }, + { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672 }, + { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865 }, + { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800 }, + { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824 }, + { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920 }, + { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690 }, + { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861 }, + { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174 }, + { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721 }, + { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763 }, + { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585 }, + { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676 }, + { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871 }, + { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312 }, + { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062 }, + { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155 }, + { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471 }, + { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208 }, + { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339 }, + { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232 }, + { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476 }, + { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377 }, + { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986 }, + { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 }, + { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276 }, +] + +[[package]] +name = "zstandard" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation == 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/40/f67e7d2c25a0e2dc1744dd781110b0b60306657f8696cafb7ad7579469bd/zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e", size = 788699 }, + { url = "https://files.pythonhosted.org/packages/e8/46/66d5b55f4d737dd6ab75851b224abf0afe5774976fe511a54d2eb9063a41/zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23", size = 633681 }, + { url = "https://files.pythonhosted.org/packages/63/b6/677e65c095d8e12b66b8f862b069bcf1f1d781b9c9c6f12eb55000d57583/zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a", size = 4944328 }, + { url = "https://files.pythonhosted.org/packages/59/cc/e76acb4c42afa05a9d20827116d1f9287e9c32b7ad58cc3af0721ce2b481/zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db", size = 5311955 }, + { url = "https://files.pythonhosted.org/packages/78/e4/644b8075f18fc7f632130c32e8f36f6dc1b93065bf2dd87f03223b187f26/zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2", size = 5344944 }, + { url = "https://files.pythonhosted.org/packages/76/3f/dbafccf19cfeca25bbabf6f2dd81796b7218f768ec400f043edc767015a6/zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca", size = 5442927 }, + { url = "https://files.pythonhosted.org/packages/0c/c3/d24a01a19b6733b9f218e94d1a87c477d523237e07f94899e1c10f6fd06c/zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c", size = 4864910 }, + { url = "https://files.pythonhosted.org/packages/1c/a9/cf8f78ead4597264f7618d0875be01f9bc23c9d1d11afb6d225b867cb423/zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e", size = 4935544 }, + { url = "https://files.pythonhosted.org/packages/2c/96/8af1e3731b67965fb995a940c04a2c20997a7b3b14826b9d1301cf160879/zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5", size = 5467094 }, + { url = "https://files.pythonhosted.org/packages/ff/57/43ea9df642c636cb79f88a13ab07d92d88d3bfe3e550b55a25a07a26d878/zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48", size = 4860440 }, + { url = "https://files.pythonhosted.org/packages/46/37/edb78f33c7f44f806525f27baa300341918fd4c4af9472fbc2c3094be2e8/zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c", size = 4700091 }, + { url = "https://files.pythonhosted.org/packages/c1/f1/454ac3962671a754f3cb49242472df5c2cced4eb959ae203a377b45b1a3c/zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003", size = 5208682 }, + { url = "https://files.pythonhosted.org/packages/85/b2/1734b0fff1634390b1b887202d557d2dd542de84a4c155c258cf75da4773/zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78", size = 5669707 }, + { url = "https://files.pythonhosted.org/packages/52/5a/87d6971f0997c4b9b09c495bf92189fb63de86a83cadc4977dc19735f652/zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473", size = 5201792 }, + { url = "https://files.pythonhosted.org/packages/79/02/6f6a42cc84459d399bd1a4e1adfc78d4dfe45e56d05b072008d10040e13b/zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160", size = 430586 }, + { url = "https://files.pythonhosted.org/packages/be/a2/4272175d47c623ff78196f3c10e9dc7045c1b9caf3735bf041e65271eca4/zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0", size = 495420 }, + { url = "https://files.pythonhosted.org/packages/7b/83/f23338c963bd9de687d47bf32efe9fd30164e722ba27fb59df33e6b1719b/zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094", size = 788713 }, + { url = "https://files.pythonhosted.org/packages/5b/b3/1a028f6750fd9227ee0b937a278a434ab7f7fdc3066c3173f64366fe2466/zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8", size = 633459 }, + { url = "https://files.pythonhosted.org/packages/26/af/36d89aae0c1f95a0a98e50711bc5d92c144939efc1f81a2fcd3e78d7f4c1/zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1", size = 4945707 }, + { url = "https://files.pythonhosted.org/packages/cd/2e/2051f5c772f4dfc0aae3741d5fc72c3dcfe3aaeb461cc231668a4db1ce14/zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072", size = 5306545 }, + { url = "https://files.pythonhosted.org/packages/0a/9e/a11c97b087f89cab030fa71206963090d2fecd8eb83e67bb8f3ffb84c024/zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20", size = 5337533 }, + { url = "https://files.pythonhosted.org/packages/fc/79/edeb217c57fe1bf16d890aa91a1c2c96b28c07b46afed54a5dcf310c3f6f/zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373", size = 5436510 }, + { url = "https://files.pythonhosted.org/packages/81/4f/c21383d97cb7a422ddf1ae824b53ce4b51063d0eeb2afa757eb40804a8ef/zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db", size = 4859973 }, + { url = "https://files.pythonhosted.org/packages/ab/15/08d22e87753304405ccac8be2493a495f529edd81d39a0870621462276ef/zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772", size = 4936968 }, + { url = "https://files.pythonhosted.org/packages/eb/fa/f3670a597949fe7dcf38119a39f7da49a8a84a6f0b1a2e46b2f71a0ab83f/zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105", size = 5467179 }, + { url = "https://files.pythonhosted.org/packages/4e/a9/dad2ab22020211e380adc477a1dbf9f109b1f8d94c614944843e20dc2a99/zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba", size = 4848577 }, + { url = "https://files.pythonhosted.org/packages/08/03/dd28b4484b0770f1e23478413e01bee476ae8227bbc81561f9c329e12564/zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd", size = 4693899 }, + { url = "https://files.pythonhosted.org/packages/2b/64/3da7497eb635d025841e958bcd66a86117ae320c3b14b0ae86e9e8627518/zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a", size = 5199964 }, + { url = "https://files.pythonhosted.org/packages/43/a4/d82decbab158a0e8a6ebb7fc98bc4d903266bce85b6e9aaedea1d288338c/zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90", size = 5655398 }, + { url = "https://files.pythonhosted.org/packages/f2/61/ac78a1263bc83a5cf29e7458b77a568eda5a8f81980691bbc6eb6a0d45cc/zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35", size = 5191313 }, + { url = "https://files.pythonhosted.org/packages/e7/54/967c478314e16af5baf849b6ee9d6ea724ae5b100eb506011f045d3d4e16/zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d", size = 430877 }, + { url = "https://files.pythonhosted.org/packages/75/37/872d74bd7739639c4553bf94c84af7d54d8211b626b352bc57f0fd8d1e3f/zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b", size = 495595 }, + { url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975 }, + { url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448 }, + { url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269 }, + { url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228 }, + { url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891 }, + { url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310 }, + { url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912 }, + { url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946 }, + { url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994 }, + { url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681 }, + { url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239 }, + { url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149 }, + { url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392 }, + { url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299 }, + { url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862 }, + { url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578 }, ]