Compare commits

...
9 Commits
Author SHA1 Message Date
Vlad Stan d5783b4291 chore: bundle 2026-05-25 15:52:29 +03:00
Vlad Stan da8fba8a9b chore: code check 2026-05-25 15:50:46 +03:00
Vlad Stan 7f73da1f22 chore: update deps 2026-05-25 15:04:34 +03:00
ArcandVlad Stan 939a7f242b cleanup 2026-05-25 14:56:22 +03:00
ArcandVlad Stan 18c31280ef cleanup 2026-05-25 14:56:20 +03:00
ArcandVlad Stan 7d20c81ff9 Hopefully this didnt change too much 2026-05-25 14:56:18 +03:00
arcbtcandVlad Stan 46a3a24ce4 make
add wasmtime

bundle

mypy fix

mypy

mypy

pyright

pyright

pyright

make

lock file
2026-05-25 14:56:15 +03:00
ArcandVlad Stan 46bf984c05 tweaks 2026-05-25 14:56:04 +03:00
ArcandVlad Stan f3dd667f4a minimal 2026-05-25 14:56:04 +03:00
12 changed files with 545 additions and 1485 deletions
+40 -1
View File
@@ -24,7 +24,7 @@ from lnbits.core.crud import (
update_installed_extension_state,
)
from lnbits.core.crud.extensions import create_installed_extension
from lnbits.core.helpers import migrate_extension_database
from lnbits.core.helpers import get_extension_type, migrate_extension_database
from lnbits.core.models.notifications import NotificationType
from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions
from lnbits.core.services.notifications import enqueue_admin_notification
@@ -423,6 +423,8 @@ def register_new_ratelimiter(app: FastAPI) -> Callable:
def register_ext_tasks(ext: Extension) -> None:
"""Register extension async tasks."""
if ext.extension_type == "wasm":
return
ext_module = importlib.import_module(ext.module_name)
if hasattr(ext_module, f"{ext.code}_start"):
@@ -432,6 +434,43 @@ def register_ext_tasks(ext: Extension) -> None:
def register_ext_routes(app: FastAPI, ext: Extension) -> None:
"""Register FastAPI routes for extension."""
if ext.extension_type != "wasm":
ext.extension_type = get_extension_type(ext.code) or ext.extension_type
if ext.extension_type == "wasm":
settings.activate_extension_paths(ext.code, ext.upgrade_hash, [])
try:
register_wasm_ext_routes = None
for module_name in (
"wasm.wasm_host.extension_host",
"lnbits.extensions.wasm.wasm_host.extension_host",
):
try:
module = importlib.import_module(module_name)
except Exception:
logger.warning(
f"Could not import module {module_name} for wasm extension "
f"{ext.code}."
)
continue
register_wasm_ext_routes = getattr(
module, "register_wasm_ext_routes", None
)
if register_wasm_ext_routes is not None:
break
except Exception: # pragma: no cover - optional parent extension
logger.error(
"WASM host extension not installed; cannot register wasm extension "
f"{ext.code}."
)
return
if register_wasm_ext_routes is None:
logger.error(
"WASM host extension missing register_wasm_ext_routes; cannot "
f"register wasm extension {ext.code}."
)
return
register_wasm_ext_routes(app, ext)
return
ext_module = importlib.import_module(ext.module_name)
ext_route = getattr(ext_module, f"{ext.code}_ext")
+61
View File
@@ -1,5 +1,7 @@
import importlib
import json
import re
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
from uuid import UUID
@@ -22,6 +24,8 @@ from lnbits.settings import settings
async def migrate_extension_database(
ext: InstallableExtension, current_version: DbVersion | None = None
):
if _is_wasm_extension(ext):
return
try:
ext_migrations = importlib.import_module(f"{ext.module_name}.migrations")
@@ -34,6 +38,63 @@ async def migrate_extension_database(
await run_migration(ext_conn, ext_migrations, ext.id, current_version)
def get_extension_type(ext_id: str, ext_dir: Path | None = None) -> str | None:
candidate_dirs = []
if ext_dir:
candidate_dirs.append(ext_dir)
candidate_dirs.extend(
[
Path(settings.lnbits_extensions_path, "extensions", ext_id),
Path(settings.lnbits_extensions_path, ext_id),
Path(settings.lnbits_path, "lnbits", "extensions", ext_id),
Path(settings.lnbits_path, "extensions", ext_id),
Path.cwd() / "lnbits" / "extensions" / ext_id,
Path.cwd() / "extensions" / ext_id,
]
)
for base in candidate_dirs:
try:
conf_path = base / "config.json"
if not conf_path.is_file():
continue
with open(conf_path) as json_file:
config_json = json.load(json_file)
ext_type = config_json.get("extension_type")
if ext_type:
return ext_type
except Exception as exc:
logger.debug(
"Failed to read extension config.json for '{}' in '{}': {}",
ext_id,
base,
exc,
)
for base in candidate_dirs:
try:
wasm_dir = base / "wasm"
if (wasm_dir / "module.wasm").is_file() or (
wasm_dir / "module.wat"
).is_file():
return "wasm"
except Exception as exc:
logger.debug(
"Failed to probe wasm files for '{}' in '{}': {}",
ext_id,
base,
exc,
)
return None
def _is_wasm_extension(ext: InstallableExtension) -> bool:
if ext.meta and getattr(ext.meta, "extension_type", None) == "wasm":
return True
return get_extension_type(ext.id, ext.ext_dir) == "wasm"
async def run_migration(
db: Connection,
migrations_module: Any,
+3
View File
@@ -115,6 +115,8 @@ class PayToEnableInfo(BaseModel):
class UserExtensionInfo(BaseModel):
paid_to_enable: bool | None = False
payment_hash_to_enable: str | None = None
granted_permissions: list[str] | None = None
granted_payment_tags: list[str] | None = None
class UserExtension(BaseModel):
@@ -147,6 +149,7 @@ class Extension(BaseModel):
short_description: str | None = None
tile: str | None = None
upgrade_hash: str | None = ""
extension_type: str | None = None
@property
def module_name(self) -> str:
+53
View File
@@ -1,6 +1,8 @@
import json
import sys
import traceback
from http import HTTPStatus
from pathlib import Path
import httpx
from bolt11 import decode as bolt11_decode
@@ -11,6 +13,7 @@ from loguru import logger
from lnbits.core.crud.extensions import get_user_extensions
from lnbits.core.crud.wallets import get_wallets_ids
from lnbits.core.db import db
from lnbits.core.helpers import get_extension_type
from lnbits.core.models import (
SimpleStatus,
)
@@ -67,6 +70,53 @@ extension_router = APIRouter(
)
def _load_wasm_extension_config(ext_id: str) -> dict:
candidate_dirs = [
Path(settings.lnbits_extensions_path, "extensions", ext_id),
Path(settings.lnbits_path, "lnbits", "extensions", ext_id),
]
for base in candidate_dirs:
conf_path = base / "config.json"
if not conf_path.is_file():
continue
try:
with open(conf_path) as json_file:
config = json.load(json_file)
return config if isinstance(config, dict) else {}
except Exception:
return {}
return {}
def _ensure_wasm_permissions_saved(ext_id: str, user_ext: UserExtension) -> None:
if get_extension_type(ext_id) != "wasm":
return
config = _load_wasm_extension_config(ext_id)
required = [
permission.get("id")
for permission in config.get("permissions", [])
if isinstance(permission, dict) and permission.get("id")
]
granted = user_ext.extra.granted_permissions if user_ext.extra else []
missing = [
permission for permission in required if permission not in (granted or [])
]
if missing:
raise HTTPException(
HTTPStatus.BAD_REQUEST,
"Save WASM permissions before enabling this extension.",
)
payment_tags = config.get("payment_tags", [])
granted_tags = user_ext.extra.granted_payment_tags if user_ext.extra else []
has_granted_payment_tag = any(tag in (granted_tags or []) for tag in payment_tags)
if payment_tags and not has_granted_payment_tag:
raise HTTPException(
HTTPStatus.BAD_REQUEST,
"Select at least one WASM payment tag before enabling this extension.",
)
@extension_router.post("", dependencies=[Depends(check_admin)])
async def api_install_extension(data: CreateExtension):
release = await InstallableExtension.get_extension_release(
@@ -197,6 +247,8 @@ async def api_enable_extension(
user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False)
await create_user_extension(user_ext)
_ensure_wasm_permissions_saved(ext_id, user_ext)
if account_id.is_admin_id or not ext.requires_payment:
user_ext.active = True
await update_user_extension(user_ext)
@@ -596,6 +648,7 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
"isPaymentRequired": ext.requires_payment,
"inProgress": False,
"selectedForUpdate": False,
"extensionType": get_extension_type(ext.id) or "python",
}
for ext in installable_exts
]
File diff suppressed because one or more lines are too long
+157
View File
@@ -28,6 +28,14 @@ window.PageExtensions = {
paylinkWebsocket: null,
searchToggle: false,
reviewsUrl: null,
permissionsDialog: {
show: false,
extension: null,
checked: [],
missing: [],
tags: [],
tagOptions: []
},
reviewsDialog: {
show: false,
extension: null,
@@ -92,6 +100,20 @@ window.PageExtensions = {
this.filterExtensions(this.searchTerm, val)
}
},
computed: {
permissionsAllChecked() {
const ext = this.permissionsDialog.extension
if (!ext || !Array.isArray(ext.permissions)) return true
const required = ext.permissions.map(p => p.id)
return required.every(p => this.permissionsDialog.checked.includes(p))
},
permissionsHasMissingEndpoints() {
return (
this.permissionsDialog.missing &&
this.permissionsDialog.missing.length > 0
)
}
},
methods: {
filterExtensions(term, tab) {
// Filter the extensions list
@@ -240,7 +262,81 @@ window.PageExtensions = {
extension.inProgress = false
})
},
async loadWasmCapabilities(extension) {
const {data} = await LNbits.api.request(
'GET',
`/wasm/api/v1/extensions/${extension.id}/capabilities`,
this.g.user.wallets[0].adminkey
)
const capabilities = data || {}
extension.permissions = capabilities.permissions || []
extension.paymentTags = capabilities.payment_tags || []
extension.grantedPermissions = capabilities.granted_permissions || []
extension.grantedPaymentTags = capabilities.granted_payment_tags || []
extension._grantedPermissions = extension.grantedPermissions.slice()
extension._grantedPaymentTags = extension.grantedPaymentTags.slice()
return capabilities
},
showWasmPermissionsDialog(extension, data = {}) {
this.permissionsDialog.extension = extension
this.permissionsDialog.checked = (
data.granted_permissions ||
extension._grantedPermissions ||
extension.grantedPermissions ||
[]
).slice()
this.permissionsDialog.missing = data.missing_permissions || []
this.permissionsDialog.tags = (
data.granted_payment_tags ||
extension._grantedPaymentTags ||
extension.grantedPaymentTags ||
[]
).slice()
this.permissionsDialog.tagOptions =
data.payment_tags || extension.paymentTags || []
this.permissionsDialog.show = true
},
async ensureWasmPermissionsReady(extension) {
if (extension.extensionType !== 'wasm') return true
const wasmHost = this.extensions.find(ext => ext.id === 'wasm')
if (!wasmHost || !wasmHost.isInstalled || !wasmHost.isActive) {
Quasar.Notify.create({
type: 'warning',
message:
'Enable the WASM! host extension before using this extension.'
})
return false
}
let data = {}
try {
data = await this.loadWasmCapabilities(extension)
} catch (err) {
LNbits.utils.notifyApiError(err)
return false
}
const required = (data.permissions || []).map(p => p.id).filter(Boolean)
const missing = required.filter(
permission => !extension.grantedPermissions.includes(permission)
)
const tags = data.payment_tags || []
const missingTags =
tags.length &&
!tags.some(tag => extension.grantedPaymentTags.includes(tag))
if (!missing.length && !missingTags) return true
Quasar.Notify.create({
type: 'warning',
message: 'Save WASM permissions before using this extension.'
})
this.showWasmPermissionsDialog(extension, data)
return false
},
async openExtension(extension) {
if (!(await this.ensureWasmPermissionsReady(extension))) return
window.location.href = `${extension.id}/`
},
async enableExtensionForUser(extension) {
if (!(await this.ensureWasmPermissionsReady(extension))) return
if (extension.isPaymentRequired) {
this.showPayToEnable(extension)
return
@@ -294,6 +390,67 @@ window.PageExtensions = {
this.selectedExtension.payToEnable.showQRCode = false
this.showPayToEnableDialog = true
},
cancelPermissionsDialog() {
this.permissionsDialog.show = false
this.permissionsDialog.extension = null
this.permissionsDialog.checked = []
this.permissionsDialog.missing = []
this.permissionsDialog.tags = []
this.permissionsDialog.tagOptions = []
},
async openPermissionsForExtension(extension) {
if (extension.extensionType !== 'wasm') {
Quasar.Notify.create({
type: 'warning',
message: 'This extension does not use WASM permissions.'
})
return
}
try {
const data = await this.loadWasmCapabilities(extension)
this.showWasmPermissionsDialog(extension, data)
} catch (err) {
LNbits.utils.notifyApiError(err)
}
},
async confirmPermissionsDialog() {
const ext = this.permissionsDialog.extension
const granted = this.permissionsDialog.checked.slice()
const tags = this.permissionsDialog.tags.slice()
this.permissionsDialog.show = false
this.permissionsDialog.extension = null
this.permissionsDialog.checked = []
const missing = this.permissionsDialog.missing || []
this.permissionsDialog.missing = []
this.permissionsDialog.tags = []
this.permissionsDialog.tagOptions = []
if (missing.length) {
Quasar.Notify.create({
type: 'negative',
message: 'Missing API endpoints for one or more permissions.'
})
return
}
if (!ext) return
ext._grantedPermissions = granted
ext.grantedPermissions = granted
ext._grantedPaymentTags = tags
ext.grantedPaymentTags = tags
try {
await LNbits.api.request(
'PUT',
`/wasm/api/v1/extensions/${ext.id}/permissions`,
this.g.user.wallets[0].adminkey,
{permissions: granted, payment_tags: tags}
)
Quasar.Notify.create({
type: 'positive',
message: 'Permissions saved.'
})
} catch (err) {
LNbits.utils.notifyApiError(err)
}
},
updatePayToInstallData(extension) {
LNbits.api
.request(
+5
View File
@@ -92,6 +92,11 @@ def register_invoice_listener(send_chan: asyncio.Queue, name: str | None = None)
invoice_listeners[name] = send_chan
def unregister_invoice_listener(name: str) -> None:
if name in invoice_listeners:
invoice_listeners.pop(name, None)
internal_invoice_queue: asyncio.Queue = asyncio.Queue(0)
+98 -2
View File
@@ -291,8 +291,7 @@
"
flat
color="primary"
type="a"
:href="extension.id + '/'"
@click="openExtension(extension)"
:label="$t('open')"
></q-btn>
<q-btn
@@ -329,6 +328,18 @@
<span v-text="$t('enable_extension_details')">
</span> </q-tooltip
></q-btn>
<q-btn
v-if="
extension.isInstalled &&
extension.isActive &&
!g.user.extensions.includes(extension.id) &&
extension.extensionType === 'wasm'
"
flat
color="grey-5"
@click="openPermissionsForExtension(extension)"
label="Permissions"
></q-btn>
<q-btn
@click="showManageExtension(extension)"
@@ -921,6 +932,91 @@
</q-card>
</q-dialog>
<q-dialog v-model="permissionsDialog.show" position="top">
<q-card class="q-pa-md" style="min-width: 360px; max-width: 90vw">
<q-card-section>
<div class="text-h6">Permissions required</div>
<div class="text-caption text-grey">This extension can:</div>
<q-list v-if="permissionsDialog.extension">
<q-item
v-for="perm in permissionsDialog.extension.permissions"
:key="perm.id || perm"
clickable
>
<q-item-section>
<q-item-label v-text="perm.label || perm"></q-item-label>
<q-item-label
caption
v-if="perm.description"
v-text="perm.description"
></q-item-label>
</q-item-section>
<q-item-section side>
<q-checkbox
v-model="permissionsDialog.checked"
:val="perm.id || perm"
/>
</q-item-section>
</q-item>
</q-list>
<div
v-if="
permissionsDialog.tagOptions && permissionsDialog.tagOptions.length
"
class="q-mt-md"
>
<div class="text-caption text-grey">
Allow this extension to listen for payment tags:
</div>
<q-list>
<q-item
v-for="tag in permissionsDialog.tagOptions"
:key="tag"
clickable
>
<q-item-section>
<q-item-label v-text="tag"></q-item-label>
</q-item-section>
<q-item-section side>
<q-checkbox v-model="permissionsDialog.tags" :val="tag" />
</q-item-section>
</q-item>
</q-list>
</div>
<div
v-if="permissionsDialog.missing && permissionsDialog.missing.length"
class="q-mt-md text-negative"
>
<div class="text-caption">
Missing API endpoints required by this extension:
</div>
<q-chip
v-for="perm in permissionsDialog.missing"
:key="perm"
:label="perm"
color="red-2"
text-color="black"
class="q-mr-xs q-mt-xs"
/>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn
flat
color="grey"
v-text="$t('cancel')"
@click="cancelPermissionsDialog"
></q-btn>
<q-btn
color="primary"
:disable="!permissionsAllChecked || permissionsHasMissingEndpoints"
label="Save"
@click="confirmPermissionsDialog"
></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
<q-dialog v-model="showExtensionDetailsDialog" position="top">
<q-card
v-if="selectedExtensionDetails"
Generated
+51 -1462
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -63,6 +63,7 @@ lnbits-cli = "lnbits.commands:main"
breez = ["breez-sdk~=0.8.0", "breez-sdk-liquid~=0.11.11"]
liquid = ["wallycore~=1.5.1"]
migration = ["psycopg2-binary~=2.9.11"]
wasm = ["wasmtime>=18.0.0"]
[dependency-groups]
dev = [
+33
View File
@@ -162,6 +162,39 @@ async def test_extension_api_install_details_and_release_endpoints(mocker):
assert release_info["is_version_compatible"] is True
@pytest.mark.anyio
async def test_wasm_extension_enable_requires_saved_permissions(mocker):
user = await create_user_account(
Account(
id=uuid4().hex,
username=f"user_{uuid4().hex[:8]}",
email=f"user_{uuid4().hex[:8]}@lnbits.com",
)
)
ext_id = f"wasm_{uuid4().hex[:8]}"
await create_installed_extension(make_installable_extension(ext_id))
mocker.patch(
"lnbits.core.views.extension_api.get_valid_extensions",
mocker.AsyncMock(return_value=[Extension(code=ext_id, is_valid=True)]),
)
mocker.patch(
"lnbits.core.views.extension_api.get_extension_type",
return_value="wasm",
)
mocker.patch(
"lnbits.core.views.extension_api._load_wasm_extension_config",
return_value={
"extension_type": "wasm",
"permissions": [{"id": "ext.db.read_write"}],
"payment_tags": ["paidtasks"],
},
)
with pytest.raises(HTTPException) as exc:
await api_enable_extension(ext_id, AccountId(id=user.id))
assert exc.value.status_code == 400
@pytest.mark.anyio
async def test_extension_api_pay_to_enable_and_catalog_views(mocker, admin_user):
regular_user = await create_user_account(
Generated
+42 -19
View File
@@ -1075,11 +1075,11 @@ wheels = [
[[package]]
name = "idna"
version = "3.14"
version = "3.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/b1/efac073e0c297ecf2fb33c346989a529d4e19164f1759102dee5953ee17e/idna-3.14.tar.gz", hash = "sha256:466d810d7a2cc1022bea9b037c39728d51ae7dad40d480fc9b7d7ecf98ba8ee3", size = 198272, upload-time = "2026-05-10T20:32:15.935Z" }
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/3c/3f62dee257eb3d6b2c1ef2a09d36d9793c7111156a73b5654d2c2305e5ce/idna-3.14-py3-none-any.whl", hash = "sha256:e677eaf072e290f7b725f9acf0b3a2bd55f9fd6f7c70abe5f0e34823d0accf69", size = 72184, upload-time = "2026-05-10T20:32:14.295Z" },
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
]
[[package]]
@@ -1335,6 +1335,9 @@ liquid = [
migration = [
{ name = "psycopg2-binary" },
]
wasm = [
{ name = "wasmtime" },
]
[package.dev-dependencies]
dev = [
@@ -1407,10 +1410,11 @@ requires-dist = [
{ name = "uvicorn", specifier = "~=0.40.0" },
{ name = "uvloop", specifier = "~=0.22.1" },
{ name = "wallycore", marker = "extra == 'liquid'", specifier = "~=1.5.1" },
{ name = "wasmtime", marker = "extra == 'wasm'", specifier = ">=18.0.0" },
{ name = "websocket-client", specifier = "~=1.9.0" },
{ name = "websockets", specifier = "~=15.0.1" },
]
provides-extras = ["breez", "liquid", "migration"]
provides-extras = ["breez", "liquid", "migration", "wasm"]
[package.metadata.requires-dev]
dev = [
@@ -2245,15 +2249,15 @@ wheels = [
[[package]]
name = "python-discovery"
version = "1.3.0"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
{ name = "platformdirs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ae/e0/cc5a8653e9a24f6cf84768f05064aa8ed5a83dcefd5e2a043db14a1c5f44/python_discovery-1.3.0.tar.gz", hash = "sha256:d098f1e86be5d45fe4d14bf1029294aabbd332f4321179dec85e76cddce834b0", size = 63925, upload-time = "2026-05-05T14:38:39.769Z" }
sdist = { url = "https://files.pythonhosted.org/packages/48/60/e88788207d81e46362cfbef0d4aaf4c0f49efc3c12d4c3fa3f542c34ebec/python_discovery-1.3.1.tar.gz", hash = "sha256:62f6db28064c9613e7ca76cb3f00c38c839a07c31c00dfe7ed0986493d2150a6", size = 68011, upload-time = "2026-05-12T20:53:36.336Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl", hash = "sha256:441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f", size = 33124, upload-time = "2026-05-05T14:38:38.539Z" },
{ url = "https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl", hash = "sha256:ed188687ebb3b82c01a17cd5ac62fc94d9f6487a7f1a0f9dfe89753fec91039c", size = 33185, upload-time = "2026-05-12T20:53:34.969Z" },
]
[[package]]
@@ -2267,11 +2271,11 @@ wheels = [
[[package]]
name = "python-multipart"
version = "0.0.28"
version = "0.0.29"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/54/a85eb421fbdd5007bc5af39d0f4ed9fa609e0fedbfdc2adcf0b34526870e/python_multipart-0.0.28.tar.gz", hash = "sha256:8550da197eac0f7ab748961fc9509b999fa2662ea25cef857f05249f6893c0f8", size = 45314, upload-time = "2026-05-10T11:05:16.596Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/a2/43bbc5860b5034e2af4ef99a0e04d726ff329c43e192ef3abaa8d7ecfce5/python_multipart-0.0.28-py3-none-any.whl", hash = "sha256:10faac07eb966c3f48dc415f9dee46c04cb10d58d30a35677db8027c825ed9b6", size = 29438, upload-time = "2026-05-10T11:05:15.052Z" },
{ 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" },
]
[[package]]
@@ -2366,7 +2370,7 @@ wheels = [
[[package]]
name = "requests"
version = "2.34.0"
version = "2.34.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@@ -2374,9 +2378,9 @@ dependencies = [
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/b8/7a707d60fea4c49094e40262cc0e2ca6c768cca21587e34d3f705afec47e/requests-2.34.0.tar.gz", hash = "sha256:7d62fe92f50eb82c529b0916bb445afa1531a566fc8f35ffdc64446e771b856a", size = 142436, upload-time = "2026-05-11T19:29:51.717Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl", hash = "sha256:917520a21b767485ce7c588f4ebb917c436b24a31231b44228715eaeb5a52c60", size = 73021, upload-time = "2026-05-11T19:29:49.923Z" },
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
]
[[package]]
@@ -2715,11 +2719,11 @@ wheels = [
[[package]]
name = "types-mock"
version = "5.2.0.20260508"
version = "5.2.0.20260518"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a7/54/cefdd866e8b69be4ce93288710a55506bf3f8c5eba8c0b0b8322131bd25d/types_mock-5.2.0.20260508.tar.gz", hash = "sha256:2049474a82a678e1979d7a1273891daccab4c19bbf1b28df5594836d675873d3", size = 11535, upload-time = "2026-05-08T04:49:03.87Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ec/a4/26595e2a9407752c2e3cd5b17d7884db847e39834a2575cf15b5c3b19a27/types_mock-5.2.0.20260518.tar.gz", hash = "sha256:49af9c18aac4caa90e0e1e8437e2160cd8b3f126053dae6453d65b393590fcf9", size = 11577, upload-time = "2026-05-18T06:02:42.607Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/bf/080b185263af2fc0c25ddf26163f9c59d24caa0edc8c885ba93725fd1ad6/types_mock-5.2.0.20260508-py3-none-any.whl", hash = "sha256:eb90adc8527fba4d800b0d65268f141978789a0398a78ba6bda453cd62de4862", size = 10455, upload-time = "2026-05-08T04:49:03.045Z" },
{ url = "https://files.pythonhosted.org/packages/82/d6/da3bf7cc26ebe587e8c50a505d302f5755840fc24fbbd704b770c43b764b/types_mock-5.2.0.20260518-py3-none-any.whl", hash = "sha256:3c511875b6f37d30c70add3e72265d1c21202b8544751361e3ca94f7a757a03d", size = 10459, upload-time = "2026-05-18T06:02:41.224Z" },
]
[[package]]
@@ -2800,7 +2804,7 @@ wheels = [
[[package]]
name = "virtualenv"
version = "21.3.1"
version = "21.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
@@ -2809,9 +2813,9 @@ dependencies = [
{ name = "python-discovery" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ec/0d/915c02c94d207b85580eb09bffab54438a709e7288524094fe781da526c2/virtualenv-21.3.1.tar.gz", hash = "sha256:c2305bc1fddeec40699b8370d13f8d431b0701f00ce895061ce493aeded4426b", size = 7613791, upload-time = "2026-05-05T01:34:31.402Z" }
sdist = { url = "https://files.pythonhosted.org/packages/15/ba/1f6e8c957e4932be060dcdc482d339c12e0216351478add3645cdaa53c05/virtualenv-21.3.3.tar.gz", hash = "sha256:f5bda277e553b1c2b3c1a8debfc30496e1288cc93ce6b7b71b3280047e317328", size = 7613784, upload-time = "2026-05-13T18:01:30.19Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl", hash = "sha256:d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35", size = 7594539, upload-time = "2026-05-05T01:34:28.98Z" },
{ url = "https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl", hash = "sha256:7d5987d8369e098e41406efb780a3d4ca79280097293899e351a6407ee153ab3", size = 7594554, upload-time = "2026-05-13T18:01:27.815Z" },
]
[[package]]
@@ -2843,6 +2847,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/41/e5/1c1d2979d074cba4f0a1516f2bf4c3ee66067b6ba3b96e5cb4460555d474/wallycore-1.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3343a1df11a7ef4572521e002f4550a0157aea2c749dcfd9be62fb5babe0d03", size = 1728038, upload-time = "2026-04-15T22:04:36.434Z" },
]
[[package]]
name = "wasmtime"
version = "44.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/9f/b9e08200c362af1f08ca3f50b54a05573b9cca840cb981e70bdfe1658b24/wasmtime-44.0.0.tar.gz", hash = "sha256:1e5e7a7046136054e12f82101a9b5ce30b02d4f92946e6fbe3e2fced61a6b1a5", size = 120544, upload-time = "2026-04-20T22:20:23.782Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/e6/756c5b1bbccb394cc86a29f47ddb9b764e05a429742bec4fdfd499b61481/wasmtime-44.0.0-py3-none-android_26_arm64_v8a.whl", hash = "sha256:5bc01e2f1dae1e85b19343a718bf2b1dc9f0fa6b9f8c5f2922b9fb4b29fb56bd", size = 7985484, upload-time = "2026-04-20T22:20:00.463Z" },
{ url = "https://files.pythonhosted.org/packages/6e/a6/5dac42dfa129042cac70d3d07bb8568d7b6319222333733de7a392cf109f/wasmtime-44.0.0-py3-none-android_26_x86_64.whl", hash = "sha256:2e7db391e7cd274c3608bf22a2b2706bfbcc44a233c9a62277f0f03f9e0f48ad", size = 8922840, upload-time = "2026-04-20T22:20:02.782Z" },
{ url = "https://files.pythonhosted.org/packages/2a/aa/19e06d2a24cffe8c6af2de28862bfc02f3cef745e24d4886787db6c3d64d/wasmtime-44.0.0-py3-none-any.whl", hash = "sha256:28903f584fd9707438551ed3878bad61d841be78c715db4e20bf30e43b185825", size = 7426512, upload-time = "2026-04-20T22:20:05.068Z" },
{ url = "https://files.pythonhosted.org/packages/33/04/672333a17f941605daab1a2b0a1f67c079526c564b7d70d5a233ce6ccfc1/wasmtime-44.0.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:019923321cf9244ed48d8f3858e50a93500a523c44b31d7b93c8c30195c4b9aa", size = 8717123, upload-time = "2026-04-20T22:20:06.707Z" },
{ url = "https://files.pythonhosted.org/packages/48/3f/53f3989a32d0cd0b8345e93e4d7e162e7b392ad8dac4fd59ec284b4c7172/wasmtime-44.0.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07e96eb5bbf40473ea51f2cb3b3f5358537e41644662e02115edf5bd7883c9ca", size = 7626477, upload-time = "2026-04-20T22:20:09.092Z" },
{ url = "https://files.pythonhosted.org/packages/c5/c1/9c64ed6c2d152b56482bb66297bfc03442d25449bfa91da7bf2e58a80f01/wasmtime-44.0.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:f8d5bbd234bb99f9e94e284fba67b5e899ddebe2d4be7384f78396bdd4ce27a6", size = 8999749, upload-time = "2026-04-20T22:20:10.825Z" },
{ url = "https://files.pythonhosted.org/packages/bf/27/bca7dcadebfbe6a304e84f85f8921a7c0dde6711b42bec14f2f4d23b1023/wasmtime-44.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:e686644bcddfec89095c9fd982b0848ab5e494ece865517214a63430b2ed08f8", size = 7943253, upload-time = "2026-04-20T22:20:13.442Z" },
{ url = "https://files.pythonhosted.org/packages/da/f0/dc7bec6880abbdf3944e728df03e4479b9f25c860e57f499d48140f740d3/wasmtime-44.0.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7326e8cc02b583a7817cf50029d8e9b06c13d2f3573b3e677dbe51a8c52245d7", size = 8002385, upload-time = "2026-04-20T22:20:15.287Z" },
{ url = "https://files.pythonhosted.org/packages/78/e6/5ebd6db7014a64e715febd3346cf0f48b3f8d216720ddfbd798a07a16e31/wasmtime-44.0.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a7840d7d81e59ff666067031b3bde727ff28f923c5040b1a3cae4d8d37340f18", size = 9023000, upload-time = "2026-04-20T22:20:17.42Z" },
{ url = "https://files.pythonhosted.org/packages/b4/3f/a32a7cc777f1ee3d314cbe99280b4b5e322423ef9ff84dbac7a1897d4230/wasmtime-44.0.0-py3-none-win_amd64.whl", hash = "sha256:4b5c52475148f827abd9feff4460689f6979c0333b612c6c3d9211c4b7c7258d", size = 7426518, upload-time = "2026-04-20T22:20:19.633Z" },
{ url = "https://files.pythonhosted.org/packages/78/66/acf3821098944ca1f0118052912ee3bf1d941ff979d4adf3ad3684672fe8/wasmtime-44.0.0-py3-none-win_arm64.whl", hash = "sha256:931cd4d886ae6a2a06f5cfe47bf6f7a3b2635fa512a81cd1c55975505ace8cc1", size = 6342611, upload-time = "2026-04-20T22:20:21.909Z" },
]
[[package]]
name = "websocket-client"
version = "1.9.0"