Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c38cbcae8 | ||
|
|
385fb4f9bc | ||
|
|
a949242f7d | ||
|
|
8db76b8864 | ||
|
|
9e3ab0ef26 | ||
|
|
04c9b67997 | ||
|
|
116f982aab | ||
|
|
63cc89e2b6 | ||
|
|
183e6e5661 |
@@ -24,6 +24,10 @@ LOG_ROTATION="100 MB"
|
||||
LOG_RETENTION="3 months"
|
||||
# for database cleanup commands
|
||||
# CLEANUP_WALLETS_DAYS=90
|
||||
# Hard limit for total created users. Set to 0 to disable the limit.
|
||||
# LNBITS_MAX_USERS=0
|
||||
# Hard limit for total installed extensions. Set to 0 to disable the limit.
|
||||
# LNBITS_MAX_EXTENSIONS=0
|
||||
|
||||
# === Admin Settings ===
|
||||
|
||||
|
||||
@@ -69,7 +69,10 @@ jobs:
|
||||
--onefile \
|
||||
--name lnbits \
|
||||
--hidden-import=embit \
|
||||
--hidden-import=bitstring.bitstore_bitarray \
|
||||
--collect-all embit \
|
||||
--collect-all bitstring \
|
||||
--collect-all bitarray \
|
||||
--collect-all lnbits \
|
||||
--collect-all sqlalchemy \
|
||||
--collect-all breez_sdk \
|
||||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
make: openapi
|
||||
|
||||
regtest:
|
||||
needs: [ lint, test-api, test-wallets, test-unit, migration, openapi ]
|
||||
needs: [ lint ]
|
||||
uses: ./.github/workflows/regtest.yml
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
@@ -6,6 +6,7 @@ __pycache__
|
||||
*$py.class
|
||||
.mypy_cache
|
||||
.vscode
|
||||
.codex
|
||||
*-lock.json
|
||||
.python-version
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from .extensions import (
|
||||
drop_extension_db,
|
||||
get_installed_extension,
|
||||
get_installed_extensions,
|
||||
get_installed_extensions_count,
|
||||
get_user_active_extensions_ids,
|
||||
get_user_extension,
|
||||
get_user_extensions,
|
||||
@@ -58,6 +59,7 @@ from .users import (
|
||||
get_account_by_username,
|
||||
get_account_by_username_or_email,
|
||||
get_accounts,
|
||||
get_accounts_count,
|
||||
get_user,
|
||||
get_user_access_control_lists,
|
||||
get_user_from_account,
|
||||
@@ -117,11 +119,13 @@ __all__ = [
|
||||
"get_account_by_username",
|
||||
"get_account_by_username_or_email",
|
||||
"get_accounts",
|
||||
"get_accounts_count",
|
||||
"get_admin_settings",
|
||||
"get_db_version",
|
||||
"get_db_versions",
|
||||
"get_installed_extension",
|
||||
"get_installed_extensions",
|
||||
"get_installed_extensions_count",
|
||||
"get_latest_payments_by_extension",
|
||||
"get_payment",
|
||||
"get_payments",
|
||||
|
||||
@@ -90,6 +90,13 @@ async def get_installed_extensions(
|
||||
return all_extensions
|
||||
|
||||
|
||||
async def get_installed_extensions_count(conn: Connection | None = None) -> int:
|
||||
row: dict | None = await (conn or db).fetchone(
|
||||
"SELECT COUNT(*) as count FROM installed_extensions"
|
||||
)
|
||||
return int(row["count"]) if row else 0
|
||||
|
||||
|
||||
async def get_user_extension(
|
||||
user_id: str, extension: str, conn: Connection | None = None
|
||||
) -> UserExtension | None:
|
||||
|
||||
@@ -37,6 +37,13 @@ async def create_account(
|
||||
return account
|
||||
|
||||
|
||||
async def get_accounts_count(conn: Connection | None = None) -> int:
|
||||
row: dict | None = await (conn or db).fetchone(
|
||||
"SELECT COUNT(*) as count FROM accounts"
|
||||
)
|
||||
return int(row["count"]) if row else 0
|
||||
|
||||
|
||||
async def update_account(account: Account, conn: Connection | None = None) -> Account:
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
await (conn or db).update("accounts", account)
|
||||
|
||||
@@ -9,6 +9,7 @@ from lnbits.core.crud import (
|
||||
delete_installed_extension,
|
||||
get_db_version,
|
||||
get_installed_extension,
|
||||
get_installed_extensions_count,
|
||||
update_installed_extension_state,
|
||||
)
|
||||
from lnbits.core.crud.extensions import (
|
||||
@@ -38,6 +39,8 @@ async def install_extension(
|
||||
if installed_ext and installed_ext.meta:
|
||||
ext_info.meta.payments = installed_ext.meta.payments
|
||||
|
||||
await check_extensions_limit(installed_ext)
|
||||
|
||||
if not skip_download:
|
||||
await ext_info.download_archive()
|
||||
|
||||
@@ -63,6 +66,15 @@ async def install_extension(
|
||||
return extension
|
||||
|
||||
|
||||
async def check_extensions_limit(installed_ext: InstallableExtension | None = None):
|
||||
if settings.lnbits_max_extensions == 0 or installed_ext:
|
||||
return
|
||||
|
||||
extensions_count = await get_installed_extensions_count()
|
||||
if extensions_count >= settings.lnbits_max_extensions:
|
||||
raise ValueError("Max amount of extensions have been installed")
|
||||
|
||||
|
||||
async def uninstall_extension(ext_id: str):
|
||||
await stop_extension_background_work(ext_id)
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from ..crud import (
|
||||
get_account_by_email,
|
||||
get_account_by_pubkey,
|
||||
get_account_by_username,
|
||||
get_accounts_count,
|
||||
get_super_settings,
|
||||
get_user_extensions,
|
||||
get_user_from_account,
|
||||
@@ -55,6 +56,8 @@ async def create_user_account_no_ckeck(
|
||||
conn: Connection | None = None,
|
||||
) -> User:
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||
await check_users_limit(conn)
|
||||
|
||||
if account:
|
||||
account.validate_fields()
|
||||
if account.username and await get_account_by_username(
|
||||
@@ -95,6 +98,15 @@ async def create_user_account_no_ckeck(
|
||||
return user
|
||||
|
||||
|
||||
async def check_users_limit(conn: Connection | None = None):
|
||||
if settings.lnbits_max_users == 0:
|
||||
return
|
||||
|
||||
users_count = await get_accounts_count(conn=conn)
|
||||
if users_count >= settings.lnbits_max_users:
|
||||
raise ValueError("Max amount of users have been created")
|
||||
|
||||
|
||||
async def update_user_account(account: Account) -> Account:
|
||||
account.validate_fields()
|
||||
|
||||
|
||||
@@ -98,12 +98,16 @@ async def api_install_extension(data: CreateExtension):
|
||||
ext_info.clean_extension_files()
|
||||
detail = (
|
||||
str(exc)
|
||||
if isinstance(exc, AssertionError)
|
||||
if isinstance(exc, (AssertionError, ValueError))
|
||||
else f"Failed to install extension '{ext_info.id}'."
|
||||
f"({ext_info.installed_version})."
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
status_code=(
|
||||
HTTPStatus.BAD_REQUEST
|
||||
if isinstance(exc, (AssertionError, ValueError))
|
||||
else HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
),
|
||||
detail=detail,
|
||||
) from exc
|
||||
|
||||
|
||||
+5
-2
@@ -1007,8 +1007,9 @@ class EnvSettings(LNbitsSettings):
|
||||
debug_database: bool = Field(default=False)
|
||||
bundle_assets: bool = Field(default=True)
|
||||
# When enabled, auth cookies require HTTPS and SSO will reject insecure HTTP.
|
||||
# Set to false for local/dev environments that run without TLS.
|
||||
auth_https_only: bool = Field(default=True)
|
||||
# Keep disabled by default so local HTTP installs continue to work unless
|
||||
# operators explicitly opt into HTTPS-only auth cookies.
|
||||
auth_https_only: bool = Field(default=False)
|
||||
host: str = Field(default="127.0.0.1")
|
||||
port: int = Field(default=5000, gt=0)
|
||||
forwarded_allow_ips: str = Field(default="*")
|
||||
@@ -1026,6 +1027,8 @@ class EnvSettings(LNbitsSettings):
|
||||
|
||||
cleanup_wallets_days: int = Field(default=90, ge=0)
|
||||
funding_source_max_retries: int = Field(default=4, ge=0)
|
||||
lnbits_max_users: int = Field(default=0, ge=0)
|
||||
lnbits_max_extensions: int = Field(default=0, ge=0)
|
||||
|
||||
@property
|
||||
def has_default_extension_path(self) -> bool:
|
||||
|
||||
@@ -75,7 +75,35 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div v-if="isSuperUser">
|
||||
<lnbits-admin-funding-sources
|
||||
:form-data="formData"
|
||||
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
|
||||
/>
|
||||
<div class="row q-col-gutter-md q-my-md">
|
||||
<div class="col-12 col-sm-8">
|
||||
<q-item tag="div">
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
v-text="$t('funding_source_retries')"
|
||||
></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('funding_source_retries_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.funding_source_max_retries"
|
||||
type="number"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md q-mt-lg">
|
||||
<div class="col-12">
|
||||
<h6 class="q-my-none">
|
||||
<span v-text="$t('routing_fee_reserve_calculations')"></span>
|
||||
@@ -182,34 +210,6 @@
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isSuperUser">
|
||||
<lnbits-admin-funding-sources
|
||||
:form-data="formData"
|
||||
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
|
||||
/>
|
||||
<div class="row q-col-gutter-md q-my-md">
|
||||
<div class="col-12 col-sm-8">
|
||||
<q-item tag="div">
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
v-text="$t('funding_source_retries')"
|
||||
></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('funding_source_retries_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.funding_source_max_retries"
|
||||
type="number"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator></q-separator>
|
||||
<h6 class="q-mt-lg q-mb-sm">
|
||||
<p v-text="$t('watchdog')"></p>
|
||||
|
||||
@@ -61,6 +61,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>=43.0.0"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
|
||||
@@ -135,6 +135,8 @@ async def test_auth_api_first_install_success_and_validation(
|
||||
)
|
||||
assert success.status_code == 200
|
||||
assert success.json()["access_token"]
|
||||
assert "cookie_access_token=" in success.headers["set-cookie"]
|
||||
assert "Secure" not in success.headers["set-cookie"]
|
||||
|
||||
updated_superuser = await get_account(settings.super_user, active_only=False)
|
||||
assert updated_superuser is not None
|
||||
@@ -159,3 +161,43 @@ async def test_auth_api_first_install_success_and_validation(
|
||||
await update_account(restored_superuser)
|
||||
settings.first_install = original_first_install
|
||||
settings.first_install_token = original_first_install_token
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_auth_api_first_install_uses_secure_cookie_when_enabled(
|
||||
http_client: AsyncClient, settings: Settings
|
||||
):
|
||||
superuser = await get_account(settings.super_user, active_only=False)
|
||||
assert superuser is not None
|
||||
|
||||
original_username = superuser.username
|
||||
original_password_hash = superuser.password_hash
|
||||
original_first_install = settings.first_install
|
||||
original_auth_https_only = settings.auth_https_only
|
||||
|
||||
new_username = f"secure_{uuid4().hex[:8]}"
|
||||
|
||||
try:
|
||||
settings.first_install = True
|
||||
settings.auth_https_only = True
|
||||
|
||||
success = await http_client.put(
|
||||
"/api/v1/auth/first_install",
|
||||
json={
|
||||
"username": new_username,
|
||||
"password": "secret1234",
|
||||
"password_repeat": "secret1234",
|
||||
},
|
||||
)
|
||||
|
||||
assert success.status_code == 200
|
||||
assert "cookie_access_token=" in success.headers["set-cookie"]
|
||||
assert "Secure" in success.headers["set-cookie"]
|
||||
finally:
|
||||
restored_superuser = await get_account(settings.super_user, active_only=False)
|
||||
assert restored_superuser is not None
|
||||
restored_superuser.username = original_username
|
||||
restored_superuser.password_hash = original_password_hash
|
||||
await update_account(restored_superuser)
|
||||
settings.first_install = original_first_install
|
||||
settings.auth_https_only = original_auth_https_only
|
||||
|
||||
+3
-1
@@ -40,7 +40,9 @@ from tests.helpers import (
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
|
||||
ADMIN_USER_ID = uuid4().hex
|
||||
_PURE_SETTINGS = Settings()
|
||||
# Snapshot the initialized module settings instead of a fresh Settings() instance.
|
||||
# The module settings include runtime-populated values like `version`.
|
||||
_PURE_SETTINGS = copy.deepcopy(lnbits_settings)
|
||||
_PURE_SETTINGS_FIELDS = tuple(
|
||||
sorted(
|
||||
{
|
||||
|
||||
+3
-1
@@ -28,7 +28,7 @@ from lnbits.core.models.extensions_builder import (
|
||||
PublicPageFields,
|
||||
SettingsFields,
|
||||
)
|
||||
from lnbits.wallets import get_funding_source
|
||||
from lnbits.wallets import get_funding_source, set_funding_source
|
||||
|
||||
|
||||
class DbTestModel(BaseModel):
|
||||
@@ -182,6 +182,8 @@ def make_lnurl_pay_response(
|
||||
)
|
||||
|
||||
|
||||
set_funding_source()
|
||||
|
||||
funding_source = get_funding_source()
|
||||
is_fake: bool = funding_source.__class__.__name__ == "FakeWallet"
|
||||
is_regtest: bool = not is_fake
|
||||
|
||||
@@ -9,15 +9,7 @@ from loguru import logger
|
||||
from lnbits.wallets import get_funding_source
|
||||
|
||||
funding_source = get_funding_source()
|
||||
|
||||
|
||||
def is_boltz_wallet():
|
||||
print(
|
||||
"### funding_source.__class__.__name__ 2",
|
||||
get_funding_source().__class__.__name__,
|
||||
)
|
||||
return get_funding_source().__class__.__name__ == "BoltzWallet"
|
||||
|
||||
is_boltz_wallet = funding_source.__class__.__name__ == "BoltzWallet"
|
||||
|
||||
docker_lightning_cli = [
|
||||
"docker",
|
||||
|
||||
@@ -21,8 +21,7 @@ async def test_create_invoice(from_wallet):
|
||||
)
|
||||
|
||||
# we cannot know the preimage of the swap yet
|
||||
funding_source = get_funding_source()
|
||||
if not is_boltz_wallet():
|
||||
if not is_boltz_wallet:
|
||||
assert payment.preimage
|
||||
|
||||
invoice = decode(payment.bolt11)
|
||||
@@ -43,8 +42,7 @@ async def test_create_internal_invoice(from_wallet):
|
||||
)
|
||||
|
||||
# we cannot know the preimage of the swap yet
|
||||
funding_source = get_funding_source()
|
||||
if not is_boltz_wallet():
|
||||
if not is_boltz_wallet:
|
||||
assert payment.preimage
|
||||
|
||||
invoice = decode(payment.bolt11)
|
||||
|
||||
@@ -20,7 +20,7 @@ async def test_services_pay_invoice(to_wallet, real_invoice):
|
||||
)
|
||||
assert payment
|
||||
assert payment.memo == description
|
||||
if not is_boltz_wallet():
|
||||
if not is_boltz_wallet:
|
||||
assert payment.status == PaymentState.SUCCESS
|
||||
assert payment.preimage
|
||||
else:
|
||||
|
||||
@@ -252,7 +252,12 @@ async def test_notification_for_internal_payment(
|
||||
assert _payment.bolt11 == payment.bolt11
|
||||
assert _payment.amount == 123_000
|
||||
updated_payment = await get_payment(_payment.checking_id)
|
||||
assert updated_payment.webhook_status == "404"
|
||||
assert (
|
||||
updated_payment.webhook_status is not None
|
||||
), "Webhook should have been called."
|
||||
assert (
|
||||
int(updated_payment.webhook_status) >= 400
|
||||
), "Webhook should have been called and failed."
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
||||
@@ -197,7 +197,7 @@ async def test_update_wallet_balance_validates_credit_and_debit(
|
||||
|
||||
settings.lnbits_wallet_limit_max_balance = 0
|
||||
queue_mock = mocker.patch(
|
||||
"lnbits.core.services.payments.internal_invoice_queue_put",
|
||||
"lnbits.tasks.internal_invoice_queue_put",
|
||||
mocker.AsyncMock(),
|
||||
)
|
||||
|
||||
|
||||
@@ -1321,6 +1321,9 @@ liquid = [
|
||||
migration = [
|
||||
{ name = "psycopg2-binary" },
|
||||
]
|
||||
wasm = [
|
||||
{ name = "wasmtime" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
@@ -1391,10 +1394,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 = ">=43.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 = [
|
||||
@@ -2022,11 +2026,11 @@ email = [
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2780,6 +2784,25 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/15/38c48fb3319725d946b926090cb3e5fc4dc1fdc15b1fa9fe8474628b6c55/wallycore-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:36da969b58cd32f95eba901027595ad35f0326c04c9e5227fa73e0c14203bb4c", size = 1728580, upload-time = "2026-02-02T12:27:01.79Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmtime"
|
||||
version = "43.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/0e/967542865d59d9529bab604b9b88f09a92636e69cc4b1d30c5013e854493/wasmtime-43.0.0.tar.gz", hash = "sha256:eb98b8e2bc35d03dd69c9dd095a388044323622526fc94a9406b8efc48ddc259", size = 117449, upload-time = "2026-03-31T19:26:23.663Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/a9/5e598c9ae8791375fa47b0dad377e0030dcd6da1be527a639670c5a3f9d6/wasmtime-43.0.0-py3-none-android_26_arm64_v8a.whl", hash = "sha256:c52d7bd47481958494b6ef9f0ed56d01ba6d7088cc9adbc1414be899b75bc04d", size = 6895231, upload-time = "2026-03-31T19:26:01.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/aa/ce764724dcede88f9010963ca7d70d0a79655174599ea85074cb2c656d59/wasmtime-43.0.0-py3-none-android_26_x86_64.whl", hash = "sha256:f65b287290f06751b2c87da3cdb2381b045ac93bc3ee0e3b805c2a6dc5327bc6", size = 7775074, upload-time = "2026-03-31T19:26:04.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ca/67db17c3f098894be798457ce261816fb67c0c1b80c1a53ed1dfa8ed4ff1/wasmtime-43.0.0-py3-none-any.whl", hash = "sha256:9441349d9346230420ed24d357d6f8330fe7251ac5938bb892147728bbe731d7", size = 6472597, upload-time = "2026-03-31T19:26:06.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/87/b9727ac8ecf02d2bd9af838fe6004c028034ce3f38215a22f8e94705b83d/wasmtime-43.0.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:0ff3815f63122d2f59e58c626aad3c4592f1cabc0b6bd7dcc1edc3890eb46783", size = 7564987, upload-time = "2026-03-31T19:26:08.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/42/d9588fa6dad9a609e5acaa72d1d5b346b2913f87c2e95d0c7ddadf5e919b/wasmtime-43.0.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a03c7aa03519df58fed5115ad8093d6deac46386115add715e725448e89ab25", size = 6615055, upload-time = "2026-03-31T19:26:10.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/a9/25b27545ad916a169583dbea41a6a03c58fe04c1d05fa39797dc43bd50b9/wasmtime-43.0.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:341542e87caf1f2ef7ff648a78827fcef5751e3e9be2ee07a1fcf3a04413c213", size = 7819110, upload-time = "2026-03-31T19:26:12.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/9a/4d8760f827931b5b265b83e52316d40b8e0eb999bb8e2d457c2ae172d5cc/wasmtime-43.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:30b042fd4a05d0f8a320baed53fcb971aff8a3789ed6967f4521f87931ace717", size = 6910375, upload-time = "2026-03-31T19:26:14.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/19/81c748c089a693b102f9a6239f2558a0ffd55fc721fcdd139361aaede1a1/wasmtime-43.0.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:34ff18384ad62625cb1438fd0266f6c74b4a72ddcb8ba30c60a66be3632db44b", size = 6938286, upload-time = "2026-03-31T19:26:15.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/fa/c37e77c907567a8802696f9ab839b719ea811cf3d59ffc815cc95d894339/wasmtime-43.0.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c7025d477d807df30dad07c9318ea747c6cfc99764c7cb2a8e44e75b8c43e3be", size = 7852033, upload-time = "2026-03-31T19:26:17.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/67/57c7e361049554cdedd9253e732a6eace5c643488a0e3886ac3f471a4be7/wasmtime-43.0.0-py3-none-win_amd64.whl", hash = "sha256:7e6b0d0641d78012bdf7d3622ca4bc969462dcf1d0a6c147dc5d7aae2f5093a9", size = 6472603, upload-time = "2026-03-31T19:26:19.724Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/27/8ecf7dbbb16dc3ab32fcb205f4d798e77cab264118bc1ac52145a76e38fb/wasmtime-43.0.0-py3-none-win_arm64.whl", hash = "sha256:5ddb2ba4b354fc4f055c8ce9285e7bc4cb259c339e5834bb4d0739d644042b8e", size = 5455362, upload-time = "2026-03-31T19:26:21.746Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websocket-client"
|
||||
version = "1.9.0"
|
||||
|
||||
Reference in New Issue
Block a user