Compare commits

...
Author SHA1 Message Date
alan c3c68bc9da chore: make bundle [skip ci] 2026-06-26 12:38:43 +00:00
Arc 95deb47765 make 2026-06-26 13:29:30 +01:00
Arc b25a5cfcf6 make 2026-06-26 13:25:59 +01:00
Arc 0e2397a813 remove tooltip 2026-06-26 13:24:22 +01:00
Arc f363418c05 tweak 2026-06-26 13:18:32 +01:00
Arc 0275b66a89 init 2026-06-26 13:04:35 +01:00
Arc c680fd647d Merge remote-tracking branch 'origin/dev' into dev 2026-06-26 12:59:58 +01:00
dni ⚡andGitHub cfc9874517 chore: update to version 1.5.5-rc3 (#4024) 2026-06-24 12:24:41 +02:00
62cd151fd6 feat: add i18n for DynamicComponent (#4018)
Co-authored-by: alan <alan@lnbits.com>
2026-06-23 12:37:54 +02:00
a4d0aa5db8 fix(phoenixd): use descriptionHash when unhashed_description is provided (#3913)
Co-authored-by: Richard Taylor <RT@MacBookPro.lan>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-23 12:16:54 +03:00
dni ⚡andGitHub 77630781d7 fix: failing make bundle ci on forks. (#4019) 2026-06-23 08:15:13 +03:00
Vlad StanandGitHub 59da350cc9 fix: fiat payment status update (#4017) 2026-06-22 16:13:02 +03:00
29e980dd67 fix: non-blocking relay publishes in send_nostr_dm (#3925)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-06-18 15:28:28 +02:00
9d9ce63c82 feat: add llms.txt endpoint for AI agents (#3877)
Co-authored-by: Storm Knight <storm-knight@openclaw.ai>
Co-authored-by: ThomsenDrake <drake@verqor.me>
Co-authored-by: Arc <33088785+arcbtc@users.noreply.github.com>
Co-authored-by: dni  <office@dnilabs.com>
2026-06-18 15:15:01 +02:00
748f458b8b feat: add extension profiles (#3914)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-06-18 15:27:33 +03:00
Ben WeeksandGitHub ce177a73b1 fix: exponential backoff for IN_FLIGHT payment polling (#3918) 2026-06-18 14:47:03 +03:00
2885e71be2 fix: populate unit selector when opening send/pay dialog (#3953)
Co-authored-by: dni  <office@dnilabs.com>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-06-18 11:50:27 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4b6f43d274 chore(deps): bump python-multipart from 0.0.29 to 0.0.31 (#4010)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 10:37:54 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
86b5cf9421 chore(deps): bump tornado from 6.5.6 to 6.5.7 (#4011)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 10:37:25 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8f5b7d85aa chore(deps): bump aiohttp from 3.13.5 to 3.14.1 (#4012)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 10:32:12 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4d51e63924 chore(deps): bump cryptography from 48.0.0 to 48.0.1 (#4013)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 10:30:50 +02:00
Nathan DayandGitHub 0ce2501e1a fix: run blocking SMTP calls in a thread to avoid blocking the event loop (#3988) 2026-06-17 17:59:21 +03:00
f2351145f0 fix: stop updating internal invoices inside the listener unlike external one (#3984)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-06-17 16:47:02 +03:00
2eb7d67b2a feat: use cached rate from exchange_rate_history_setting (#4001)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-06-17 15:16:38 +03:00
dni ⚡andGitHub a82093b7ec feat: electrum util for onchain data (#3999) 2026-06-17 13:16:52 +02:00
Vlad StanandGitHub ee595eede1 fix: revolut unsubscribe (#4009) 2026-06-16 11:48:32 +03:00
dfdce54e57 fix(lnd): enable MPP via max_parts=16 and time_pref in SendPaymentV2 (#3916)
Co-authored-by: dni  <office@dnilabs.com>
2026-06-08 15:33:48 +03:00
Arc 5708f0707d Merge remote-tracking branch 'origin/dev' into dev 2026-06-05 10:01:43 +01:00
Arc 7ba0bf5e70 Merge remote-tracking branch 'origin/dev' into dev 2026-06-03 20:49:03 +01:00
Arc aeeaed9609 revolut fix 2026-06-02 12:20:53 +01:00
41 changed files with 1604 additions and 247 deletions
+7 -3
View File
@@ -10,15 +10,16 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.head_ref || github.event.pull_request.head.sha }}
- uses: lnbits/lnbits/.github/actions/prepare@dev - uses: lnbits/lnbits/.github/actions/prepare@dev
with: with:
python-version: "3.10" python-version: "3.10"
node-version: "24.x" node-version: "24.x"
npm: true npm: true
- run: make bundle - name: Build and commit bundle (same-repo PR)
- name: Commit and push bundle changes if: github.event.pull_request.head.repo.full_name == github.repository
run: | run: |
make bundle
git config user.name "alan" git config user.name "alan"
git config user.email "alan@lnbits.com" git config user.email "alan@lnbits.com"
git add lnbits/static git add lnbits/static
@@ -27,3 +28,6 @@ jobs:
fi fi
git commit -m "chore: make bundle [skip ci]" git commit -m "chore: make bundle [skip ci]"
git push git push
- name: Check bundle is up-to-date (fork PR)
if: github.event.pull_request.head.repo.full_name != github.repository
run: make checkbundle
+1
View File
@@ -66,6 +66,7 @@ jobs:
BOLTZ_MNEMONIC: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about BOLTZ_MNEMONIC: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
LNBITS_MAX_OUTGOING_PAYMENT_AMOUNT_SATS: 1000000000 LNBITS_MAX_OUTGOING_PAYMENT_AMOUNT_SATS: 1000000000
LNBITS_MAX_INCOMING_PAYMENT_AMOUNT_SATS: 1000000000 LNBITS_MAX_INCOMING_PAYMENT_AMOUNT_SATS: 1000000000
LNBITS_FUNDING_SOURCE_PAY_INVOICE_WAIT_SECONDS: ${{ inputs.backend-wallet-class == 'CoreLightningRestWallet' && 60 || 5 }}
ECLAIR_PASS: lnbits ECLAIR_PASS: lnbits
PYTHONUNBUFFERED: 1 PYTHONUNBUFFERED: 1
DEBUG: true DEBUG: true
+4
View File
@@ -40,6 +40,7 @@ from lnbits.core.tasks import (
) )
from lnbits.exceptions import register_exception_handlers from lnbits.exceptions import register_exception_handlers
from lnbits.helpers import version_parse from lnbits.helpers import version_parse
from lnbits.llms_txt import create_llms_txt_route
from lnbits.settings import settings from lnbits.settings import settings
from lnbits.tasks import ( from lnbits.tasks import (
cancel_all_tasks, cancel_all_tasks,
@@ -102,6 +103,9 @@ async def startup(app: FastAPI):
# register core routes # register core routes
init_core_routers(app) init_core_routers(app)
# register llms.txt endpoint for AI agents
create_llms_txt_route(app)
# initialize tasks # initialize tasks
register_async_tasks() register_async_tasks()
+39
View File
@@ -16,6 +16,7 @@ from ..models import (
PaymentFilters, PaymentFilters,
PaymentHistoryPoint, PaymentHistoryPoint,
PaymentsStatusCount, PaymentsStatusCount,
PaymentTotalBreakdown,
PaymentWalletStats, PaymentWalletStats,
) )
@@ -428,6 +429,44 @@ async def get_payment_count_stats(
return data return data
async def get_wallet_payment_total_breakdown(
wallet_id: str,
conn: Connection | None = None,
) -> list[PaymentTotalBreakdown]:
wallet = await get_wallet(wallet_id, conn=conn)
if not wallet or not wallet.can_view_payments:
return []
values = {"wallet_id": wallet.source_wallet_id}
data = await (conn or db).fetchall(
query=f"""
SELECT tag,
CASE
WHEN fiat_provider IS NOT NULL
OR checking_id LIKE 'fiat_%'
OR extra LIKE '%"fiat_payment_request"%'
OR extra LIKE '%"fiat_amount"%'
THEN true
ELSE false
END AS is_fiat,
COUNT(*) AS payments_count,
SUM(amount - ABS(fee)) AS total
FROM apipayments
WHERE wallet_id = :wallet_id
AND (
status = '{PaymentState.SUCCESS}'
OR (amount < 0 AND status = '{PaymentState.PENDING}')
)
GROUP BY tag, is_fiat
ORDER BY tag
""", # noqa: S608
values=values,
model=PaymentTotalBreakdown,
)
return data
async def get_daily_stats( async def get_daily_stats(
filters: Filters[PaymentFilters] | None = None, filters: Filters[PaymentFilters] | None = None,
user_id: str | None = None, user_id: str | None = None,
+2
View File
@@ -23,6 +23,7 @@ from .payments import (
PaymentHistoryPoint, PaymentHistoryPoint,
PaymentsStatusCount, PaymentsStatusCount,
PaymentState, PaymentState,
PaymentTotalBreakdown,
PaymentWalletStats, PaymentWalletStats,
SettleInvoice, SettleInvoice,
UpdatePaymentExtra, UpdatePaymentExtra,
@@ -83,6 +84,7 @@ __all__ = [
"PaymentFilters", "PaymentFilters",
"PaymentHistoryPoint", "PaymentHistoryPoint",
"PaymentState", "PaymentState",
"PaymentTotalBreakdown",
"PaymentWalletStats", "PaymentWalletStats",
"PaymentsStatusCount", "PaymentsStatusCount",
"RegisterUser", "RegisterUser",
+13 -2
View File
@@ -55,9 +55,10 @@ class GitHubRelease(BaseModel):
class Manifest(BaseModel): class Manifest(BaseModel):
featured: list[str] = []
extensions: list[ExplicitRelease] = [] extensions: list[ExplicitRelease] = []
repos: list[GitHubRelease] = [] repos: list[GitHubRelease] = []
featured: list[str] = []
categories: dict[str, list[str]] = {}
class GitHubRepoRelease(BaseModel): class GitHubRepoRelease(BaseModel):
@@ -308,7 +309,6 @@ class ExtensionRelease(BaseModel):
@classmethod @classmethod
async def fetch_release_details(cls, details_link: str) -> dict | None: async def fetch_release_details(cls, details_link: str) -> dict | None:
try: try:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
resp = await client.get(details_link) resp = await client.get(details_link)
@@ -333,6 +333,7 @@ class ExtensionMeta(BaseModel):
dependencies: list[str] = [] dependencies: list[str] = []
archive: str | None = None archive: str | None = None
featured: bool = False featured: bool = False
categories: list[str] = []
paid_features: str | None = None paid_features: str | None = None
has_paid_release: bool = False has_paid_release: bool = False
has_free_release: bool = False has_free_release: bool = False
@@ -680,6 +681,11 @@ class InstallableExtension(BaseModel):
meta = ext.meta or ExtensionMeta() meta = ext.meta or ExtensionMeta()
meta.featured = ext.id in manifest.featured meta.featured = ext.id in manifest.featured
meta.categories = [
category
for category, ext_ids in manifest.categories.items()
if ext.id in ext_ids
]
ext.meta = meta ext.meta = meta
extension_list += [ext] extension_list += [ext]
@@ -695,6 +701,11 @@ class InstallableExtension(BaseModel):
ext.check_release_updates(release) ext.check_release_updates(release)
meta = ext.meta or ExtensionMeta() meta = ext.meta or ExtensionMeta()
meta.featured = ext.id in manifest.featured meta.featured = ext.id in manifest.featured
meta.categories = [
category
for category, ext_ids in manifest.categories.items()
if ext.id in ext_ids
]
ext.meta = meta ext.meta = meta
extension_list += [ext] extension_list += [ext]
except Exception as e: except Exception as e:
+11 -8
View File
@@ -140,22 +140,18 @@ class Payment(BaseModel):
) )
# DEPRECATED: in v1.5.0, use service check_payment_status instead # DEPRECATED: in v1.5.0, use service check_payment_status instead
async def check_status( async def check_status(self) -> PaymentStatus:
self, skip_internal_payment_notifications: bool | None = False
) -> PaymentStatus:
logger.warning("payment.check_status() is deprecated.") logger.warning("payment.check_status() is deprecated.")
from lnbits.core.services.payments import check_payment_status from lnbits.core.services.payments import check_payment_status
return await check_payment_status(self, skip_internal_payment_notifications) return await check_payment_status(self)
# DEPRECATED: in v1.5.0, use service check_payment_status instead # DEPRECATED: in v1.5.0, use service check_payment_status instead
async def check_fiat_status( async def check_fiat_status(self) -> FiatPaymentStatus:
self, skip_internal_payment_notifications: bool | None = False
) -> FiatPaymentStatus:
logger.warning("payment.check_fiat_status() is deprecated.") logger.warning("payment.check_fiat_status() is deprecated.")
from lnbits.core.services.fiat_providers import check_fiat_status from lnbits.core.services.fiat_providers import check_fiat_status
return await check_fiat_status(self, skip_internal_payment_notifications) return await check_fiat_status(self)
class PaymentFilters(FilterModel): class PaymentFilters(FilterModel):
@@ -224,6 +220,13 @@ class PaymentWalletStats(BaseModel):
balance: float = 0 balance: float = 0
class PaymentTotalBreakdown(BaseModel):
tag: str | None = None
is_fiat: bool = False
payments_count: int = 0
total: int = 0
class PaymentDailyStats(BaseModel): class PaymentDailyStats(BaseModel):
date: datetime date: datetime
balance: float = 0 balance: float = 0
+6 -7
View File
@@ -8,7 +8,7 @@ import httpx
from loguru import logger from loguru import logger
from lnbits.core.crud import get_wallet from lnbits.core.crud import get_wallet
from lnbits.core.crud.payments import create_payment from lnbits.core.crud.payments import create_payment, update_payment
from lnbits.core.models import CreatePayment, Payment, PaymentState from lnbits.core.models import CreatePayment, Payment, PaymentState
from lnbits.core.models.misc import SimpleStatus from lnbits.core.models.misc import SimpleStatus
from lnbits.db import Connection from lnbits.db import Connection
@@ -36,9 +36,7 @@ async def handle_fiat_payment_confirmation(
logger.warning(e) logger.warning(e)
async def check_fiat_status( async def check_fiat_status(payment: Payment) -> FiatPaymentStatus:
payment: Payment, skip_internal_payment_notifications: bool | None = False
) -> FiatPaymentStatus:
if not payment.is_internal: if not payment.is_internal:
return FiatPaymentPendingStatus() return FiatPaymentPendingStatus()
if payment.success: if payment.success:
@@ -58,10 +56,11 @@ async def check_fiat_status(
return FiatPaymentPendingStatus() return FiatPaymentPendingStatus()
fiat_status = await fiat_provider.get_invoice_status(checking_id) fiat_status = await fiat_provider.get_invoice_status(checking_id)
if skip_internal_payment_notifications:
return fiat_status
if fiat_status.success: if fiat_status.success:
payment.status = PaymentState.SUCCESS.value
await update_payment(payment)
await handle_fiat_payment_confirmation(payment)
# notify receivers asynchronously # notify receivers asynchronously
from lnbits.tasks import internal_invoice_queue from lnbits.tasks import internal_invoice_queue
+35 -6
View File
@@ -74,7 +74,7 @@ async def send_admin_notification(
message: str, message: str,
message_type: str | None = None, message_type: str | None = None,
) -> None: ) -> None:
return await send_notification( return await send_notification_in_background(
settings.lnbits_telegram_notifications_chat_id, settings.lnbits_telegram_notifications_chat_id,
settings.lnbits_nostr_notifications_identifiers, settings.lnbits_nostr_notifications_identifiers,
settings.lnbits_email_notifications_to_emails, settings.lnbits_email_notifications_to_emails,
@@ -97,7 +97,7 @@ async def send_user_notification(
if user_notifications.nostr_identifier if user_notifications.nostr_identifier
else [] else []
) )
return await send_notification( return await send_notification_in_background(
user_notifications.telegram_chat_id, user_notifications.telegram_chat_id,
nostr_identifiers, nostr_identifiers,
email_address, email_address,
@@ -222,12 +222,20 @@ async def send_email(
msg["Subject"] = subject msg["Subject"] = subject
msg.attach(MIMEText(message, "plain")) msg.attach(MIMEText(message, "plain"))
username = username if len(username) > 0 else from_email username = username if len(username) > 0 else from_email
with smtplib.SMTP(server, port) as smtp_server:
smtp_server.starttls() def _send() -> bool:
smtp_server.login(username, password) with smtplib.SMTP(server, port) as smtp_server:
smtp_server.sendmail(from_email, to_emails, msg.as_string()) smtp_server.starttls()
smtp_server.login(username, password)
smtp_server.sendmail(from_email, to_emails, msg.as_string())
return True return True
try:
return await asyncio.to_thread(_send)
except Exception as e:
logger.warning(f"Sending Email failed. {e!s}")
return False
async def dispatch_webhook(payment: Payment): async def dispatch_webhook(payment: Payment):
""" """
@@ -294,6 +302,27 @@ def send_payment_notification_in_background(wallet: Wallet, payment: Payment):
logger.warning(f"Error sending payment notification: {e}") logger.warning(f"Error sending payment notification: {e}")
async def send_notification_in_background(
telegram_chat_id: str | None,
nostr_identifiers: list[str] | None,
email_addresses: list[str] | None,
message: str,
message_type: str | None = None,
):
try:
create_task(
send_notification(
telegram_chat_id,
nostr_identifiers,
email_addresses,
message,
message_type,
)
)
except Exception as e:
logger.warning(f"Error sending notification in background: {e}")
async def send_ws_payment_notification(wallet: Wallet, payment: Payment): async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
# TODO: websocket message should be a clean payment model # TODO: websocket message should be a clean payment model
# await websocket_manager.send(wallet.inkey, payment.json()) # await websocket_manager.send(wallet.inkey, payment.json())
+22 -22
View File
@@ -13,7 +13,6 @@ from lnbits.core.crud.payments import get_daily_stats
from lnbits.core.db import db from lnbits.core.db import db
from lnbits.core.models import PaymentDailyStats, PaymentFilters from lnbits.core.models import PaymentDailyStats, PaymentFilters
from lnbits.core.models.payments import CreateInvoice from lnbits.core.models.payments import CreateInvoice
from lnbits.core.services.fiat_providers import handle_fiat_payment_confirmation
from lnbits.db import Connection, Filters from lnbits.db import Connection, Filters
from lnbits.decorators import check_user_extension_access from lnbits.decorators import check_user_extension_access
from lnbits.exceptions import InvoiceError, PaymentError, UnsupportedError from lnbits.exceptions import InvoiceError, PaymentError, UnsupportedError
@@ -630,18 +629,14 @@ async def check_transaction_status(
return await check_payment_status(payment) return await check_payment_status(payment)
async def check_payment_status( async def check_payment_status(payment: Payment) -> PaymentStatus:
payment: Payment, skip_internal_payment_notifications: bool | None = False
) -> PaymentStatus:
if payment.is_internal: if payment.is_internal:
if payment.success: if payment.success:
return PaymentSuccessStatus() return PaymentSuccessStatus()
if payment.failed: if payment.failed:
return PaymentFailedStatus() return PaymentFailedStatus()
if payment.is_in and payment.fiat_provider: if payment.is_in and payment.fiat_provider:
fiat_status = await check_fiat_status( fiat_status = await check_fiat_status(payment)
payment, skip_internal_payment_notifications
)
return PaymentStatus(paid=fiat_status.paid) return PaymentStatus(paid=fiat_status.paid)
return PaymentPendingStatus() return PaymentPendingStatus()
funding_source = get_funding_source() funding_source = get_funding_source()
@@ -783,9 +778,14 @@ async def _pay_internal_invoice(
await update_payment(internal_payment, conn=conn) await update_payment(internal_payment, conn=conn)
logger.success(f"internal payment successful {internal_payment.checking_id}") logger.success(f"internal payment successful {internal_payment.checking_id}")
await _send_payment_notification_in_background(wallet.id, payment, conn=conn) await _send_payment_notification_in_background(
wallet.id, payment, conn=conn
) # notify the sender
await _send_payment_notification_in_background(
internal_payment.wallet_id, internal_payment, conn=conn
) # notify the receiver
# notify receiver asynchronously # notify receiver asynchronously (extension listeners)
from lnbits.tasks import internal_invoice_queue from lnbits.tasks import internal_invoice_queue
logger.debug(f"enqueuing internal invoice {internal_payment.checking_id}") logger.debug(f"enqueuing internal invoice {internal_payment.checking_id}")
@@ -1079,29 +1079,29 @@ async def _send_payment_notification_in_background(
send_payment_notification_in_background(wallet, payment) send_payment_notification_in_background(wallet, payment)
async def update_invoice_callback(checking_id: str) -> Payment | None: async def update_invoice_from_paid_invoices_stream(checking_id: str) -> Payment | None:
""" """
Takes a checking_id of an incoming payment, from either paid_invoices_stream() Takes a checking_id of an incoming payment from paid_invoices_stream()
or internal_invoice_queue. Checks its status, updates and returns it. Checks its status, updates its status and returns it.
returns None if no payment was found or it not and incoming payment. returns None if no incoming payment was found or the status is not successful
""" """
payment = await get_standalone_payment(checking_id, incoming=True) payment = await get_standalone_payment(checking_id, incoming=True)
if not payment: if not payment:
logger.warning(f"No payment found for '{checking_id}'.") logger.warning(f"No incoming payment found for '{checking_id}'.")
return None return None
if not payment.is_in:
logger.warning(f"Payment '{checking_id}' is not incoming, skipping.") status = await check_payment_status(payment)
if not status.success:
logger.error(
"Unexpected status response from paid_invoices_stream. Skipping update."
)
return None return None
status = await check_payment_status(
payment, skip_internal_payment_notifications=True
)
payment.fee = status.fee_msat or payment.fee payment.fee = status.fee_msat or payment.fee
# only overwrite preimage if status.preimage provides it # only overwrite preimage if status.preimage provides it
payment.preimage = status.preimage or payment.preimage payment.preimage = status.preimage or payment.preimage
payment.status = PaymentState.SUCCESS payment.status = PaymentState.SUCCESS
payment = await update_payment(payment) payment = await update_payment(payment)
if payment.fiat_provider:
await handle_fiat_payment_confirmation(payment)
return payment return payment
+6
View File
@@ -25,6 +25,7 @@ from lnbits.core.services.notifications import (
) )
from lnbits.db import Filters from lnbits.db import Filters
from lnbits.settings import settings from lnbits.settings import settings
from lnbits.utils.cache import cache
from lnbits.utils.exchange_rates import btc_rates from lnbits.utils.exchange_rates import btc_rates
audit_queue: asyncio.Queue[AuditEntry] = asyncio.Queue() audit_queue: asyncio.Queue[AuditEntry] = asyncio.Queue()
@@ -155,6 +156,11 @@ async def collect_exchange_rates_data() -> None:
rates_values = [r[1] for r in rates] rates_values = [r[1] for r in rates]
lnbits_rate = sum(rates_values) / len(rates_values) lnbits_rate = sum(rates_values) / len(rates_values)
rates.append(("LNbits", lnbits_rate)) rates.append(("LNbits", lnbits_rate))
cache.set(
f"btc-price-{currency}",
lnbits_rate,
expiry=settings.lnbits_exchange_rate_cache_seconds,
)
settings.append_exchange_rate_datapoint(dict(rates), max_history_size) settings.append_exchange_rate_datapoint(dict(rates), max_history_size)
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
+1 -1
View File
@@ -439,7 +439,7 @@ async def _handle_revolut_subscription(
extra = { extra = {
**(reference.extra or {}), **(reference.extra or {}),
"subscription_request_id": reference.subscription_request_id, "subscription_request_id": subscription_id,
"fiat_method": "subscription", "fiat_method": "subscription",
"tag": reference.tag, "tag": reference.tag,
"subscription": { "subscription": {
+1 -1
View File
@@ -292,7 +292,6 @@ async def api_deactivate_extension(ext_id: str) -> SimpleStatus:
@extension_router.delete("/{ext_id}", dependencies=[Depends(check_admin)]) @extension_router.delete("/{ext_id}", dependencies=[Depends(check_admin)])
async def api_uninstall_extension(ext_id: str) -> SimpleStatus: async def api_uninstall_extension(ext_id: str) -> SimpleStatus:
extension = await get_installed_extension(ext_id) extension = await get_installed_extension(ext_id)
if not extension: if not extension:
raise HTTPException( raise HTTPException(
@@ -567,6 +566,7 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
"shortDescription": ext.short_description, "shortDescription": ext.short_description,
"stars": ext.stars, "stars": ext.stars,
"isFeatured": ext.meta.featured if ext.meta else False, "isFeatured": ext.meta.featured if ext.meta else False,
"categories": ext.meta.categories if ext.meta else [],
"dependencies": ext.meta.dependencies if ext.meta else "", "dependencies": ext.meta.dependencies if ext.meta else "",
"isInstalled": ext.id in installed_exts_ids, "isInstalled": ext.id in installed_exts_ids,
"hasDatabaseTables": next( "hasDatabaseTables": next(
+13
View File
@@ -14,6 +14,7 @@ from lnurl import url_decode
from lnbits import bolt11 from lnbits import bolt11
from lnbits.core.crud.payments import ( from lnbits.core.crud.payments import (
get_payment_count_stats, get_payment_count_stats,
get_wallet_payment_total_breakdown,
get_wallets_stats, get_wallets_stats,
update_payment, update_payment,
) )
@@ -31,6 +32,7 @@ from lnbits.core.models import (
PaymentDailyStats, PaymentDailyStats,
PaymentFilters, PaymentFilters,
PaymentHistoryPoint, PaymentHistoryPoint,
PaymentTotalBreakdown,
PaymentWalletStats, PaymentWalletStats,
SettleInvoice, SettleInvoice,
SimpleStatus, SimpleStatus,
@@ -131,6 +133,17 @@ async def api_payments_counting_stats(
return await get_payment_count_stats(count_by, filters=filters, user_id=for_user_id) return await get_payment_count_stats(count_by, filters=filters, user_id=for_user_id)
@payment_router.get(
"/stats/breakdown",
name="Get wallet payment total breakdown",
response_model=list[PaymentTotalBreakdown],
)
async def api_payments_total_breakdown(
key_info: BaseWalletTypeInfo = Depends(require_base_invoice_key),
):
return await get_wallet_payment_total_breakdown(key_info.wallet.id)
@payment_router.get( @payment_router.get(
"/stats/wallets", "/stats/wallets",
name="Get payments history for all users", name="Get payments history for all users",
+10 -1
View File
@@ -283,7 +283,7 @@ class RevolutWallet(FiatProvider):
return FiatSubscriptionResponse( return FiatSubscriptionResponse(
ok=True, ok=True,
checkout_session_url=checkout_url, checkout_session_url=checkout_url,
subscription_request_id=payment_options.subscription_request_id, subscription_request_id=revolut_subscription_id,
) )
except json.JSONDecodeError: except json.JSONDecodeError:
return FiatSubscriptionResponse( return FiatSubscriptionResponse(
@@ -302,6 +302,15 @@ class RevolutWallet(FiatProvider):
**kwargs, **kwargs,
) -> FiatSubscriptionResponse: ) -> FiatSubscriptionResponse:
try: try:
subscription = await self.get_subscription(subscription_id)
reference = self.deserialize_subscription_reference(
subscription.get("external_reference")
)
if not reference or reference.wallet_id != correlation_id:
return FiatSubscriptionResponse(
ok=False, error_message="Subscription not found."
)
r = await self.client.post( r = await self.client.post(
f"/api/subscriptions/{subscription_id}/cancel", f"/api/subscriptions/{subscription_id}/cancel",
timeout=REVOLUT_REQUEST_TIMEOUT, timeout=REVOLUT_REQUEST_TIMEOUT,
+78
View File
@@ -0,0 +1,78 @@
"""Generate llms.txt markdown from FastAPI OpenAPI schema for AI agents."""
from typing import Any
from fastapi import FastAPI
from fastapi.responses import PlainTextResponse
def generate_llms_txt(app: FastAPI) -> str:
"""Convert an OpenAPI schema to llms.txt markdown format."""
openapi_schema = app.openapi()
lines: list[str] = []
# H1: API Title
info = openapi_schema.get("info", {})
title = info.get("title", "API")
lines.append(f"# {title}")
lines.append("")
# Blockquote: Description
description = info.get("description")
if description:
for line in description.strip().split("\n"):
lines.append(f"> {line}")
lines.append("")
# Group endpoints by tag
paths = openapi_schema.get("paths", {})
endpoints_by_tag: dict[str, list[dict[str, Any]]] = {}
for path, path_item in paths.items():
for method in ["get", "post", "put", "patch", "delete", "head", "options"]:
if method not in path_item:
continue
operation = path_item[method]
tags = operation.get("tags", ["Endpoints"])
tag = tags[0] if tags else "Endpoints"
if tag not in endpoints_by_tag:
endpoints_by_tag[tag] = []
endpoints_by_tag[tag].append(
{
"path": path,
"method": method.upper(),
"operation": operation,
}
)
# Generate sections by tag
for tag, endpoints in endpoints_by_tag.items():
lines.append(f"## {tag}")
lines.append("")
for endpoint in endpoints:
method = endpoint["method"]
path = endpoint["path"]
operation = endpoint["operation"]
summary = operation.get("summary", "")
if summary:
lines.append(f"### `{method} {path}` - {summary}")
else:
lines.append(f"### `{method} {path}`")
lines.append("")
lines.append("")
return "\n".join(lines).strip() + "\n"
def create_llms_txt_route(app: FastAPI) -> None:
"""Add a /llms.txt endpoint to the app."""
@app.get(
"/llms.txt",
response_class=PlainTextResponse,
include_in_schema=False,
summary="Get LLM-friendly API documentation",
)
async def get_llms_txt() -> str:
"""Return the API documentation in llms.txt markdown format."""
return generate_llms_txt(app)
+1 -1
View File
@@ -359,7 +359,7 @@ class FeeSettings(LNbitsSettings):
class ExchangeProvidersSettings(LNbitsSettings): class ExchangeProvidersSettings(LNbitsSettings):
lnbits_exchange_rate_cache_seconds: int = Field(default=30, ge=0) lnbits_exchange_rate_cache_seconds: int = Field(default=60, ge=0)
lnbits_exchange_history_size: int = Field(default=60, ge=0) lnbits_exchange_history_size: int = Field(default=60, ge=0)
lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300, ge=0) lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300, ge=0)
File diff suppressed because one or more lines are too long
+10 -10
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -184,6 +184,7 @@ window.localisation.en = {
release_notes: 'Release Notes', release_notes: 'Release Notes',
activate_extension_details: 'Make extension available/unavailable for users', activate_extension_details: 'Make extension available/unavailable for users',
featured: 'Featured', featured: 'Featured',
categories: 'Categories',
all: 'All', all: 'All',
only_admins_can_install: '(Only admin accounts can install extensions)', only_admins_can_install: '(Only admin accounts can install extensions)',
only_admins_can_create_extensions: only_admins_can_create_extensions:
+3
View File
@@ -175,6 +175,9 @@ window._lnbitsApi = {
wallet.inkey wallet.inkey
) )
}, },
getPaymentTotalBreakdown(wallet) {
return this.request('get', '/api/v1/payments/stats/breakdown', wallet.inkey)
},
getPayment(wallet, paymentHash) { getPayment(wallet, paymentHash) {
return this.request('get', '/api/v1/payments/' + paymentHash, wallet.inkey) return this.request('get', '/api/v1/payments/' + paymentHash, wallet.inkey)
}, },
+32
View File
@@ -27,6 +27,14 @@ const DynamicComponent = {
name: r.name, name: r.name,
component: async () => { component: async () => {
await LNbits.utils.loadTemplate(r.template) await LNbits.utils.loadTemplate(r.template)
if (r.i18n) {
const locale =
window.i18n?.global?.locale?.value ??
window.i18n?.global?.locale ??
window.g.locale ??
'en'
await LNbits.utils.loadExtI18n(r.i18n, locale)
}
await LNbits.utils.loadScript(r.component) await LNbits.utils.loadScript(r.component)
return window[r.name] return window[r.name]
} }
@@ -151,6 +159,30 @@ window.i18n = new VueI18n.createI18n({
fallbackLocale: 'en', fallbackLocale: 'en',
messages: window.localisation messages: window.localisation
}) })
;(function () {
let _applying = false
let _target = null
Vue.watch(
() => window.i18n.global.locale,
async (locale, prevLocale) => {
if (_applying || !LNbits.utils._extI18nDirs.size) return
_target = locale
_applying = true
window.i18n.global.locale = prevLocale
_applying = false
await Promise.all(
[...LNbits.utils._extI18nDirs].map(dir =>
LNbits.utils.loadExtI18n(dir, locale)
)
)
if (_target !== locale) return
_applying = true
window.i18n.global.locale = locale
_applying = false
},
{flush: 'sync'}
)
})()
window.app.mixin({ window.app.mixin({
data() { data() {
+9
View File
@@ -10,6 +10,7 @@ window.PageExtensions = {
tab: 'installed', tab: 'installed',
manageExtensionTab: 'releases', manageExtensionTab: 'releases',
filteredExtensions: [], filteredExtensions: [],
categories: new Set(),
updatableExtensions: [], updatableExtensions: [],
showUninstallDialog: false, showUninstallDialog: false,
showManageExtensionDialog: false, showManageExtensionDialog: false,
@@ -106,6 +107,10 @@ window.PageExtensions = {
} }
} }
const isCategoryTab = !['installed', 'all', 'featured'].includes(tab)
const isInSelectedCategory = extension =>
extension.categories?.includes(tab) ?? false
this.filteredExtensions = this.extensions this.filteredExtensions = this.extensions
.filter(e => (tab === 'all' ? !e.isInstalled : true)) .filter(e => (tab === 'all' ? !e.isInstalled : true))
.filter(e => (tab === 'installed' ? e.isInstalled : true)) .filter(e => (tab === 'installed' ? e.isInstalled : true))
@@ -113,6 +118,7 @@ window.PageExtensions = {
tab === 'installed' ? (e.isActive ? true : !!this.g.user.admin) : true tab === 'installed' ? (e.isActive ? true : !!this.g.user.admin) : true
) )
.filter(e => (tab === 'featured' ? e.isFeatured : true)) .filter(e => (tab === 'featured' ? e.isFeatured : true))
.filter(e => (isCategoryTab ? isInSelectedCategory(e) : true))
.filter(extensionNameContains(term)) .filter(extensionNameContains(term))
.map(e => ({ .map(e => ({
...e, ...e,
@@ -832,6 +838,9 @@ window.PageExtensions = {
async fetchAllExtensions() { async fetchAllExtensions() {
try { try {
const {data} = await LNbits.api.request('GET', `/api/v1/extension/all`) const {data} = await LNbits.api.request('GET', `/api/v1/extension/all`)
data.forEach(ext => {
ext.categories?.forEach(category => this.categories.add(category))
})
return data return data
} catch (error) { } catch (error) {
console.warn(error) console.warn(error)
+120
View File
@@ -48,6 +48,13 @@ window.PageWallet = {
hasNfc: false, hasNfc: false,
nfcReaderAbortController: null, nfcReaderAbortController: null,
formattedFiatAmount: 0, formattedFiatAmount: 0,
totalBreakdown: {
show: false,
loading: false,
rows: [],
selectedTypes: ['bitcoin', 'fiat'],
selectedTags: []
},
paymentFilter: { paymentFilter: {
'status[ne]': 'failed' 'status[ne]': 'failed'
}, },
@@ -84,9 +91,116 @@ window.PageWallet = {
}, },
formattedSatAmount() { formattedSatAmount() {
return LNbits.utils.formatMsat(this.receive.amountMsat) + ' sat' return LNbits.utils.formatMsat(this.receive.amountMsat) + ' sat'
},
totalBreakdownTags() {
const tags = this.totalBreakdown.rows.map(row => row.tag || null)
return [...new Set(tags)].sort((a, b) =>
this.totalBreakdownTagLabel(a).localeCompare(
this.totalBreakdownTagLabel(b)
)
)
},
hasFiatTotalBreakdown() {
return this.totalBreakdown.rows.some(row => row.is_fiat)
},
selectedTotalBreakdownRows() {
return this.totalBreakdown.rows.filter(row => {
const type = row.is_fiat ? 'fiat' : 'bitcoin'
return (
this.totalBreakdown.selectedTypes.includes(type) &&
this.totalBreakdown.selectedTags.includes(
this.totalBreakdownTagKey(row.tag)
)
)
})
},
selectedTotalBreakdownMsat() {
return this.selectedTotalBreakdownRows.reduce(
(total, row) => total + row.total,
0
)
},
selectedTotalBreakdownSat() {
return Math.round(this.selectedTotalBreakdownMsat / 1000)
},
selectedTotalBreakdownCount() {
return this.selectedTotalBreakdownRows.reduce(
(total, row) => total + row.payments_count,
0
)
},
formattedTotalBreakdown() {
return this.utils.formatBalance(
this.selectedTotalBreakdownSat,
this.g.denomination
)
},
formattedTotalBreakdownFiat() {
if (!this.g.fiatTracking) return null
const amount =
(this.selectedTotalBreakdownSat / 100000000) * this.g.exchangeRate
return LNbits.utils.formatCurrency(amount, this.g.wallet.currency)
},
primaryTotalBreakdownValue() {
if (this.g.isFiatPriority && this.g.fiatTracking) {
return this.formattedTotalBreakdownFiat || this.formattedTotalBreakdown
}
return this.formattedTotalBreakdown
},
secondaryTotalBreakdownValue() {
if (!this.g.fiatTracking) return null
if (this.g.isFiatPriority) {
return this.formattedTotalBreakdown
}
return this.formattedTotalBreakdownFiat
} }
}, },
methods: { methods: {
showWalletTotalBreakdown() {
this.totalBreakdown.show = true
if (!this.totalBreakdown.rows.length) {
this.fetchTotalBreakdown()
}
},
fetchTotalBreakdown() {
this.totalBreakdown.loading = true
LNbits.api
.getPaymentTotalBreakdown(this.g.wallet)
.then(response => {
this.totalBreakdown.rows = response.data
this.totalBreakdown.selectedTypes = ['bitcoin', 'fiat']
this.totalBreakdown.selectedTags = this.totalBreakdownTags.map(
this.totalBreakdownTagKey
)
this.totalBreakdown.loading = false
})
.catch(err => {
this.totalBreakdown.loading = false
LNbits.utils.notifyApiError(err)
})
},
totalBreakdownTagLabel(tag) {
return tag || 'No tag'
},
totalBreakdownTagKey(tag) {
return tag || '__untagged__'
},
totalBreakdownTagCount(tag) {
return this.totalBreakdown.rows
.filter(row => (row.tag || null) === tag)
.reduce((total, row) => total + row.payments_count, 0)
},
totalBreakdownTagMsat(tag) {
return this.totalBreakdown.rows
.filter(row => (row.tag || null) === tag)
.reduce((total, row) => total + row.total, 0)
},
formatTotalBreakdownMsat(msat) {
return this.utils.formatBalance(
Math.round(msat / 1000),
this.g.denomination
)
},
handleSendLnurl(lnurl) { handleSendLnurl(lnurl) {
this.parse.data.request = lnurl this.parse.data.request = lnurl
this.parse.show = true this.parse.show = true
@@ -223,6 +337,12 @@ window.PageWallet = {
if (data.tag === 'payRequest') { if (data.tag === 'payRequest') {
this.parse.lnurlpay = Object.freeze(data) this.parse.lnurlpay = Object.freeze(data)
this.parse.data.amount = data.minSendable / 1000 this.parse.data.amount = data.minSendable / 1000
this.receive.units = [
'sats',
...(this.g.allowedCurrencies.length > 0
? this.g.allowedCurrencies
: this.g.currencies)
]
} else if (data.tag === 'login') { } else if (data.tag === 'login') {
this.parse.lnurlauth = Object.freeze(data) this.parse.lnurlauth = Object.freeze(data)
} else if (data.tag === 'withdrawRequest') { } else if (data.tag === 'withdrawRequest') {
+14
View File
@@ -323,6 +323,20 @@ window._lnbitsUtils = {
converter.setOption('simpleLineBreaks', true) converter.setOption('simpleLineBreaks', true)
return converter.makeHtml(text) return converter.makeHtml(text)
}, },
_extI18nDirs: new Set(),
_extI18nLoaded: {},
loadExtI18n(dir, locale) {
this._extI18nDirs.add(dir)
const loaded = (this._extI18nLoaded[dir] ??= {})
if (loaded[locale]) return loaded[locale]
loaded[locale] = this.loadScript(`${dir}/${locale}.js`).catch(() => {
if (locale !== 'en') {
loaded['en'] ??= this.loadScript(`${dir}/en.js`).catch(() => {})
return loaded['en']
}
})
return loaded[locale]
},
async decryptLnurlPayAES(success_action, preimage) { async decryptLnurlPayAES(success_action, preimage) {
let keyb = new Uint8Array( let keyb = new Uint8Array(
preimage.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16)) preimage.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))
+6 -3
View File
@@ -6,7 +6,10 @@ from collections.abc import Callable, Coroutine
from loguru import logger from loguru import logger
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.core.services.payments import update_invoice_callback from lnbits.core.services.payments import (
get_standalone_payment,
update_invoice_from_paid_invoices_stream,
)
from lnbits.settings import settings from lnbits.settings import settings
from lnbits.wallets import get_funding_source from lnbits.wallets import get_funding_source
@@ -112,7 +115,7 @@ async def internal_invoice_listener() -> None:
while settings.lnbits_running: while settings.lnbits_running:
checking_id = await internal_invoice_queue.get() checking_id = await internal_invoice_queue.get()
logger.info(f"got an internal payment notification {checking_id}") logger.info(f"got an internal payment notification {checking_id}")
payment = await update_invoice_callback(checking_id) payment = await get_standalone_payment(checking_id, incoming=True)
if payment: if payment:
logger.success(f"internal invoice {checking_id} settled") logger.success(f"internal invoice {checking_id} settled")
await invoice_callback_dispatcher(payment) await invoice_callback_dispatcher(payment)
@@ -128,7 +131,7 @@ async def invoice_listener() -> None:
funding_source = get_funding_source() funding_source = get_funding_source()
async for checking_id in funding_source.paid_invoices_stream(): async for checking_id in funding_source.paid_invoices_stream():
logger.info(f"got a payment notification {checking_id}") logger.info(f"got a payment notification {checking_id}")
payment = await update_invoice_callback(checking_id) payment = await update_invoice_from_paid_invoices_stream(checking_id)
if payment: if payment:
logger.success(f"fundingsource invoice {checking_id} settled") logger.success(f"fundingsource invoice {checking_id} settled")
await invoice_callback_dispatcher(payment) await invoice_callback_dispatcher(payment)
+23 -1
View File
@@ -7,7 +7,29 @@
<q-tabs v-model="tab" active-color="primary" align="left"> <q-tabs v-model="tab" active-color="primary" align="left">
<q-tab name="installed" :label="$t('installed')"></q-tab> <q-tab name="installed" :label="$t('installed')"></q-tab>
<q-tab name="all" :label="$t('all')"></q-tab> <q-tab name="all" :label="$t('all')"></q-tab>
<q-tab name="featured" :label="$t('featured')"></q-tab> <q-tab
v-show="$q.screen.gt.xs"
name="featured"
:label="$t('featured')"
></q-tab>
<q-btn-dropdown auto-close stretch flat :label="$t('categories')">
<q-list>
<q-item
clickable
v-close-popup
v-for="category in categories"
@click="tab = category"
:key="category"
>
<q-item-section>
<q-item-label
class="text-capitalize"
v-text="category"
></q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<i <i
v-if="!g.user.admin && tab != 'installed'" v-if="!g.user.admin && tab != 'installed'"
v-text="$t('only_admins_can_install')" v-text="$t('only_admins_can_install')"
+119 -2
View File
@@ -28,7 +28,13 @@
<div class="col-7"> <div class="col-7">
<div class="row"> <div class="row">
<div class="col-auto"> <div class="col-auto">
<div class="text-h3 q-my-none full-width"> <div
class="text-h3 q-my-none full-width cursor-pointer"
role="button"
tabindex="0"
@click="showWalletTotalBreakdown"
@keyup.enter="showWalletTotalBreakdown"
>
<strong <strong
v-text=" v-text="
utils.formatBalance(g.wallet.sat, g.denomination) utils.formatBalance(g.wallet.sat, g.denomination)
@@ -77,7 +83,11 @@
<div class="col-auto"> <div class="col-auto">
<div <div
v-if="g.fiatTracking" v-if="g.fiatTracking"
class="text-h3 q-my-none text-no-wrap" class="text-h3 q-my-none text-no-wrap cursor-pointer"
role="button"
tabindex="0"
@click="showWalletTotalBreakdown"
@keyup.enter="showWalletTotalBreakdown"
> >
<strong v-text="formattedFiatAmount"></strong> <strong v-text="formattedFiatAmount"></strong>
</div> </div>
@@ -228,6 +238,113 @@
</div> </div>
</div> </div>
<q-dialog v-model="totalBreakdown.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-card-section>
<div class="row items-start q-mb-md">
<div class="col">
<div
class="text-h4 text-bold"
v-text="primaryTotalBreakdownValue"
></div>
<div
v-if="secondaryTotalBreakdownValue"
class="text-h6 text-italic"
style="opacity: 0.75"
v-text="secondaryTotalBreakdownValue"
></div>
<div
class="text-caption text-grey-5"
v-text="selectedTotalBreakdownCount + ' payments'"
></div>
</div>
<q-btn
flat
dense
round
color="grey"
icon="refresh"
:loading="totalBreakdown.loading"
@click="fetchTotalBreakdown"
>
<q-tooltip>Refresh</q-tooltip>
</q-btn>
</div>
<q-inner-loading :showing="totalBreakdown.loading"></q-inner-loading>
<div v-if="hasFiatTotalBreakdown" class="q-mb-md">
<q-checkbox
v-model="totalBreakdown.selectedTypes"
val="bitcoin"
label="Bitcoin"
></q-checkbox>
<q-checkbox
v-model="totalBreakdown.selectedTypes"
val="fiat"
label="Fiat"
></q-checkbox>
</div>
<q-separator v-if="hasFiatTotalBreakdown" class="q-mb-md"></q-separator>
<div style="max-height: min(52vh, 420px); overflow-y: auto">
<q-list dense>
<q-item
v-for="tag in totalBreakdownTags"
:key="totalBreakdownTagKey(tag)"
tag="label"
v-ripple
>
<q-item-section side>
<q-checkbox
v-model="totalBreakdown.selectedTags"
:val="totalBreakdownTagKey(tag)"
></q-checkbox>
</q-item-section>
<q-item-section>
<q-item-label>
<q-badge
v-if="tag"
color="yellow"
text-color="black"
v-text="'#' + tag"
></q-badge>
<span v-else v-text="totalBreakdownTagLabel(tag)"></span>
</q-item-label>
<q-item-label
caption
v-text="totalBreakdownTagCount(tag) + ' payments'"
></q-item-label>
</q-item-section>
<q-item-section side>
<span
v-text="formatTotalBreakdownMsat(totalBreakdownTagMsat(tag))"
></span>
</q-item-section>
</q-item>
</q-list>
</div>
<div v-if="!totalBreakdown.loading && !totalBreakdown.rows.length">
<q-banner class="bg-transparent text-grey-5">
No completed payments.
</q-banner>
</div>
<div class="row q-mt-md">
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('close')"
></q-btn>
</div>
</q-card-section>
</q-card>
</q-dialog>
<q-dialog v-model="receive.show" position="top" @hide="onReceiveDialogHide"> <q-dialog v-model="receive.show" position="top" @hide="onReceiveDialogHide">
<q-card <q-card
v-if="!receive.paymentReq" v-if="!receive.paymentReq"
+439
View File
@@ -0,0 +1,439 @@
"""
Electrum protocol client (https://github.com/spesmilo/electrum-protocol).
JSON-RPC 2.0 over TCP / SSL (newline-delimited), with request/response
correlation, subscription dispatch, and automatic keepalive pings.
server.version is sent automatically on connect as required by the spec.
"""
import asyncio
import hashlib
import itertools
import json
import ssl
from collections.abc import Callable
from typing import Any
from urllib.parse import urlparse
from loguru import logger
from pydantic import BaseModel
class ElectrumError(Exception):
pass
def scripthash_from_scriptpubkey(scriptpubkey: bytes) -> str:
"""Electrum script hash: SHA-256 of scriptPubKey, byte-reversed to hex."""
return hashlib.sha256(scriptpubkey).digest()[::-1].hex()
# ---------------------------------------------------------------------------
# Response models
# ---------------------------------------------------------------------------
class Balance(BaseModel):
confirmed: int
unconfirmed: int
class HistoryEntry(BaseModel):
tx_hash: str
height: int
fee: int | None = None # present for mempool entries
class MempoolEntry(BaseModel):
tx_hash: str
height: int
fee: int
class UTXO(BaseModel):
tx_hash: str
tx_pos: int
height: int
value: int # satoshis
class BlockHeader(BaseModel):
height: int
hex: str
class BlockHeaderProof(BaseModel):
"""Returned by get_block_header when cp_height > 0."""
branch: list[str]
header: str
root: str
class BlockHeaders(BaseModel):
count: int
hex: str
max: int
class MerkleProof(BaseModel):
block_height: int
merkle: list[str]
pos: int
class TxIdWithMerkle(BaseModel):
tx_hash: str
merkle: list[str]
class FeeHistogramEntry(BaseModel):
fee_rate: float
vsize: float
class ServerFeatures(BaseModel):
class Config:
extra = "allow"
genesis_hash: str = ""
protocol_max: str = ""
protocol_min: str = ""
server_version: str = ""
pruning: int | None = None
hash_function: str = "sha256d"
hosts: dict[str, Any] = {}
# ---------------------------------------------------------------------------
# Client
# ---------------------------------------------------------------------------
class ElectrumClient:
"""
Async Electrum protocol client over plain TCP or SSL.
Messages are newline-terminated JSON-RPC 2.0, as required by the spec.
Handles request/response correlation by id, routes push notifications to
registered callbacks, and sends periodic pings to keep the connection alive.
Usage::
# Plain TCP
async with ElectrumClient("tcp://blockstream.info:110") as client:
height = await client.get_height()
# SSL
async with ElectrumClient("ssl://electrum.blockstream.info:50002") as c:
height = await c.get_height()
"""
def __init__(
self,
url: str,
client_name: str = "lnbits",
protocol_version: str = "1.4",
ping_interval: float = 60.0,
) -> None:
parsed = urlparse(url)
self.host = parsed.hostname or ""
self.port = parsed.port or (
50002 if parsed.scheme in ("ssl", "https") else 50001
)
self.use_ssl = parsed.scheme in ("ssl", "https")
self.client_name = client_name
self.protocol_version = protocol_version
self.ping_interval = ping_interval
self._counter = itertools.count(1)
self._pending: dict[int, asyncio.Future[Any]] = {}
self._subscriptions: dict[str, list[Callable[[list[Any]], Any]]] = {}
self._recv_task: asyncio.Task[None] | None = None
self._ping_task: asyncio.Task[None] | None = None
self._reader: asyncio.StreamReader | None = None
self._writer: asyncio.StreamWriter | None = None
self.server_version: str = ""
self.negotiated_protocol: str = ""
async def connect(self, timeout: float = 10.0) -> None:
ssl_ctx: ssl.SSLContext | None = None
if self.use_ssl:
ssl_ctx = ssl.create_default_context()
self._reader, self._writer = await asyncio.wait_for(
asyncio.open_connection(
self.host, self.port, ssl=ssl_ctx, limit=4 * 1024 * 1024
),
timeout=timeout,
)
self._recv_task = asyncio.create_task(self._recv_loop())
result = await self._call(
"server.version", [self.client_name, self.protocol_version], timeout=timeout
)
self.server_version, self.negotiated_protocol = result[0], result[1]
logger.debug(
f"Electrum connected: server={self.server_version}"
f" protocol={self.negotiated_protocol}"
)
if self.ping_interval > 0:
self._ping_task = asyncio.create_task(self._ping_loop())
async def close(self) -> None:
for task in (self._ping_task, self._recv_task):
if task:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
except Exception:
logger.debug("Electrum: error while cancelling task")
self._ping_task = None
self._recv_task = None
if self._writer:
self._writer.close()
try:
await asyncio.wait_for(self._writer.wait_closed(), timeout=5.0)
except Exception:
logger.debug("Electrum: error while closing writer")
self._reader = None
self._writer = None
async def __aenter__(self) -> "ElectrumClient":
try:
await self.connect()
except BaseException:
await self.close()
raise
return self
async def __aexit__(self, *_: Any) -> None:
await self.close()
# ---- internal plumbing ----
async def _call(
self,
method: str,
params: list[Any] | dict[str, Any] | None = None,
timeout: float = 30.0,
) -> Any:
if not self._writer:
raise ElectrumError("Not connected")
req_id = next(self._counter)
fut: asyncio.Future[Any] = asyncio.get_running_loop().create_future()
self._pending[req_id] = fut
self._writer.write(
json.dumps(
{
"jsonrpc": "2.0",
"id": req_id,
"method": method,
"params": params if params is not None else [],
}
).encode()
+ b"\n"
)
await self._writer.drain()
try:
return await asyncio.wait_for(asyncio.shield(fut), timeout=timeout)
except asyncio.TimeoutError as exc:
self._pending.pop(req_id, None)
raise ElectrumError(f"Timeout waiting for response to {method!r}") from exc
def _dispatch(self, msg: dict[str, Any]) -> None:
msg_id = msg.get("id")
if msg_id is not None:
fut = self._pending.pop(msg_id, None)
if fut and not fut.done():
err = msg.get("error")
if err:
fut.set_exception(ElectrumError(err))
else:
fut.set_result(msg.get("result"))
else:
method = msg.get("method", "")
params = msg.get("params", [])
for cb in list(self._subscriptions.get(method, [])):
try:
result = cb(params)
if asyncio.iscoroutine(result):
self._bg_tasks.add(asyncio.create_task(result))
except Exception:
logger.exception(f"Electrum: callback error for {method!r}")
async def _recv_loop(self) -> None:
assert self._reader
self._bg_tasks: set[asyncio.Task[Any]] = set()
buf = b""
try:
while True:
chunk = await self._reader.read(65536)
if not chunk:
break
buf += chunk
while b"\n" in buf:
line, buf = buf.split(b"\n", 1)
if not line.strip():
continue
try:
msg: dict[str, Any] = json.loads(line)
except json.JSONDecodeError:
logger.warning(f"Electrum: invalid JSON: {line!r}")
continue
self._dispatch(msg)
except asyncio.CancelledError:
pass
except Exception:
logger.exception("Electrum: recv loop error")
finally:
for fut in self._pending.values():
if not fut.done():
fut.set_exception(ElectrumError("Connection closed"))
self._pending.clear()
async def _ping_loop(self) -> None:
try:
while True:
await asyncio.sleep(self.ping_interval)
await self._call("server.ping")
except asyncio.CancelledError:
pass
except Exception:
logger.exception("Electrum: ping loop error")
# ---- subscription management ----
def on(self, method: str, callback: Callable[[list[Any]], Any]) -> None:
"""Register a notification callback for a subscription method."""
self._subscriptions.setdefault(method, []).append(callback)
def off(self, method: str, callback: Callable[[list[Any]], Any]) -> None:
"""Remove a previously registered notification callback."""
cbs = self._subscriptions.get(method)
if cbs and callback in cbs:
cbs.remove(callback)
# ---- server methods ----
async def server_ping(self) -> None:
await self._call("server.ping")
async def server_banner(self) -> str:
return await self._call("server.banner")
async def server_features(self) -> ServerFeatures:
data = await self._call("server.features")
return ServerFeatures.parse_obj(data)
async def server_peers(self) -> list[Any]:
return await self._call("server.peers.subscribe")
# ---- scripthash methods ----
async def get_balance(self, scripthash: str) -> Balance:
data = await self._call("blockchain.scripthash.get_balance", [scripthash])
return Balance.parse_obj(data)
async def get_history(self, scripthash: str) -> list[HistoryEntry]:
data = await self._call("blockchain.scripthash.get_history", [scripthash])
return [HistoryEntry.parse_obj(e) for e in data]
async def get_mempool(self, scripthash: str) -> list[MempoolEntry]:
data = await self._call("blockchain.scripthash.get_mempool", [scripthash])
return [MempoolEntry.parse_obj(e) for e in data]
async def listunspent(self, scripthash: str) -> list[UTXO]:
data = await self._call("blockchain.scripthash.listunspent", [scripthash])
return [UTXO.parse_obj(e) for e in data]
async def subscribe_scripthash(
self,
scripthash: str,
callback: Callable[[list[Any]], Any] | None = None,
) -> str | None:
"""Subscribe to status changes; returns current status hash or None."""
if callback:
self.on("blockchain.scripthash.subscribe", callback)
return await self._call("blockchain.scripthash.subscribe", [scripthash])
async def unsubscribe_scripthash(
self,
scripthash: str,
callback: Callable[[list[Any]], Any] | None = None,
) -> bool:
if callback:
self.off("blockchain.scripthash.subscribe", callback)
return await self._call("blockchain.scripthash.unsubscribe", [scripthash])
async def subscribe_headers(
self,
callback: Callable[[list[Any]], Any] | None = None,
) -> BlockHeader:
"""Subscribe to new block headers; returns current tip."""
if callback:
self.on("blockchain.headers.subscribe", callback)
data = await self._call("blockchain.headers.subscribe")
return BlockHeader.parse_obj(data)
# ---- transaction methods ----
async def broadcast(self, raw_tx: str) -> str:
"""Broadcast a raw transaction hex; returns txid on success."""
return await self._call("blockchain.transaction.broadcast", [raw_tx])
async def get_transaction(
self, txid: str, verbose: bool = False
) -> str | dict[str, Any]:
return await self._call("blockchain.transaction.get", [txid, verbose])
async def get_merkle(self, txid: str, height: int) -> MerkleProof:
data = await self._call("blockchain.transaction.get_merkle", [txid, height])
return MerkleProof.parse_obj(data)
async def get_tx_id_from_pos(
self, height: int, tx_pos: int, merkle: bool = False
) -> str | TxIdWithMerkle:
data = await self._call(
"blockchain.transaction.id_from_pos", [height, tx_pos, merkle]
)
if isinstance(data, dict):
return TxIdWithMerkle.parse_obj(data)
return data
# ---- block methods ----
async def get_tip(self) -> BlockHeader:
"""Returns current chain tip."""
data = await self._call("blockchain.headers.subscribe")
return BlockHeader.parse_obj(data)
async def get_height(self) -> int:
"""Returns the current best block height."""
return (await self.get_tip()).height
async def get_block_header(
self, height: int, cp_height: int = 0
) -> str | BlockHeaderProof:
data = await self._call("blockchain.block.header", [height, cp_height])
if isinstance(data, dict):
return BlockHeaderProof.parse_obj(data)
return data
async def get_block_headers(
self, start_height: int, count: int, cp_height: int = 0
) -> BlockHeaders:
data = await self._call(
"blockchain.block.headers", [start_height, count, cp_height]
)
return BlockHeaders.parse_obj(data)
# ---- fee methods ----
async def estimate_fee(self, num_blocks: int) -> float:
"""Returns estimated fee rate in BTC/kB for confirmation within num_blocks."""
return await self._call("blockchain.estimatefee", [num_blocks])
async def fee_histogram(self) -> list[FeeHistogramEntry]:
"""Returns mempool fee histogram as FeeHistogramEntry(fee_rate, vsize) list."""
data = await self._call("mempool.get_fee_histogram")
return [FeeHistogramEntry(fee_rate=r[0], vsize=r[1]) for r in data]
+2
View File
@@ -192,6 +192,8 @@ class LndWallet(Wallet):
fee_limit_msat=fee_limit_msat, fee_limit_msat=fee_limit_msat,
timeout_seconds=30, timeout_seconds=30,
no_inflight_updates=True, no_inflight_updates=True,
max_parts=16,
time_pref=0.9,
) )
try: try:
res: Payment = await self.router_rpc.SendPaymentV2(req).read() res: Payment = await self.router_rpc.SendPaymentV2(req).read()
+41 -40
View File
@@ -3,6 +3,7 @@ import base64
import hashlib import hashlib
import json import json
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from typing import Any
import httpx import httpx
from loguru import logger from loguru import logger
@@ -123,8 +124,8 @@ class LndRestWallet(Wallet):
hashlib.sha256(unhashed_description).digest() hashlib.sha256(unhashed_description).digest()
).decode("ascii") ).decode("ascii")
preimage, _payment_hash = random_secret_and_hash() preimage, payment_hash = random_secret_and_hash()
_data["r_hash"] = base64.b64encode(bytes.fromhex(_payment_hash)).decode() _data["r_hash"] = base64.b64encode(bytes.fromhex(payment_hash)).decode()
_data["r_preimage"] = base64.b64encode(bytes.fromhex(preimage)).decode() _data["r_preimage"] = base64.b64encode(bytes.fromhex(preimage)).decode()
try: try:
@@ -132,34 +133,7 @@ class LndRestWallet(Wallet):
r.raise_for_status() r.raise_for_status()
data = r.json() data = r.json()
if len(data) == 0: return self._parse_create_invoice_response(r, data, preimage)
return InvoiceResponse(ok=False, error_message="no data")
if "error" in data:
return InvoiceResponse(
ok=False, error_message=f"""Server error: '{data["error"]}'"""
)
if r.is_error:
return InvoiceResponse(
ok=False, error_message=f"Server error: '{r.text}'"
)
if "payment_request" not in data or "r_hash" not in data:
return InvoiceResponse(
ok=False, error_message="Server error: 'missing required fields'"
)
payment_request = data["payment_request"]
payment_hash = base64.b64decode(data["r_hash"]).hex()
checking_id = payment_hash
return InvoiceResponse(
ok=True,
checking_id=checking_id,
payment_request=payment_request,
preimage=preimage,
)
except json.JSONDecodeError: except json.JSONDecodeError:
return InvoiceResponse( return InvoiceResponse(
ok=False, error_message="Server error: 'invalid json response'" ok=False, error_message="Server error: 'invalid json response'"
@@ -242,12 +216,13 @@ class LndRestWallet(Wallet):
logger.warning(f"Error getting invoice status: {e}") logger.warning(f"Error getting invoice status: {e}")
return PaymentPendingStatus() return PaymentPendingStatus()
if r.is_error or data.get("settled") is None: if r.is_error or data.get("state") is None:
# this must also work when checking_id is not a hex recognizable by lnd # this must also work when checking_id is not a hex recognizable by lnd
# it will return an error and no "settled" attribute on the object # it will return an error and no "state" attribute on the object
logger.warning(f"Error checking invoice from LND REST API: {r.text}")
return PaymentPendingStatus() return PaymentPendingStatus()
if data.get("settled") is True: if data.get("state") == "SETTLED":
return PaymentSuccessStatus() return PaymentSuccessStatus()
if data.get("state") == "CANCELED": if data.get("state") == "CANCELED":
@@ -264,10 +239,11 @@ class LndRestWallet(Wallet):
"ascii" "ascii"
) )
except ValueError: except ValueError:
logger.warning("Invalid checking_id format, must be hex: {checking_id}")
return PaymentPendingStatus() return PaymentPendingStatus()
url = f"/v2/router/track/{checking_id}" url = f"/v2/router/track/{checking_id}"
async with self.client.stream("GET", url, timeout=None) as r: async with self.client.stream("GET", url, timeout=30) as r:
async for json_line in r.aiter_lines(): async for json_line in r.aiter_lines():
try: try:
line = json.loads(json_line) line = json.loads(json_line)
@@ -298,7 +274,7 @@ class LndRestWallet(Wallet):
return PaymentFailedStatus() return PaymentFailedStatus()
elif status == "IN_FLIGHT": elif status == "IN_FLIGHT":
logger.info(f"LNDRest Payment in flight: {checking_id}") logger.info(f"LNDRest Payment in flight: {checking_id}")
return PaymentPendingStatus() continue
logger.info(f"LNDRest Payment non-existent: {checking_id}") logger.info(f"LNDRest Payment non-existent: {checking_id}")
return PaymentPendingStatus() return PaymentPendingStatus()
@@ -311,13 +287,12 @@ class LndRestWallet(Wallet):
async for line in r.aiter_lines(): async for line in r.aiter_lines():
try: try:
inv = json.loads(line)["result"] inv = json.loads(line)["result"]
if not inv["settled"]: if not inv.get("state") == "SETTLED":
continue continue
payment_hash = base64.b64decode(inv.get("r_hash")).hex()
except Exception as exc: except Exception as exc:
logger.debug(exc) logger.debug(exc)
continue continue
payment_hash = base64.b64decode(inv["r_hash"]).hex()
yield payment_hash yield payment_hash
except Exception as exc: except Exception as exc:
logger.warning( logger.warning(
@@ -363,8 +338,6 @@ class LndRestWallet(Wallet):
return InvoiceResponse(ok=False, error_message=str(exc)) return InvoiceResponse(ok=False, error_message=str(exc))
payment_request = data["payment_request"] payment_request = data["payment_request"]
payment_hash = base64.b64encode(bytes.fromhex(payment_hash)).decode("ascii")
return InvoiceResponse( return InvoiceResponse(
ok=True, checking_id=payment_hash, payment_request=payment_request ok=True, checking_id=payment_hash, payment_request=payment_request
) )
@@ -399,3 +372,31 @@ class LndRestWallet(Wallet):
except Exception as exc: except Exception as exc:
logger.warning(exc) logger.warning(exc)
return InvoiceResponse(ok=False, error_message=str(exc)) return InvoiceResponse(ok=False, error_message=str(exc))
def _parse_create_invoice_response(
self, r: Any, data: dict, preimage: str
) -> InvoiceResponse:
if not data:
return InvoiceResponse(ok=False, error_message="no data")
if "error" in data:
return InvoiceResponse(
ok=False, error_message=f"Server error: '{data['error']}'"
)
if r.is_error:
return InvoiceResponse(ok=False, error_message=f"Server error: '{r.text}'")
if "payment_request" not in data or "r_hash" not in data:
return InvoiceResponse(
ok=False, error_message="Server error: 'missing required fields'"
)
try:
payment_hash = base64.b64decode(data["r_hash"]).hex()
except Exception:
return InvoiceResponse(
ok=False, error_message=f"Unable to b64decode to {data['r_hash']}."
)
return InvoiceResponse(
ok=True,
checking_id=payment_hash,
payment_request=data["payment_request"],
preimage=preimage,
)
+5 -4
View File
@@ -117,11 +117,12 @@ class PhoenixdWallet(Wallet):
# PhoenixD description limited to 128 characters # PhoenixD description limited to 128 characters
if description_hash: if description_hash:
data["descriptionHash"] = description_hash.hex() data["descriptionHash"] = description_hash.hex()
elif unhashed_description:
data["descriptionHash"] = hashlib.sha256(
unhashed_description
).hexdigest()
else: else:
desc = memo desc = memo or ""
if desc is None and unhashed_description:
desc = unhashed_description.decode()
desc = desc or ""
if len(desc) > 128: if len(desc) > 128:
data["descriptionHash"] = hashlib.sha256(desc.encode()).hexdigest() data["descriptionHash"] = hashlib.sha256(desc.encode()).hexdigest()
else: else:
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "lnbits" name = "lnbits"
version = "1.5.5-rc2" version = "1.5.5-rc3"
requires-python = ">=3.10,<3.13" requires-python = ">=3.10,<3.13"
description = "LNbits, free and open-source Lightning wallet and accounts system." description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }] authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
+73 -3
View File
@@ -6,11 +6,16 @@ import pytest
from fastapi import HTTPException from fastapi import HTTPException
from pydantic import ValidationError from pydantic import ValidationError
from lnbits.core.crud.payments import create_payment, get_payment, get_payments from lnbits.core.crud.payments import (
create_payment,
get_payment,
get_payments,
update_payment,
)
from lnbits.core.models import Account, CreateInvoice, PaymentFilters, PaymentState from lnbits.core.models import Account, CreateInvoice, PaymentFilters, PaymentState
from lnbits.core.models.payments import CancelInvoice, CreatePayment, SettleInvoice from lnbits.core.models.payments import CancelInvoice, CreatePayment, SettleInvoice
from lnbits.core.models.users import AccountId from lnbits.core.models.users import AccountId
from lnbits.core.models.wallets import KeyType, WalletTypeInfo from lnbits.core.models.wallets import BaseWalletTypeInfo, KeyType, WalletTypeInfo
from lnbits.core.services.payments import create_wallet_invoice from lnbits.core.services.payments import create_wallet_invoice
from lnbits.core.services.users import create_user_account from lnbits.core.services.users import create_user_account
from lnbits.core.views.payment_api import ( from lnbits.core.views.payment_api import (
@@ -20,6 +25,7 @@ from lnbits.core.views.payment_api import (
api_payments_daily_stats, api_payments_daily_stats,
api_payments_fee_reserve, api_payments_fee_reserve,
api_payments_settle, api_payments_settle,
api_payments_total_breakdown,
api_payments_wallets_stats, api_payments_wallets_stats,
) )
from lnbits.db import Filter, Filters from lnbits.db import Filter, Filters
@@ -146,6 +152,61 @@ async def test_payment_external_id_is_stored_and_validated():
CreateInvoice(out=False, amount=21, external_id="provider payment 123") CreateInvoice(out=False, amount=21, external_id="provider payment 123")
@pytest.mark.anyio
async def test_payment_api_total_breakdown_groups_wallet_tags_and_fiat():
first_user = await create_user_account(
Account(
id=uuid4().hex,
username=f"user_{uuid4().hex[:8]}",
email=f"user_{uuid4().hex[:8]}@lnbits.com",
)
)
second_user = await create_user_account(
Account(
id=uuid4().hex,
username=f"user_{uuid4().hex[:8]}",
email=f"user_{uuid4().hex[:8]}@lnbits.com",
)
)
first_wallet = first_user.wallets[0]
second_wallet = second_user.wallets[0]
await _create_payment(first_wallet.id, amount_msat=2_000, tag="coffee")
await _create_payment(
first_wallet.id,
amount_msat=4_000,
tag="coffee",
fiat_provider="stripe",
extra={"fiat_payment_request": "https://stripe.test/session"},
)
await _create_payment(first_wallet.id, amount_msat=-1_000)
await _create_payment(second_wallet.id, amount_msat=8_000, tag="books")
breakdown = await api_payments_total_breakdown(
BaseWalletTypeInfo(key_type=KeyType.invoice, wallet=first_wallet)
)
assert any(
item.tag == "coffee"
and item.is_fiat is False
and item.total == 2_000
and item.payments_count == 1
for item in breakdown
)
assert any(
item.tag == "coffee"
and item.is_fiat is True
and item.total == 4_000
and item.payments_count == 1
for item in breakdown
)
assert any(
item.tag is None and item.is_fiat is False and item.total == -1_000
for item in breakdown
)
assert all(item.tag != "books" for item in breakdown)
@pytest.mark.anyio @pytest.mark.anyio
async def test_payment_api_fee_reserve_and_hold_invoice_actions(mocker): async def test_payment_api_fee_reserve_and_hold_invoice_actions(mocker):
user = await create_user_account( user = await create_user_account(
@@ -383,8 +444,13 @@ async def _create_payment(
status: PaymentState = PaymentState.SUCCESS, status: PaymentState = PaymentState.SUCCESS,
payment_hash: str | None = None, payment_hash: str | None = None,
tag: str | None = None, tag: str | None = None,
fiat_provider: str | None = None,
extra: dict | None = None,
) -> str: ) -> str:
checking_id = f"checking_{uuid4().hex[:8]}" checking_id = f"checking_{uuid4().hex[:8]}"
payment_extra = extra or {}
if tag:
payment_extra["tag"] = tag
await create_payment( await create_payment(
checking_id=checking_id, checking_id=checking_id,
data=CreatePayment( data=CreatePayment(
@@ -393,8 +459,12 @@ async def _create_payment(
bolt11=f"bolt11_{checking_id}", bolt11=f"bolt11_{checking_id}",
amount_msat=amount_msat, amount_msat=amount_msat,
memo=f"payment_{checking_id}", memo=f"payment_{checking_id}",
extra={"tag": tag} if tag else {}, extra=payment_extra,
), ),
status=status, status=status,
) )
if fiat_provider:
payment = await get_payment(checking_id)
payment.fiat_provider = fiat_provider
await update_payment(payment)
return checking_id return checking_id
-2
View File
@@ -26,8 +26,6 @@ docker_bitcoin_cli = [
"exec", "exec",
"lnbits-bitcoind-1", "lnbits-bitcoind-1",
"bitcoin-cli", "bitcoin-cli",
"-rpcuser=lnbits",
"-rpcpassword=lnbits",
"-regtest", "-regtest",
] ]
+184
View File
@@ -0,0 +1,184 @@
"""
Electrum client integration tests against the regtest electrs container.
Requires the regtest docker-compose stack (docker/regtest/docker-compose.yml).
electrs is exposed on localhost:19001 (plain TCP) and localhost:3002 (HTTP).
"""
import asyncio
import httpx
import pytest
from loguru import logger
from lnbits.utils.electrum import ElectrumClient, scripthash_from_scriptpubkey
from .helpers import docker_bitcoin_cli, run_cmd, run_cmd_json
ELECTRS_HOST = "localhost"
ELECTRS_PORT = 19001
ELECTRS_HTTP = "http://localhost:3002"
def bitcoin_height() -> int:
return run_cmd_json([*docker_bitcoin_cli, "getblockchaininfo"])["blocks"]
def mine_blocks(n: int = 1) -> int:
"""Mine n blocks and return the new chain height."""
run_cmd([*docker_bitcoin_cli, "-generate", str(n)])
return bitcoin_height()
def new_address() -> str:
return run_cmd([*docker_bitcoin_cli, "getnewaddress", "bech32"])
def get_scriptpubkey(address: str) -> bytes:
info = run_cmd_json([*docker_bitcoin_cli, "getaddressinfo", address])
return bytes.fromhex(info["scriptPubKey"])
def send_to_address(address: str, sats: int) -> str:
btc = f"{sats * 1e-8:.8f}"
return run_cmd([*docker_bitcoin_cli, "sendtoaddress", address, btc])
async def wait_for_electrs(height: int, timeout: float = 15.0) -> None:
"""Poll electrs HTTP until it has indexed up to `height`."""
loop = asyncio.get_running_loop()
deadline = loop.time() + timeout
async with httpx.AsyncClient() as http:
while loop.time() < deadline:
try:
r = await http.get(f"{ELECTRS_HTTP}/blocks/tip/height", timeout=2)
if int(r.text) >= height:
return
except Exception:
logger.debug("electrs not ready yet")
await asyncio.sleep(0.25)
raise TimeoutError(f"electrs did not reach height {height} within {timeout}s")
@pytest.fixture(scope="module", autouse=True)
async def wait_after_electrum_tests():
yield
await asyncio.sleep(1)
@pytest.mark.anyio
async def test_connect_and_height():
async with ElectrumClient(f"tcp://{ELECTRS_HOST}:{ELECTRS_PORT}") as client:
before = await client.get_height()
target = mine_blocks(3)
await wait_for_electrs(target)
async with ElectrumClient(f"tcp://{ELECTRS_HOST}:{ELECTRS_PORT}") as client:
after = await client.get_height()
assert isinstance(before, int) and before >= 0
assert after == before + 3
@pytest.mark.anyio
async def test_get_tip():
async with ElectrumClient(f"tcp://{ELECTRS_HOST}:{ELECTRS_PORT}") as client:
tip = await client.get_tip()
assert isinstance(tip.height, int)
assert isinstance(tip.hex, str)
assert len(tip.hex) == 160 # 80-byte serialised header
@pytest.mark.anyio
async def test_server_banner():
async with ElectrumClient(f"tcp://{ELECTRS_HOST}:{ELECTRS_PORT}") as client:
banner = await client.server_banner()
assert isinstance(banner, str)
@pytest.mark.anyio
async def test_balance_after_payment():
address = new_address()
scripthash = scripthash_from_scriptpubkey(get_scriptpubkey(address))
async with ElectrumClient(f"tcp://{ELECTRS_HOST}:{ELECTRS_PORT}") as client:
empty = await client.get_balance(scripthash)
assert empty.confirmed == 0
assert empty.unconfirmed == 0
send_to_address(address, 500_000)
target = mine_blocks(1)
await wait_for_electrs(target)
async with ElectrumClient(f"tcp://{ELECTRS_HOST}:{ELECTRS_PORT}") as client:
confirmed = await client.get_balance(scripthash)
assert confirmed.confirmed == 500_000
assert confirmed.unconfirmed == 0
@pytest.mark.anyio
async def test_history_and_utxos():
address = new_address()
scripthash = scripthash_from_scriptpubkey(get_scriptpubkey(address))
send_to_address(address, 250_000)
target = mine_blocks(1)
await wait_for_electrs(target)
async with ElectrumClient(f"tcp://{ELECTRS_HOST}:{ELECTRS_PORT}") as client:
history = await client.get_history(scripthash)
assert len(history) >= 1
assert history[0].tx_hash
assert history[0].height > 0
utxos = await client.listunspent(scripthash)
assert len(utxos) == 1
assert utxos[0].value == 250_000
raw_tx = await client.get_transaction(utxos[0].tx_hash)
assert isinstance(raw_tx, str) and len(raw_tx) > 0
@pytest.mark.anyio
async def test_subscribe_scripthash_payment():
address = new_address()
scripthash = scripthash_from_scriptpubkey(get_scriptpubkey(address))
received: list = []
event = asyncio.Event()
def on_change(params: list) -> None:
received.append(params)
event.set()
async with ElectrumClient(f"tcp://{ELECTRS_HOST}:{ELECTRS_PORT}") as client:
initial_status = await client.subscribe_scripthash(
scripthash, callback=on_change
)
assert initial_status is None # fresh address has no history
send_to_address(address, 777_000)
target = mine_blocks(1)
await wait_for_electrs(target)
await asyncio.wait_for(event.wait(), timeout=10)
assert len(received) == 1
assert received[0][0] == scripthash # first param is the scripthash
assert received[0][1] is not None # second param is the new status hash
balance = await client.get_balance(scripthash)
assert balance.confirmed == 777_000
@pytest.mark.anyio
async def test_subscribe_headers():
async with ElectrumClient(f"tcp://{ELECTRS_HOST}:{ELECTRS_PORT}") as client:
notifications: list = []
tip = await client.subscribe_headers(callback=lambda p: notifications.append(p))
height_before = tip.height
target = mine_blocks(1)
await wait_for_electrs(target)
assert await client.get_height() == height_before + 1
+97 -11
View File
@@ -8,7 +8,7 @@ from unittest.mock import AsyncMock
import pytest import pytest
from pytest_mock.plugin import MockerFixture from pytest_mock.plugin import MockerFixture
from lnbits.core.crud.payments import get_payments from lnbits.core.crud.payments import get_payment, get_payments
from lnbits.core.crud.users import get_user from lnbits.core.crud.users import get_user
from lnbits.core.crud.wallets import create_wallet from lnbits.core.crud.wallets import create_wallet
from lnbits.core.models.payments import CreateInvoice, Payment, PaymentState from lnbits.core.models.payments import CreateInvoice, Payment, PaymentState
@@ -919,8 +919,10 @@ async def test_revolut_wallet_create_subscription(settings: Settings):
"PLAN_VARIATION_123", 1, payment_options "PLAN_VARIATION_123", 1, payment_options
) )
subscription_request_id = payment_options.subscription_request_id
assert response.ok is True assert response.ok is True
assert response.subscription_request_id is not None assert response.subscription_request_id == "SUBSCRIPTION123"
assert subscription_request_id is not None
assert ( assert (
response.checkout_session_url response.checkout_session_url
== "https://checkout.revolut.com/payment-link/sub_123" == "https://checkout.revolut.com/payment-link/sub_123"
@@ -933,16 +935,14 @@ async def test_revolut_wallet_create_subscription(settings: Settings):
assert payload["plan_variation_id"] == "PLAN_VARIATION_123" assert payload["plan_variation_id"] == "PLAN_VARIATION_123"
assert payload["customer_id"] == "CUSTOMER123" assert payload["customer_id"] == "CUSTOMER123"
assert client.calls[1][1]["timeout"] == 30 assert client.calls[1][1]["timeout"] == 30
assert client.calls[1][1]["headers"]["Idempotency-Key"] == ( assert client.calls[1][1]["headers"]["Idempotency-Key"] == (subscription_request_id)
response.subscription_request_id
)
assert payload["setup_order_redirect_url"] == ( assert payload["setup_order_redirect_url"] == (
"https://lnbits.example/subscription-success" "https://lnbits.example/subscription-success"
) )
reference = json.loads(payload["external_reference"]) reference = json.loads(payload["external_reference"])
assert reference["wallet_id"] == "wallet_1" assert reference["wallet_id"] == "wallet_1"
assert reference["tag"] == "gold" assert reference["tag"] == "gold"
assert reference["subscription_request_id"] == response.subscription_request_id assert reference["subscription_request_id"] == subscription_request_id
assert reference["memo"] == "Monthly Gold" assert reference["memo"] == "Monthly Gold"
assert reference["extra"]["link"] == "link-1" assert reference["extra"]["link"] == "link-1"
assert client.calls[2][0] == "/api/orders/ORDER123" assert client.calls[2][0] == "/api/orders/ORDER123"
@@ -1235,13 +1235,55 @@ async def test_revolut_wallet_cancel_subscription(settings: Settings):
settings.revolut_api_version = "2026-04-20" settings.revolut_api_version = "2026-04-20"
wallet = RevolutWallet() wallet = RevolutWallet()
client = MockHTTPClient([MockHTTPResponse(json_data={})]) client = MockHTTPClient(
[
MockHTTPResponse(
json_data={
"external_reference": json.dumps({"wallet_id": "wallet_1"}),
}
),
MockHTTPResponse(json_data={}),
]
)
wallet.client = client # type: ignore[assignment] wallet.client = client # type: ignore[assignment]
response = await wallet.cancel_subscription("SUBSCRIPTION123", "wallet_1") response = await wallet.cancel_subscription("SUBSCRIPTION123", "wallet_1")
assert response.ok is True assert response.ok is True
assert client.calls[0][0] == "/api/subscriptions/SUBSCRIPTION123/cancel" assert client.calls[0][0] == "/api/subscriptions/SUBSCRIPTION123"
assert client.calls[1][0] == "/api/subscriptions/SUBSCRIPTION123/cancel"
@pytest.mark.anyio
async def test_revolut_wallet_cancel_subscription_checks_wallet_id(
settings: Settings,
):
settings.revolut_api_endpoint = "https://sandbox-merchant.revolut.com"
settings.revolut_api_secret_key = "revolut-secret"
settings.revolut_api_version = "2026-04-20"
wallet = RevolutWallet()
client = MockHTTPClient(
[
MockHTTPResponse(
json_data={
"external_reference": json.dumps({"wallet_id": "wallet_2"}),
}
),
]
)
wallet.client = client # type: ignore[assignment]
response = await wallet.cancel_subscription("SUBSCRIPTION123", "wallet_1")
assert response.ok is False
assert response.error_message == "Subscription not found."
assert client.calls == [
(
"/api/subscriptions/SUBSCRIPTION123",
{"timeout": 30},
)
]
@pytest.mark.anyio @pytest.mark.anyio
@@ -1702,15 +1744,59 @@ async def test_check_fiat_status_handles_internal_states(mocker: MockerFixture):
amount=1000, amount=1000,
fee=0, fee=0,
bolt11="bolt11", bolt11="bolt11",
status=PaymentState.PENDING, status=PaymentState.SUCCESS,
fiat_provider="stripe", fiat_provider="stripe",
extra={"fiat_checking_id": "stripe_checking_id"}, extra={"fiat_checking_id": "stripe_checking_id"},
), )
skip_internal_payment_notifications=True,
) )
assert queue_put.await_count == 1 assert queue_put.await_count == 1
@pytest.mark.anyio
async def test_check_fiat_status_persists_successful_payment(
to_wallet: Wallet, settings: Settings, mocker: MockerFixture
):
settings.stripe_enabled = True
settings.stripe_api_secret_key = "mock_sk_test_4eC39HqLyjWDarjtT1zdp7dc"
settings.stripe_limits.service_min_amount_sats = 0
settings.stripe_limits.service_max_amount_sats = 0
settings.stripe_limits.service_fee_wallet_id = None
settings.stripe_limits.service_faucet_wallet_id = None
invoice_data = CreateInvoice(
unit="USD", amount=1.0, memo="Test", fiat_provider="stripe"
)
fiat_mock_response = FiatInvoiceResponse(
ok=True,
checking_id=f"session_paid_{get_random_string(10)}",
payment_request="https://stripe.com/pay/session_paid",
)
mocker.patch(
"lnbits.fiat.StripeWallet.create_invoice",
AsyncMock(return_value=fiat_mock_response),
)
mocker.patch(
"lnbits.utils.exchange_rates.get_fiat_rate_satoshis",
AsyncMock(return_value=1000),
)
payment = await payments.create_fiat_invoice(to_wallet.id, invoice_data)
assert payment.status == PaymentState.PENDING
mocker.patch(
"lnbits.fiat.StripeWallet.get_invoice_status",
AsyncMock(return_value=FiatPaymentStatus(paid=True)),
)
queue_put = mocker.patch("lnbits.tasks.internal_invoice_queue.put", AsyncMock())
status = await check_fiat_status(payment)
assert status.success is True
assert payment.status == PaymentState.SUCCESS
updated_payment = await get_payment(payment.checking_id)
assert updated_payment.status == PaymentState.SUCCESS
queue_put.assert_awaited_once_with(payment.checking_id)
@pytest.mark.anyio @pytest.mark.anyio
async def test_verify_paypal_webhook_requires_configuration(settings: Settings): async def test_verify_paypal_webhook_requires_configuration(settings: Settings):
settings.paypal_webhook_id = None settings.paypal_webhook_id = None
+14 -1
View File
@@ -15,7 +15,12 @@ from lnbits.core.services import create_invoice, create_user_account, pay_invoic
from lnbits.core.services.payments import update_wallet_balance from lnbits.core.services.payments import update_wallet_balance
from lnbits.exceptions import InvoiceError, PaymentError from lnbits.exceptions import InvoiceError, PaymentError
from lnbits.settings import Settings from lnbits.settings import Settings
from lnbits.tasks import create_task, wait_for_paid_invoices from lnbits.tasks import (
create_task,
internal_invoice_listener,
internal_invoice_queue,
wait_for_paid_invoices,
)
from lnbits.wallets.base import PaymentResponse from lnbits.wallets.base import PaymentResponse
from lnbits.wallets.fake import FakeWallet from lnbits.wallets.fake import FakeWallet
@@ -231,7 +236,15 @@ async def test_notification_for_internal_payment(
): ):
test_name = "test_notification_for_internal_payment" test_name = "test_notification_for_internal_payment"
# Drain stale items left by session-scoped fixtures (e.g. update_wallet_balance)
while not internal_invoice_queue.empty():
try:
internal_invoice_queue.get_nowait()
except asyncio.QueueEmpty:
break
on_paid_mock = mocker.AsyncMock() on_paid_mock = mocker.AsyncMock()
create_task(internal_invoice_listener())
create_task(wait_for_paid_invoices(test_name, on_paid_mock)()) create_task(wait_for_paid_invoices(test_name, on_paid_mock)())
payment = await create_invoice( payment = await create_invoice(
wallet_id=to_wallet.id, wallet_id=to_wallet.id,
+41 -1
View File
@@ -39,6 +39,7 @@ from lnbits.core.services.notifications import (
send_nostr_notification, send_nostr_notification,
send_nostr_notifications, send_nostr_notifications,
send_notification, send_notification,
send_notification_in_background,
send_payment_notification, send_payment_notification,
send_payment_push_notification, send_payment_push_notification,
send_push_notification, send_push_notification,
@@ -117,7 +118,7 @@ async def test_send_admin_and_user_notification_use_expected_targets(
settings: Settings, mocker: MockerFixture settings: Settings, mocker: MockerFixture
): ):
send_mock = mocker.patch( send_mock = mocker.patch(
"lnbits.core.services.notifications.send_notification", "lnbits.core.services.notifications.send_notification_in_background",
mocker.AsyncMock(), mocker.AsyncMock(),
) )
original_chat_id = settings.lnbits_telegram_notifications_chat_id original_chat_id = settings.lnbits_telegram_notifications_chat_id
@@ -159,6 +160,45 @@ async def test_send_admin_and_user_notification_use_expected_targets(
) )
@pytest.mark.anyio
async def test_send_notification_in_background_schedules_notification(
mocker: MockerFixture,
):
scheduled = []
def create_task(coro):
scheduled.append(coro)
coro.close()
return mocker.Mock()
create_task_mock = mocker.patch(
"lnbits.core.services.notifications.create_task",
side_effect=create_task,
)
send_mock = mocker.patch(
"lnbits.core.services.notifications.send_notification",
mocker.AsyncMock(),
)
await send_notification_in_background(
"chat-id",
["alice@example.com"],
["admin@example.com"],
"hello",
"settings_update",
)
create_task_mock.assert_called_once()
send_mock.assert_called_once_with(
"chat-id",
["alice@example.com"],
["admin@example.com"],
"hello",
"settings_update",
)
assert len(scheduled) == 1
@pytest.mark.anyio @pytest.mark.anyio
async def test_send_notification_uses_available_channels_and_swallows_exceptions( async def test_send_notification_uses_available_channels_and_swallows_exceptions(
settings: Settings, mocker: MockerFixture settings: Settings, mocker: MockerFixture
+10 -10
View File
@@ -2073,7 +2073,7 @@
{ {
"response_type": "json", "response_type": "json",
"response": { "response": {
"settled": true "state": "SETTLED"
} }
} }
] ]
@@ -2155,8 +2155,15 @@
] ]
}, },
"lndrest": { "lndrest": {
"description": "lndrest.py doesn't handle the 'failed' status for `get_invoice_status`", "get_invoice_status_endpoint": [
"get_invoice_status_endpoint": [] {
"description": "error status",
"response_type": "json",
"response": {
"state": "CANCELED"
}
}
]
}, },
"alby": { "alby": {
"description": "alby.py doesn't handle the 'failed' status for `get_invoice_status`", "description": "alby.py doesn't handle the 'failed' status for `get_invoice_status`",
@@ -2243,13 +2250,6 @@
"response_type": "json", "response_type": "json",
"response": {} "response": {}
}, },
{
"description": "error status",
"response_type": "json",
"response": {
"seetled": false
}
},
{ {
"description": "bad json", "description": "bad json",
"response_type": "data", "response_type": "data",
Generated
+109 -105
View File
@@ -3,7 +3,7 @@ revision = 3
requires-python = ">=3.10, <3.13" requires-python = ">=3.10, <3.13"
[options] [options]
exclude-newer = "2026-05-27T19:33:23.836682466Z" exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
exclude-newer-span = "P1W" exclude-newer-span = "P1W"
[[package]] [[package]]
@@ -17,7 +17,7 @@ wheels = [
[[package]] [[package]]
name = "aiohttp" name = "aiohttp"
version = "3.13.5" version = "3.14.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "aiohappyeyeballs" }, { name = "aiohappyeyeballs" },
@@ -27,61 +27,65 @@ dependencies = [
{ name = "frozenlist" }, { name = "frozenlist" },
{ name = "multidict" }, { name = "multidict" },
{ name = "propcache" }, { name = "propcache" },
{ name = "typing-extensions" },
{ name = "yarl" }, { name = "yarl" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/85/cebc47ee74d8b408749073a1a46c6fcba13d170dc8af7e61996c6c9394ac/aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b", size = 750547, upload-time = "2026-03-31T21:56:30.024Z" }, { url = "https://files.pythonhosted.org/packages/6d/67/58ded4b3f2e10f94972d8928050c85330e249a31dd45a0e5f3c0e9c3fa05/aiohttp-3.14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8f6bb621e5863cfe8fe5ff5468002d200ec31f30f1280b259dc505b02595099e", size = 766140, upload-time = "2026-06-07T21:05:37.471Z" },
{ url = "https://files.pythonhosted.org/packages/05/98/afd308e35b9d3d8c9ec54c0918f1d722c86dc17ddfec272fcdbcce5a3124/aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5", size = 503535, upload-time = "2026-03-31T21:56:31.935Z" }, { url = "https://files.pythonhosted.org/packages/18/68/4ae5b4e08943f316594bb68da89957d3baf5760588fa09509594bd777e4b/aiohttp-3.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f7215cb3933784f79ed20e5f050e15984f390424339b22375d5a53c933a0491", size = 519430, upload-time = "2026-06-07T21:05:40.751Z" },
{ url = "https://files.pythonhosted.org/packages/6f/4d/926c183e06b09d5270a309eb50fbde7b09782bfd305dec1e800f329834fb/aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670", size = 497830, upload-time = "2026-03-31T21:56:33.654Z" }, { url = "https://files.pythonhosted.org/packages/cb/c1/316c8f3549dbe5245f92bfd523ec6f32dd4d98cafe21df3f6a19b1184c75/aiohttp-3.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9d4e294455b23a68c9b8f042d0e8e377a265bcb15332753695f6e5b6819e0ce", size = 514406, upload-time = "2026-06-07T21:05:42.111Z" },
{ url = "https://files.pythonhosted.org/packages/e4/d6/f47d1c690f115a5c2a5e8938cce4a232a5be9aac5c5fb2647efcbbbda333/aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274", size = 1682474, upload-time = "2026-03-31T21:56:35.513Z" }, { url = "https://files.pythonhosted.org/packages/5a/ee/fb0ac28684e8d753b83c8a4eebc19a5846912aa0a4daaabb6a9936363840/aiohttp-3.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b238af795833d5731d049d82bc84b768ae6f8f97f0495963b3ed9935c5901cc3", size = 1703649, upload-time = "2026-06-07T21:05:43.427Z" },
{ url = "https://files.pythonhosted.org/packages/01/44/056fd37b1bb52eac760303e5196acc74d9d546631b035704ae5927f7b4ac/aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a", size = 1655259, upload-time = "2026-03-31T21:56:37.843Z" }, { url = "https://files.pythonhosted.org/packages/3b/57/aa2beab673331f111885db8a7b69dfe3ab0e53e446a0ace18ca694b4dc58/aiohttp-3.14.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4e5e0ae56914ecdbf446493addefc0159053dd53962cef37d7839f37f73d505", size = 1675126, upload-time = "2026-06-07T21:05:44.897Z" },
{ url = "https://files.pythonhosted.org/packages/91/9f/78eb1a20c1c28ae02f6a3c0f4d7b0dcc66abce5290cadd53d78ce3084175/aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d", size = 1736204, upload-time = "2026-03-31T21:56:39.822Z" }, { url = "https://files.pythonhosted.org/packages/47/ea/dad128abe365e79be03b16ed464198ac73e0d257e8260c6f7d6f31cbef26/aiohttp-3.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:092e4ce3619a7c6dee52a6bdabda973d9b34b66781f840ce93c7e0cec30cf521", size = 1771558, upload-time = "2026-06-07T21:05:46.405Z" },
{ url = "https://files.pythonhosted.org/packages/de/6c/d20d7de23f0b52b8c1d9e2033b2db1ac4dacbb470bb74c56de0f5f86bb4f/aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796", size = 1826198, upload-time = "2026-03-31T21:56:41.378Z" }, { url = "https://files.pythonhosted.org/packages/63/f3/b5b4e10327cb85d34d24232c6b71b64602f190b3ccb238a043ac6b187dac/aiohttp-3.14.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb33777ea21e8b7ecde0e6fc84f598be0a1192eab1a63bc746d75aa75d38e7bd", size = 1856631, upload-time = "2026-06-07T21:05:47.844Z" },
{ url = "https://files.pythonhosted.org/packages/2f/86/a6f3ff1fd795f49545a7c74b2c92f62729135d73e7e4055bf74da5a26c82/aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95", size = 1681329, upload-time = "2026-03-31T21:56:43.374Z" }, { url = "https://files.pythonhosted.org/packages/2b/9d/93294c3045775c708ac8310eb3d3622a11d2951345ad590d532d62a1faa4/aiohttp-3.14.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23119f8fd4f5d16902ed459b63b100bcd269628075162bddac56cc7b5273b3fb", size = 1714139, upload-time = "2026-06-07T21:05:49.982Z" },
{ url = "https://files.pythonhosted.org/packages/fb/68/84cd3dab6b7b4f3e6fe9459a961acb142aaab846417f6e8905110d7027e5/aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5", size = 1560023, upload-time = "2026-03-31T21:56:45.031Z" }, { url = "https://files.pythonhosted.org/packages/29/c4/93067c85a0373492ce8e577435203c5947c454af074ac48ed4f3a1b9dd4a/aiohttp-3.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57fc6745a4b7d0f5a9eb4f40a69718be6c0bc1b8368cc9fe89e90118719f4f42", size = 1588321, upload-time = "2026-06-07T21:05:51.431Z" },
{ url = "https://files.pythonhosted.org/packages/41/2c/db61b64b0249e30f954a65ab4cb4970ced57544b1de2e3c98ee5dc24165f/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a", size = 1652372, upload-time = "2026-03-31T21:56:47.075Z" }, { url = "https://files.pythonhosted.org/packages/c4/39/9ff91aaf02af8b7b8222a987466da539f154c3e01732c22b5f5a20a8ee66/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6fd35beba67c4183b09375c5fff9accb47524191a244a99f95fd4472f5402c2b", size = 1670375, upload-time = "2026-06-07T21:05:53.109Z" },
{ url = "https://files.pythonhosted.org/packages/25/6f/e96988a6c982d047810c772e28c43c64c300c943b0ed5c1c0c4ce1e1027c/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73", size = 1662031, upload-time = "2026-03-31T21:56:48.835Z" }, { url = "https://files.pythonhosted.org/packages/aa/e4/77452a3676b8d99ac1375f77691d6bf65ea6e9f4b201b82ef77c916dc767/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:672b9d65f42eb877f5c3f234a4547e4e1a226ca8c2eed879bb34670a0ce51192", size = 1690933, upload-time = "2026-06-07T21:05:54.902Z" },
{ url = "https://files.pythonhosted.org/packages/b7/26/a56feace81f3d347b4052403a9d03754a0ab23f7940780dada0849a38c92/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297", size = 1708118, upload-time = "2026-03-31T21:56:50.833Z" }, { url = "https://files.pythonhosted.org/packages/7d/84/b0059a7c7fc05ea23f3bc1596ba91c12f79588b9450564a24cac37536d0a/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:24ba13339fed9251d9b1a1bec8c7ab84c0d1675d79d33501e11f94f8b9a84e05", size = 1740798, upload-time = "2026-06-07T21:05:56.458Z" },
{ url = "https://files.pythonhosted.org/packages/78/6e/b6173a8ff03d01d5e1a694bc06764b5dad1df2d4ed8f0ceec12bb3277936/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074", size = 1548667, upload-time = "2026-03-31T21:56:52.81Z" }, { url = "https://files.pythonhosted.org/packages/8f/3a/e2a513ecbfc362591caa51a7f7e011b3bfc8938b388ae44cd95560d36999/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:94da27378da0610e341c4d30de29a191672683cc82b8f9556e8f7c7212a020fe", size = 1576412, upload-time = "2026-06-07T21:05:57.953Z" },
{ url = "https://files.pythonhosted.org/packages/16/13/13296ffe2c132d888b3fe2c195c8b9c0c24c89c3fa5cc2c44464dc23b22e/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e", size = 1724490, upload-time = "2026-03-31T21:56:54.541Z" }, { url = "https://files.pythonhosted.org/packages/a1/10/08f1654f538f93d36dcac66310a06eefce4641cdafca83f9f0a5317be254/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52cdac9432d8b4a719f35094a818d95adcae0f0b4fe9b9b921909e0c87de9e7d", size = 1750199, upload-time = "2026-06-07T21:05:59.488Z" },
{ url = "https://files.pythonhosted.org/packages/7a/b4/1f1c287f4a79782ef36e5a6e62954c85343bc30470d862d30bd5f26c9fa2/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7", size = 1667109, upload-time = "2026-03-31T21:56:56.21Z" }, { url = "https://files.pythonhosted.org/packages/99/e4/d91b70c57d8b8e9611e4a2e52238ca3698d3dc1c2efe25b7a9bf594ac584/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:672ac254412a24d0d0cf00a9e6c238877e4be5e5fa2d188832c1244f45f31966", size = 1699356, upload-time = "2026-06-07T21:06:01.131Z" },
{ url = "https://files.pythonhosted.org/packages/ef/42/8461a2aaf60a8f4ea4549a4056be36b904b0eb03d97ca9a8a2604681a500/aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9", size = 439478, upload-time = "2026-03-31T21:56:58.292Z" }, { url = "https://files.pythonhosted.org/packages/3d/f1/15340176f35ff61b95dbe34020bcf43f9e624a2d7bbac934715ff97d2033/aiohttp-3.14.1-cp310-cp310-win32.whl", hash = "sha256:2fe3607e71acc6ebb0ec8e492a247bf7a291226192dc0084236dfc12478916f6", size = 458939, upload-time = "2026-06-07T21:06:02.86Z" },
{ url = "https://files.pythonhosted.org/packages/e5/71/06956304cb5ee439dfe8d86e1b2e70088bd88ed1ced1f42fb29e5d855f0e/aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76", size = 462047, upload-time = "2026-03-31T21:57:00.257Z" }, { url = "https://files.pythonhosted.org/packages/c3/c2/a2f1ec5b37f903109e43ae2862268cfe4a67a60c1b2cf43169fcdff5995f/aiohttp-3.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:30099eda75a53c32efb0920e9c33c195314d2cc1c680fbfd30894932ac5f27df", size = 482583, upload-time = "2026-06-07T21:06:04.666Z" },
{ url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" }, { url = "https://files.pythonhosted.org/packages/d0/7a/7b56f6732ef79530afaa72aa335d41b67c8d79b946995f0b11ad72985435/aiohttp-3.14.1-cp310-cp310-win_arm64.whl", hash = "sha256:5a837f49d901f9e368651b676912bff1104ed8c1a83b280bcd7b29adccef5c9c", size = 453470, upload-time = "2026-06-07T21:06:06.322Z" },
{ url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" }, { url = "https://files.pythonhosted.org/packages/26/dd/bf526e6f0a1120dd6f2df2e97bacfe4d358f13d17a0ff5847301a1375a51/aiohttp-3.14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2", size = 765225, upload-time = "2026-06-07T21:06:07.957Z" },
{ url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" }, { url = "https://files.pythonhosted.org/packages/8f/e1/a2872aa55495a70f61310d411541c6ee23812d9a884e000c716e1bc3edbf/aiohttp-3.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f", size = 518743, upload-time = "2026-06-07T21:06:09.749Z" },
{ url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" }, { url = "https://files.pythonhosted.org/packages/5b/e7/c60c7b209e509cc787de3cea0550a518538cfc08003e1c1e14c1c63fff71/aiohttp-3.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8", size = 514139, upload-time = "2026-06-07T21:06:11.26Z" },
{ url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" }, { url = "https://files.pythonhosted.org/packages/5b/8d/614ace2f579702c9840ab1e1447fd8509e35b0b904f7196418fa2f57b25d/aiohttp-3.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04", size = 1784088, upload-time = "2026-06-07T21:06:12.887Z" },
{ url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" }, { url = "https://files.pythonhosted.org/packages/49/e0/726e90f99542bf292f81a96a12cc4847deb86f3ccf62c6f4014a201f4d33/aiohttp-3.14.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8", size = 1737835, upload-time = "2026-06-07T21:06:14.564Z" },
{ url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" }, { url = "https://files.pythonhosted.org/packages/0b/4b/d176d5c4db9d33dacf0543102ea59503bc1d528af4cfd0b719949ca49389/aiohttp-3.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6", size = 1842801, upload-time = "2026-06-07T21:06:16.228Z" },
{ url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" }, { url = "https://files.pythonhosted.org/packages/dc/d6/5a99b563690ea0cbed912ae94a2ce33993a5709a651a3a4fe761e7dd973a/aiohttp-3.14.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af", size = 1929992, upload-time = "2026-06-07T21:06:17.947Z" },
{ url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" }, { url = "https://files.pythonhosted.org/packages/76/7f/a987b14a3859094b3cea3f4825219c3e5536242564af6e3f9c2f6c994eb2/aiohttp-3.14.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730", size = 1786989, upload-time = "2026-06-07T21:06:19.677Z" },
{ url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" }, { url = "https://files.pythonhosted.org/packages/f1/1a/420e5c85a3e73349372ed22ce0b6af86bfa6ce16a4b20a64a2e94608c781/aiohttp-3.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621", size = 1640129, upload-time = "2026-06-07T21:06:22.558Z" },
{ url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" }, { url = "https://files.pythonhosted.org/packages/a7/80/18a592ed3be0a402cc03670bd72ee1f8563ddbe1d8d5542dbf868f274136/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee", size = 1756576, upload-time = "2026-06-07T21:06:24.8Z" },
{ url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" }, { url = "https://files.pythonhosted.org/packages/ec/0b/8b3d5713373858ff71a617daf6e3b0e81ad63e79d09a3cf2f6b6b983939c/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573", size = 1754668, upload-time = "2026-06-07T21:06:26.528Z" },
{ url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" }, { url = "https://files.pythonhosted.org/packages/9f/49/fd564575cf225821d7ba5a117cb8bc27213d8a7e1811162afb43ae077039/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7", size = 1817019, upload-time = "2026-06-07T21:06:28.297Z" },
{ url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" }, { url = "https://files.pythonhosted.org/packages/ed/1b/e850c9ae6fc91356552ae668bb6c51e93fa29c8aef13398a10b56678557f/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf", size = 1631638, upload-time = "2026-06-07T21:06:30.242Z" },
{ url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" }, { url = "https://files.pythonhosted.org/packages/eb/94/3c337ba72451a89806ace6f75bddc92bafc5b8d53d90115a512858024b63/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85", size = 1835660, upload-time = "2026-06-07T21:06:31.943Z" },
{ url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" }, { url = "https://files.pythonhosted.org/packages/2b/9c/9c18cf367a0498212d9ba7daf990b504a5e8ae064cda4b504e2647c89c03/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3", size = 1775698, upload-time = "2026-06-07T21:06:33.72Z" },
{ url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" }, { url = "https://files.pythonhosted.org/packages/b5/63/a251a9d2a6cb45065b2ddc0bde2b3dd10108740a9a42f632c66405a761a2/aiohttp-3.14.1-cp311-cp311-win32.whl", hash = "sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126", size = 458386, upload-time = "2026-06-07T21:06:35.279Z" },
{ url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, { url = "https://files.pythonhosted.org/packages/17/ca/69274c51dcd6e8947d77b2806cf47a4a15f2c846e2cbeb1882547d3da283/aiohttp-3.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5", size = 483406, upload-time = "2026-06-07T21:06:36.824Z" },
{ url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, { url = "https://files.pythonhosted.org/packages/2c/8a/c25904f77690c3688ec140f87591ef11a0cfe36bf3d5c0f1f38056fb62b3/aiohttp-3.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b", size = 452987, upload-time = "2026-06-07T21:06:38.371Z" },
{ url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, { url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402, upload-time = "2026-06-07T21:06:40.311Z" },
{ url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, { url = "https://files.pythonhosted.org/packages/c2/82/280619e0bd7bf2454987e19282616e84762255dd9c8468f62382e8c191f1/aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", size = 512310, upload-time = "2026-06-07T21:06:42.207Z" },
{ url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, { url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448, upload-time = "2026-06-07T21:06:43.813Z" },
{ url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, { url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854, upload-time = "2026-06-07T21:06:45.391Z" },
{ url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, { url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884, upload-time = "2026-06-07T21:06:47.413Z" },
{ url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, { url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034, upload-time = "2026-06-07T21:06:50.165Z" },
{ url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, { url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054, upload-time = "2026-06-07T21:06:52.035Z" },
{ url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, { url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278, upload-time = "2026-06-07T21:06:54.049Z" },
{ url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, { url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795, upload-time = "2026-06-07T21:06:55.911Z" },
{ url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, { url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397, upload-time = "2026-06-07T21:06:57.777Z" },
{ url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, { url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504, upload-time = "2026-06-07T21:07:00.009Z" },
{ url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, { url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806, upload-time = "2026-06-07T21:07:02.064Z" },
{ url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, { url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707, upload-time = "2026-06-07T21:07:03.942Z" },
{ url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, { url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121, upload-time = "2026-06-07T21:07:05.987Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, { url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580, upload-time = "2026-06-07T21:07:07.873Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/a0c0a8f327a9c52095cdd8e312391b00d3ed64ab6c72bb5c33d8ec251cf7/aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", size = 452771, upload-time = "2026-06-07T21:07:09.669Z" },
{ url = "https://files.pythonhosted.org/packages/df/d9/ea367c75f16ac9c6cdc8febb25e8318fa21a2b1bc8d6514d4b2d890bface/aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", size = 479873, upload-time = "2026-06-07T21:07:11.538Z" },
{ url = "https://files.pythonhosted.org/packages/03/64/8d96784a7851156db8a4c6c3f6f91042fdf39fb15a4cc38c8b3c14833c45/aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", size = 448073, upload-time = "2026-06-07T21:07:13.637Z" },
] ]
[[package]] [[package]]
@@ -684,48 +688,48 @@ toml = [
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "48.0.0" version = "48.0.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, { url = "https://files.pythonhosted.org/packages/1b/bc/ee4137cbbe105652c0ee4252792b78fc8e7afa4b8e61d9d5dc05a7f45731/cryptography-48.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1", size = 8008324, upload-time = "2026-06-09T22:31:00.702Z" },
{ url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, { url = "https://files.pythonhosted.org/packages/d5/85/6379d42181bfc713094f081360fc5784d6c816b599d45e7f082502d173ce/cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225", size = 4696243, upload-time = "2026-06-09T22:32:33.446Z" },
{ url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, { url = "https://files.pythonhosted.org/packages/9c/87/c85d147b53323c7eb4d850920c8901377323c2a0ff8d79c262d4fee89aa2/cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691", size = 4713235, upload-time = "2026-06-09T22:31:40.141Z" },
{ url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, { url = "https://files.pythonhosted.org/packages/79/58/67cbf8cf1ee7c54b439ca07bbecf8362c07afc11a3724fea70f745784add/cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242", size = 4702323, upload-time = "2026-06-09T22:31:42.191Z" },
{ url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, { url = "https://files.pythonhosted.org/packages/89/c6/24266ac10c47f6cd2a865f4446062b466da1d1f10b27189eac00e61bf0c9/cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08", size = 5300085, upload-time = "2026-06-09T22:31:58.703Z" },
{ url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, { url = "https://files.pythonhosted.org/packages/d2/bb/cc4b78784f97efc8c5874c2a9743708d172be6663024b34a0467885ae0c8/cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6", size = 4746137, upload-time = "2026-06-09T22:31:31.01Z" },
{ url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, { url = "https://files.pythonhosted.org/packages/1f/52/0c44de3f5267f8fbe8e835138017522a333436166e406f0db9b9e6e3033f/cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8", size = 4333867, upload-time = "2026-06-09T22:32:28.096Z" },
{ url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, { url = "https://files.pythonhosted.org/packages/9a/2e/772d7adbfa931537bc401640b7cac9976bff689bda187833e5d63b428e49/cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429", size = 4701805, upload-time = "2026-06-09T22:31:38.284Z" },
{ url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, { url = "https://files.pythonhosted.org/packages/f8/a3/b06844f303873493c963caf581c04df31c7035e0c1b0f02c4814d319ec80/cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f", size = 5258461, upload-time = "2026-06-09T22:31:04.187Z" },
{ url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, { url = "https://files.pythonhosted.org/packages/9f/13/8b765e2e12b07c74941caadb9d1c8fdc006c4dfbf2b8f2d610519758954d/cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f", size = 4745488, upload-time = "2026-06-09T22:32:30.07Z" },
{ url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, { url = "https://files.pythonhosted.org/packages/2e/aa/48972bce55049b32a94f4907eda4d75fa385aad8a39506cc2fc72196ecf0/cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41", size = 4830256, upload-time = "2026-06-09T22:31:43.868Z" },
{ url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, { url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117, upload-time = "2026-06-09T22:31:05.845Z" },
{ url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, { url = "https://files.pythonhosted.org/packages/b7/a0/8f50cae9c74e718ed769d63ed5c74bd0ea830c9550a74629cebd1b9c7bc7/cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158", size = 3304154, upload-time = "2026-06-09T22:32:16.845Z" },
{ url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, { url = "https://files.pythonhosted.org/packages/c5/69/0572c77dbace6fef72f33755bd52ea399c71367250d366237f8691826b9e/cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24", size = 3817138, upload-time = "2026-06-09T22:32:00.388Z" },
{ url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, { url = "https://files.pythonhosted.org/packages/ca/6c/00fa2a95997164c8b2072ce327c23d4ab20809ccc323ea5fab91e53a4bba/cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67", size = 7987408, upload-time = "2026-06-09T22:32:20.777Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, { url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196, upload-time = "2026-06-09T22:31:54.74Z" },
{ url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, { url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782, upload-time = "2026-06-09T22:31:45.615Z" },
{ url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, { url = "https://files.pythonhosted.org/packages/66/c2/81a4fb4e4373c500bb526bc337ac5719dd31dd15b970b84a238168c6aa08/cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577", size = 4696618, upload-time = "2026-06-09T22:31:11.564Z" },
{ url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, { url = "https://files.pythonhosted.org/packages/e5/0b/aa68b221dde92d09cb29a024ede17550ee21e77a404e59fc093c82bb51e1/cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1", size = 5289970, upload-time = "2026-06-09T22:31:20.368Z" },
{ url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, { url = "https://files.pythonhosted.org/packages/78/13/fba657f958d2af66ea959a4ba01212632089249d34af1ae48054136344d7/cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d", size = 4731873, upload-time = "2026-06-09T22:31:22.253Z" },
{ url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, { url = "https://files.pythonhosted.org/packages/4c/4c/9a964756d24a26b3e34dfcb16f961b89838786e6700b635b0d1e3adff4b6/cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6", size = 4330804, upload-time = "2026-06-09T22:31:36.56Z" },
{ url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, { url = "https://files.pythonhosted.org/packages/4b/0f/a10f3a6eb12950a10e3a874070283aa2dd5875b2bfd15fad8a3e17b3f13e/cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46", size = 4696217, upload-time = "2026-06-09T22:31:13.351Z" },
{ url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, { url = "https://files.pythonhosted.org/packages/f3/6f/5cd12f951165ea73ef85266775d97e4c763b2474ccfd816dd69d3a18d6f8/cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401", size = 5245252, upload-time = "2026-06-09T22:32:02.193Z" },
{ url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, { url = "https://files.pythonhosted.org/packages/68/ab/8aaa12e4516ec4464033ab79b6f3b592bd5a92102467c4ace8a0d970203f/cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b", size = 4731388, upload-time = "2026-06-09T22:32:04.019Z" },
{ url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, { url = "https://files.pythonhosted.org/packages/1b/24/50027ea4dca85ec1f40688f3c24fb32ccacd520583c9592c3cc95628e6fb/cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1", size = 4824186, upload-time = "2026-06-09T22:32:18.707Z" },
{ url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, { url = "https://files.pythonhosted.org/packages/52/41/04cb5eb17085ade6f50cc611fb657df6a0f5885350de8764ece89c050197/cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475", size = 4964539, upload-time = "2026-06-09T22:31:18.793Z" },
{ url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, { url = "https://files.pythonhosted.org/packages/36/bf/ed70785c496e89d7e73b7cda2d21f2447fd6d4e821714b8d04ff217fed92/cryptography-48.0.1-cp39-abi3-win32.whl", hash = "sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1", size = 3282307, upload-time = "2026-06-09T22:30:53.162Z" },
{ url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, { url = "https://files.pythonhosted.org/packages/b3/ff/371ea7d252656ee1eb6d83eeeef3d1d0c6baf1d6497687d081ea03814670/cryptography-48.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac", size = 3793408, upload-time = "2026-06-09T22:32:15.191Z" },
{ url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, { url = "https://files.pythonhosted.org/packages/a9/d3/eb4e394e587341fdad09a09101fa76478ead3a78b0ad63e55c22f0d75c02/cryptography-48.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a", size = 3951747, upload-time = "2026-06-09T22:31:23.871Z" },
{ url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, { url = "https://files.pythonhosted.org/packages/e0/4a/3f43451b4f858bfceaaaffc649e6e787e8d4fb332a1d443af39ab02cc8f1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd", size = 4641226, upload-time = "2026-06-09T22:31:02.532Z" },
{ url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, { url = "https://files.pythonhosted.org/packages/73/4e/855584c2c23b09e4ce2d3b9c30e983e679cd60b068c513c6bbdb91e11782/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c", size = 4668958, upload-time = "2026-06-09T22:32:06.213Z" },
{ url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, { url = "https://files.pythonhosted.org/packages/42/3b/d35750e41d803d1e516fd6d6011f065424924da7af1748cef4cc9cb3ede1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9", size = 4640793, upload-time = "2026-06-09T22:32:26.331Z" },
{ url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, { url = "https://files.pythonhosted.org/packages/ca/aa/cdb7181fe865285e87e96825aaab239400f1de0c3bfba9bd9769b79f1a92/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92", size = 4668505, upload-time = "2026-06-09T22:31:27.534Z" },
{ url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, { url = "https://files.pythonhosted.org/packages/5d/8c/ce3823c06c2804f194f9e64f0d67fa3f4094a39f2bb1a990cd03603af8fc/cryptography-48.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6184ca7b174f28d7c703f1290d4b297217c45355f77a98f67e9b7f14549ac54a", size = 3742204, upload-time = "2026-06-09T22:31:34.773Z" },
] ]
[[package]] [[package]]
@@ -1284,7 +1288,7 @@ wheels = [
[[package]] [[package]]
name = "lnbits" name = "lnbits"
version = "1.5.5rc2" version = "1.5.5rc3"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiosqlite" }, { name = "aiosqlite" },
@@ -2278,11 +2282,11 @@ wheels = [
[[package]] [[package]]
name = "python-multipart" name = "python-multipart"
version = "0.0.29" version = "0.0.31"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4e/fe/70bd71a6738b09a0bdf6480ca6436b167469ca4578b2a0efbe390b4b0e70/python_multipart-0.0.29.tar.gz", hash = "sha256:643e93849196645e2dbdd81a0f8829a23123ad7f797a84a364c6fb3563f18904", size = 45678, upload-time = "2026-05-17T17:29:47.654Z" } sdist = { url = "https://files.pythonhosted.org/packages/64/7e/9b35ad8f3d9ca680f7c87a88f19612fdd8da9796c4d3b46e560ac79dcc4a/python_multipart-0.0.31.tar.gz", hash = "sha256:fc631183bb13e56db3158a4909908dfb2e23565286744e798241e63750e5d680", size = 46689, upload-time = "2026-06-04T08:27:49.014Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/cb/769cfc37177252872a45a71f3fbdde9d51b471a3f3c14bfe95dde3407386/python_multipart-0.0.29-py3-none-any.whl", hash = "sha256:2ddcc971cef266225f54f552d8fa10bcfbb1f14446caec199060daac59ff2d69", size = 29640, upload-time = "2026-05-17T17:29:45.69Z" }, { url = "https://files.pythonhosted.org/packages/5e/1e/7f7f299527a5a8ad90acd5f2f78dfa6c8495c6301a3205106ea68a84de96/python_multipart-0.0.31-py3-none-any.whl", hash = "sha256:8408153d68a9773291fc1da39a8b85a50044bddbabd2dd72e9229776b7b15e28", size = 29996, upload-time = "2026-06-04T08:27:47.804Z" },
] ]
[[package]] [[package]]
@@ -2682,19 +2686,19 @@ wheels = [
[[package]] [[package]]
name = "tornado" name = "tornado"
version = "6.5.6" version = "6.5.7"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/50/57/6d7303a77ae439d9189108f76c0c4fd89ee5e2cc8387bffb55232565c4ed/tornado-6.5.6.tar.gz", hash = "sha256:9a365179fe8ff6b8766f602c0f67c185d778193e9bdd828b19f0b6ed7764177d", size = 518139, upload-time = "2026-05-27T15:35:54.646Z" } sdist = { url = "https://files.pythonhosted.org/packages/64/24/95ec527ad67b76d59299e5465b3935d05e4294b7e0290a3924b7487df30b/tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2", size = 519252, upload-time = "2026-06-08T17:34:51.232Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/0d/b4f481e18c5a51864e6d12b9a05ecf72919696680b747c958c3fc1f4fbae/tornado-6.5.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:65fcfaafb079435c2c19dc9e07c0f1cf0fa9051759ed0a7d0a3ba7ea7f64919c", size = 447737, upload-time = "2026-05-27T15:35:38.122Z" }, { url = "https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163", size = 448543, upload-time = "2026-06-08T17:34:38.052Z" },
{ url = "https://files.pythonhosted.org/packages/9e/9c/5430c39fcab1144d35860f457b15e9c08b4bc7ac86764354204e983d6183/tornado-6.5.6-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:38bc01b4acacded2de63ae78023548e41ebe6fbed3ec05a796d7ae3ad893887e", size = 445899, upload-time = "2026-05-27T15:35:40.519Z" }, { url = "https://files.pythonhosted.org/packages/92/4f/090b1431e5a43df696feceffc268c5383cc079ecb5f08ce58f917109aafe/tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100", size = 446707, upload-time = "2026-06-08T17:34:39.594Z" },
{ url = "https://files.pythonhosted.org/packages/8b/79/fa7e14a2f939c807a8d30619b4eb604eab219601b78792516ebe22d40cf9/tornado-6.5.6-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b942e6a137fda31ff54bf8e6e2c8d1c37f1f50583f3ed53fb840b53b9601d104", size = 448964, upload-time = "2026-05-27T15:35:42.106Z" }, { url = "https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972", size = 449774, upload-time = "2026-06-08T17:34:41.204Z" },
{ url = "https://files.pythonhosted.org/packages/a7/71/bd67d5f5199f937dafe03a49a37989f60f600ff6fef34c79412a829d97bd/tornado-6.5.6-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8666946e70171b8c3f1fc9b7876fac492e84822c4c7f3746f4e8f8bc9ac92a79", size = 449935, upload-time = "2026-05-27T15:35:43.906Z" }, { url = "https://files.pythonhosted.org/packages/35/37/d434c73f4c6e014b745b9b37085f34f40c022f007efff3d7fe65991899f3/tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b", size = 450745, upload-time = "2026-06-08T17:34:42.531Z" },
{ url = "https://files.pythonhosted.org/packages/cc/a4/c24388c9cf5b3c3a513b56a158af9f23092c9a2810d789e294310797df21/tornado-6.5.6-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c34cfab7ad6d104f052f55de06d39bbafc5885cfeb4da688803308dbcfa90b7", size = 449767, upload-time = "2026-05-27T15:35:45.793Z" }, { url = "https://files.pythonhosted.org/packages/b6/2b/56b9aff361d7f1ab728a805ec7d7ea835f8807afa9f5cc690ea0e630efb9/tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92", size = 450578, upload-time = "2026-06-08T17:34:43.787Z" },
{ url = "https://files.pythonhosted.org/packages/a5/eb/6a07ad550c3f7b37244bd0becdf293ec3d3e961783d8b720a97df50de1b2/tornado-6.5.6-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:385f35e4e22fb52551dfcda4cdc8c30c61c2c001aef5ddad99cdfe116952efd3", size = 449174, upload-time = "2026-05-27T15:35:47.485Z" }, { url = "https://files.pythonhosted.org/packages/02/30/a7444fb23aa76860a14198fab96ac79f1866b0a6e19e26c4381b0938e50f/tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5", size = 449985, upload-time = "2026-06-08T17:34:45.326Z" },
{ url = "https://files.pythonhosted.org/packages/bb/84/3469e098dccdb6763130e06aacd786bb4363fca7b590a55c101ddf34ed30/tornado-6.5.6-cp39-abi3-win32.whl", hash = "sha256:db475f1b67b2809b10bb16264829087724ca8d24fe4ed47f7b8675cae453ef86", size = 450230, upload-time = "2026-05-27T15:35:49.322Z" }, { url = "https://files.pythonhosted.org/packages/5c/42/5f0e56c01e8d9d36f4e23f367b85ae6cae0c1ecddd5e6977d8388ad27488/tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4", size = 451047, upload-time = "2026-06-08T17:34:46.784Z" },
{ url = "https://files.pythonhosted.org/packages/d2/3c/273a04e0b9dd9016f1685cca0c1c8795a71ac88a34a8c889a0b443483226/tornado-6.5.6-cp39-abi3-win_amd64.whl", hash = "sha256:6739bf1e8eb09230f1280ddbd3236f0309db70f2c551a8dbc40f62babdf82f79", size = 450667, upload-time = "2026-05-27T15:35:51.194Z" }, { url = "https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4", size = 451485, upload-time = "2026-06-08T17:34:48.248Z" },
{ url = "https://files.pythonhosted.org/packages/02/98/0cffe22a224f60c5fb1e3aa0b76f9da2e1ca78b0e9545e3d077c68ce60a7/tornado-6.5.6-cp39-abi3-win_arm64.whl", hash = "sha256:2543597b24a695d72338a9a77818362d72387c03ae173f1f169eadc5c91466ac", size = 449690, upload-time = "2026-05-27T15:35:52.902Z" }, { url = "https://files.pythonhosted.org/packages/71/2e/7b1c769803121b809112cf9a00681c472eae1d80e32d7ec0e0bd61d0d0e1/tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796", size = 450506, upload-time = "2026-06-08T17:34:49.702Z" },
] ]
[[package]] [[package]]