Compare commits
69
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4744ef9bba | ||
|
|
fdf061c08d | ||
|
|
d348234cad | ||
|
|
fc5261132e | ||
|
|
4e18c423de | ||
|
|
37e5601207 | ||
|
|
231f6fc01c | ||
|
|
811957f5e1 | ||
|
|
c63a53cb14 | ||
|
|
78b8f7f16e | ||
|
|
4642878c05 | ||
|
|
d7c850ccbb | ||
|
|
4fbb4b5eef | ||
|
|
db2829a5f6 | ||
|
|
13ce1ac172 | ||
|
|
512697ae53 | ||
|
|
dfa89ca063 | ||
|
|
182694d2e5 | ||
|
|
2f10c57a10 | ||
|
|
d685824596 | ||
|
|
e816eb2dac | ||
|
|
d3c099350a | ||
|
|
691ac969dc | ||
|
|
85e8ac6057 | ||
|
|
99da300f6d | ||
|
|
cd413d85ed | ||
|
|
9245e2c268 | ||
|
|
0b5c8cdee2 | ||
|
|
c94cef07c2 | ||
|
|
22b4ab779f | ||
|
|
42061d0cc8 | ||
|
|
d0ea42fd70 | ||
|
|
cd58a0dc9b | ||
|
|
cac4e309f4 | ||
|
|
632cd257d7 | ||
|
|
a11274593c | ||
|
|
ae92965b69 | ||
|
|
3e1f75c4cc | ||
|
|
d668747ca2 | ||
|
|
dedfcd4818 | ||
|
|
f66f2fe6c2 | ||
|
|
f3ca37e846 | ||
|
|
d45709a80f | ||
|
|
e4b6dc84b5 | ||
|
|
67d1b4be3e | ||
|
|
bd514f1a46 | ||
|
|
03b302da69 | ||
|
|
b373e35915 | ||
|
|
0fd116e7ee | ||
|
|
2c84f9781d | ||
|
|
9fcf3121ac | ||
|
|
1df9fa831d | ||
|
|
42039070b9 | ||
|
|
2f9146fff3 | ||
|
|
5738795c8c | ||
|
|
3c77acf266 | ||
|
|
a4f2f55279 | ||
|
|
2ab7c17ceb | ||
|
|
7a2c509cd3 | ||
|
|
8a51ea53c8 | ||
|
|
19d3e9e64e | ||
|
|
9f9f14ead0 | ||
|
|
0a093d82ae | ||
|
|
17fd459fa7 | ||
|
|
fe9aa4d8c9 | ||
|
|
1af3b62739 | ||
|
|
e9cbc3492f | ||
|
|
ca2f78d25b | ||
|
|
9d9ab48345 |
+1
-9
@@ -7,13 +7,9 @@
|
||||
# They are NOT managed by the Admin UI and are not stored in the database.
|
||||
|
||||
# === First Install Token ===
|
||||
# If set the user is required to enter this token on the /first_install page
|
||||
# if set the user is required to enter this token on the /first_install page
|
||||
# FIRST_INSTALL_TOKEN="myaccesstoken"
|
||||
|
||||
# === Security ===
|
||||
# When enabled (recommended), auth cookies require HTTPS and SSO will reject insecure HTTP.
|
||||
AUTH_HTTPS_ONLY=true
|
||||
|
||||
# === Logging and Development ===
|
||||
|
||||
DEBUG=False
|
||||
@@ -28,10 +24,6 @@ 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,10 +69,7 @@ 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 ]
|
||||
needs: [ lint, test-api, test-wallets, test-unit, migration, openapi ]
|
||||
uses: ./.github/workflows/regtest.yml
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
@@ -6,7 +6,6 @@ __pycache__
|
||||
*$py.class
|
||||
.mypy_cache
|
||||
.vscode
|
||||
.codex
|
||||
*-lock.json
|
||||
.python-version
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/logos/lnbits-full-inverse.svg">
|
||||
<img src="docs/logos/lnbits-full.svg" alt="LNbits" style="width:300px">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
||||
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
 [![license-badge]](LICENSE) [![docs-badge]][docs]  [](https://extensions.lnbits.com/) [](https://shop.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits) [<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
||||
<img alt="lnbits_head" src="docs/assets/header.jpg" />
|
||||
<img width="2000" height="203" alt="lnbits_head" src="https://github.com/user-attachments/assets/77669718-ac10-43c7-ae95-6ce236c77401" />
|
||||
[](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
||||
|
||||
# LNbits — The most powerful Bitcoin & Lightning toolkit
|
||||
@@ -46,7 +46,7 @@ Get yourself familiar and test on our demo server [demo.lnbits.com](https://demo
|
||||
|
||||
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending, export to csv. Each wallet also comes with its own API keys, to help partition the exposure of your funding source.
|
||||
|
||||
<img alt="lnbits_wallet" src="docs/assets/wallet.jpg" />
|
||||
<img src="https://i.imgur.com/w8jdGpF.png" style="width:800px">
|
||||
|
||||
## LNbits extension universe
|
||||
|
||||
@@ -54,25 +54,25 @@ Extend YOUR LNbits to meet YOUR needs.
|
||||
|
||||
All non-core features are installed as extensions, reducing your code base and making your LNbits unique to you. Extend your LNbits install in any direction, and even create and share your own extensions.
|
||||
|
||||
<img alt="lnbits_extensions" src="docs/assets/extensions.jpg" />
|
||||
<img src="https://i.imgur.com/aEBpwJF.png" style="width:800px">
|
||||
|
||||
## LNbits API
|
||||
|
||||
LNbits has a powerful API, many projects use LNbits to do the heavy lifting for their bitcoin/lightning services.
|
||||
|
||||
<img alt="lnbits_api" src="docs/assets/api.jpg" />
|
||||
<img src="https://i.imgur.com/V742sb9.png" style="width:800px">
|
||||
|
||||
## LNbits node manager
|
||||
|
||||
LNbits comes packaged with a light node management UI, to make running your node that much easier.
|
||||
|
||||
<img alt="lnbits_api" src="docs/assets/lightning_node.jpg" />
|
||||
<img src="https://i.imgur.com/TYqIK60.png" style="width:800px">
|
||||
|
||||
## LNbits merchant tools
|
||||
## LNbits across all your devices
|
||||
|
||||
The LNbits stack can process both bitcoin and fiat payments, making it a turnkey, all-in-one solution for merchants. With orders and inventory shared across extensions, and built-in notifications for Nostr, Telegram, and email, LNbits keeps everything in sync, freeing merchants to focus on their business.
|
||||
As well as working great in a browser, LNbits has native IoS and Android apps as well as a chrome extension. So you can enjoy the same UI across ALL your devices.
|
||||
|
||||
<img alt="lnbits_merchants" src="docs/assets/merchants_small.webp" />
|
||||
<img src="https://i.imgur.com/J96EbRf.png" style="width:800px">
|
||||
|
||||
## Powered by LNbits
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 180 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 317 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 139 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 157 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 305 KiB |
@@ -12,7 +12,6 @@ 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,
|
||||
@@ -59,7 +58,6 @@ 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,
|
||||
@@ -119,13 +117,11 @@ __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,13 +90,6 @@ 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,13 +37,6 @@ 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,7 +9,6 @@ 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 (
|
||||
@@ -39,8 +38,6 @@ 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()
|
||||
|
||||
@@ -66,15 +63,6 @@ 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,7 +23,6 @@ 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,
|
||||
@@ -56,8 +55,6 @@ 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(
|
||||
@@ -98,15 +95,6 @@ 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,16 +98,12 @@ async def api_install_extension(data: CreateExtension):
|
||||
ext_info.clean_extension_files()
|
||||
detail = (
|
||||
str(exc)
|
||||
if isinstance(exc, (AssertionError, ValueError))
|
||||
if isinstance(exc, AssertionError)
|
||||
else f"Failed to install extension '{ext_info.id}'."
|
||||
f"({ext_info.installed_version})."
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=(
|
||||
HTTPStatus.BAD_REQUEST
|
||||
if isinstance(exc, (AssertionError, ValueError))
|
||||
else HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
),
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=detail,
|
||||
) from exc
|
||||
|
||||
|
||||
+2
-6
@@ -324,8 +324,7 @@ class AssetSettings(LNbitsSettings):
|
||||
"heif",
|
||||
"heics",
|
||||
"text/plain",
|
||||
"text/json",
|
||||
"text/xml",
|
||||
"text/json" "text/xml",
|
||||
"application/json",
|
||||
"application/pdf",
|
||||
]
|
||||
@@ -589,8 +588,6 @@ class ZBDFundingSource(LNbitsSettings):
|
||||
class PhoenixdFundingSource(LNbitsSettings):
|
||||
phoenixd_api_endpoint: str | None = Field(default="http://localhost:9740/")
|
||||
phoenixd_api_password: str | None = Field(default=None)
|
||||
phoenixd_data_dir: str | None = Field(default=None)
|
||||
phoenixd_mnemonic: str | None = Field(default=None)
|
||||
|
||||
|
||||
class AlbyFundingSource(LNbitsSettings):
|
||||
@@ -1010,6 +1007,7 @@ 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)
|
||||
host: str = Field(default="127.0.0.1")
|
||||
port: int = Field(default=5000, gt=0)
|
||||
@@ -1028,8 +1026,6 @@ 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:
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+11
-11
File diff suppressed because one or more lines are too long
@@ -275,8 +275,6 @@ window.localisation.en = {
|
||||
requires_server_restart:
|
||||
'Changing these settings requires a server restart to take effect.',
|
||||
funding_source_info: 'Select the active funding wallet',
|
||||
phoenixd_warning:
|
||||
"Phoenixd mnemonic is only available if phoenixd data-dir is specified and is readable by LNbits. It's not indicative of phoenixd not running. It just means LNbits cannot access the mnemonic to display it here.",
|
||||
latest_update: 'You are on the latest version {version}.',
|
||||
notifications: 'Notifications',
|
||||
notifications_configure: 'Configure Notifications',
|
||||
|
||||
@@ -202,18 +202,7 @@ window.app.component('lnbits-admin-funding-sources', {
|
||||
'Phoenixd',
|
||||
{
|
||||
phoenixd_api_endpoint: 'Endpoint',
|
||||
phoenixd_api_password: 'Key',
|
||||
phoenixd_data_dir: {
|
||||
label: 'Data Directory',
|
||||
hint: 'Directory where phoenixd stores its data, including the seed phrase.'
|
||||
},
|
||||
phoenixd_mnemonic: {
|
||||
label: 'Phoenixd Seed Phrase',
|
||||
hint: 'Only available if phoenixd data-dir is specified',
|
||||
readonly: true,
|
||||
copy: true,
|
||||
qrcode: true
|
||||
}
|
||||
phoenixd_api_password: 'Key'
|
||||
}
|
||||
],
|
||||
[
|
||||
|
||||
@@ -41,37 +41,5 @@ window.app.component('lnbits-admin-site-customisation', {
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onBackgroundImageInput(e) {
|
||||
const file = e.target.files[0]
|
||||
if (file) {
|
||||
this.uploadBackgroundImage(file)
|
||||
}
|
||||
e.target.value = null
|
||||
},
|
||||
async uploadBackgroundImage(file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
'/api/v1/assets?public_asset=true',
|
||||
null,
|
||||
formData,
|
||||
{
|
||||
headers: {'Content-Type': 'multipart/form-data'}
|
||||
}
|
||||
)
|
||||
const assetUrl = `${window.location.origin}/api/v1/assets/${data.id}/thumbnail`
|
||||
this.formData.lnbits_default_bgimage = assetUrl
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Background image uploaded.',
|
||||
icon: null
|
||||
})
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
methods: {}
|
||||
})
|
||||
|
||||
@@ -545,56 +545,31 @@ window.PageAccount = {
|
||||
if (file) {
|
||||
this.uploadAsset(file)
|
||||
}
|
||||
e.target.value = null
|
||||
},
|
||||
onBackgroundImageInput(e) {
|
||||
const file = e.target.files[0]
|
||||
if (file) {
|
||||
this.uploadBackgroundImage(file)
|
||||
}
|
||||
e.target.value = null
|
||||
},
|
||||
async uploadAsset(
|
||||
file,
|
||||
{isPublic = this.assetsUploadToPublic, notifySuccess = true} = {}
|
||||
) {
|
||||
async uploadAsset(file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
await LNbits.api.request(
|
||||
'POST',
|
||||
`/api/v1/assets?public_asset=${isPublic}`,
|
||||
`/api/v1/assets?public_asset=${this.assetsUploadToPublic}`,
|
||||
null,
|
||||
formData,
|
||||
{
|
||||
headers: {'Content-Type': 'multipart/form-data'}
|
||||
}
|
||||
)
|
||||
if (notifySuccess) {
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Upload successful!',
|
||||
icon: null
|
||||
})
|
||||
}
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Upload successful!',
|
||||
icon: null
|
||||
})
|
||||
await this.getUserAssets()
|
||||
return data
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
},
|
||||
async uploadBackgroundImage(file) {
|
||||
const asset = await this.uploadAsset(file, {
|
||||
isPublic: false,
|
||||
notifySuccess: false
|
||||
})
|
||||
if (!asset) {
|
||||
return
|
||||
}
|
||||
const assetUrl = `${window.location.origin}/api/v1/assets/${asset.id}/thumbnail`
|
||||
await this.siteCustomisationChanged({bgimageChoice: assetUrl})
|
||||
},
|
||||
async deleteAsset(asset) {
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this asset?')
|
||||
|
||||
Vendored
+153
-294
File diff suppressed because it is too large
Load Diff
@@ -75,35 +75,7 @@
|
||||
</p>
|
||||
</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>
|
||||
<div class="row q-col-gutter-md q-mt-lg">
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12">
|
||||
<h6 class="q-my-none">
|
||||
<span v-text="$t('routing_fee_reserve_calculations')"></span>
|
||||
@@ -210,6 +182,34 @@
|
||||
></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>
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
:label="prop.label"
|
||||
:hint="prop.hint"
|
||||
:value="prop.value"
|
||||
:readonly="prop.readonly || false"
|
||||
>
|
||||
<q-btn
|
||||
v-if="prop.copy"
|
||||
@@ -74,15 +73,6 @@
|
||||
></q-btn>
|
||||
</q-input>
|
||||
</div>
|
||||
<p
|
||||
v-if="fund === 'PhoenixdWallet' && key === 'phoenixd_mnemonic'"
|
||||
class="col-12 q-my-md"
|
||||
>
|
||||
<span>
|
||||
<q-icon name="warning" color="orange" size="xs"></q-icon>
|
||||
<span v-text="$t('phoenixd_warning')"></span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<q-expansion-item
|
||||
v-if="
|
||||
|
||||
@@ -251,27 +251,10 @@
|
||||
type="text"
|
||||
v-model="formData.lnbits_default_bgimage"
|
||||
label="Background Image"
|
||||
@update:model-value="applyGlobalBgimage"
|
||||
hint="This must be a trusted source. It can change the content and it can log your IP address."
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
round
|
||||
icon="upload"
|
||||
@click="$refs.adminBackgroundImageInput.click()"
|
||||
>
|
||||
<q-tooltip>Upload background image</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
<input
|
||||
type="file"
|
||||
ref="adminBackgroundImageInput"
|
||||
accept="image/*"
|
||||
style="display: none"
|
||||
@change="onBackgroundImageInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md q-mb-md">
|
||||
|
||||
@@ -492,28 +492,10 @@
|
||||
siteCustomisationChanged({bgimageChoice: $event})
|
||||
"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
round
|
||||
icon="upload"
|
||||
@click="$refs.backgroundImageInput.click()"
|
||||
>
|
||||
<q-tooltip>Upload background image</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
<q-tooltip
|
||||
><span v-text="$t('background_image')"></span
|
||||
></q-tooltip>
|
||||
</q-input>
|
||||
<input
|
||||
type="file"
|
||||
ref="backgroundImageInput"
|
||||
accept="image/*"
|
||||
style="display: none"
|
||||
@change="onBackgroundImageInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mb-md">
|
||||
@@ -1126,130 +1108,82 @@
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-card-section>
|
||||
<div
|
||||
class="row items-center no-wrap q-col-gutter-sm"
|
||||
<q-btn-dropdown
|
||||
color="grey"
|
||||
dense
|
||||
outline
|
||||
no-caps
|
||||
:label="props.row.name"
|
||||
:icon="props.row.is_public ? 'public' : ''"
|
||||
>
|
||||
<div class="col">
|
||||
<q-btn-dropdown
|
||||
color="grey"
|
||||
dense
|
||||
outline
|
||||
no-caps
|
||||
class="full-width"
|
||||
:label="props.row.name"
|
||||
:icon="props.row.is_public ? 'public' : ''"
|
||||
<q-list>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="copyAssetLinkToClipboard(props.row)"
|
||||
>
|
||||
<q-list>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="
|
||||
copyAssetLinkToClipboard(props.row)
|
||||
"
|
||||
<q-item-section avatar>
|
||||
<q-avatar icon="content_copy" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Copy Link</q-item-label>
|
||||
<q-item-label caption
|
||||
>Copy asset link to
|
||||
clipboard</q-item-label
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar icon="content_copy" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Copy Link</q-item-label>
|
||||
<q-item-label caption
|
||||
>Copy asset link to
|
||||
clipboard</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="
|
||||
toggleAssetPublicAccess(props.row)
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="toggleAssetPublicAccess(props.row)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar
|
||||
:icon="
|
||||
props.row.is_public
|
||||
? 'public_off'
|
||||
: 'public'
|
||||
"
|
||||
text-color="primary"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section v-if="props.row.is_public">
|
||||
<q-item-label>Unpublish</q-item-label>
|
||||
<q-item-label caption
|
||||
>Make this asset private</q-item-label
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar
|
||||
:icon="
|
||||
props.row.is_public
|
||||
? 'public_off'
|
||||
: 'public'
|
||||
"
|
||||
text-color="primary"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section
|
||||
v-if="props.row.is_public"
|
||||
>
|
||||
<q-item-label>Unpublish</q-item-label>
|
||||
<q-item-label caption
|
||||
>Make this asset
|
||||
private</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
<q-item-section v-else>
|
||||
<q-item-label>Publish</q-item-label>
|
||||
<q-item-label caption
|
||||
>Make this asset
|
||||
public</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-item-section>
|
||||
<q-item-section v-else>
|
||||
<q-item-label>Publish</q-item-label>
|
||||
<q-item-label caption
|
||||
>Make this asset public</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="deleteAsset(props.row)"
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="deleteAsset(props.row)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar
|
||||
icon="delete"
|
||||
text-color="negative"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Delete</q-item-label>
|
||||
<q-item-label caption
|
||||
>Permanently delete this
|
||||
asset</q-item-label
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar
|
||||
icon="delete"
|
||||
text-color="negative"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Delete</q-item-label>
|
||||
<q-item-label caption
|
||||
>Permanently delete this
|
||||
asset</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
type="a"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
color="primary"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
icon="image"
|
||||
:href="`/api/v1/assets/${props.row.id}/data`"
|
||||
>
|
||||
<q-tooltip>Full image</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div
|
||||
class="col-auto"
|
||||
v-if="props.row.thumbnail_base64"
|
||||
>
|
||||
<q-btn
|
||||
type="a"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
color="secondary"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
icon="photo_size_select_small"
|
||||
:href="`/api/v1/assets/${props.row.id}/thumbnail`"
|
||||
>
|
||||
<q-tooltip>Thumbnail</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
@@ -4,11 +4,9 @@ import hashlib
|
||||
import json
|
||||
import urllib.parse
|
||||
from collections.abc import AsyncGenerator
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from embit.bip39 import mnemonic_is_valid
|
||||
from httpx import RequestError, TimeoutException
|
||||
from loguru import logger
|
||||
from websockets import connect
|
||||
@@ -63,8 +61,6 @@ class PhoenixdWallet(Wallet):
|
||||
}
|
||||
|
||||
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.headers)
|
||||
self._seed_mnemonic_to_persist: str | None = None
|
||||
self._load_mnemonic_from_seed_file()
|
||||
|
||||
async def cleanup(self):
|
||||
try:
|
||||
@@ -73,7 +69,6 @@ class PhoenixdWallet(Wallet):
|
||||
logger.warning(f"Error closing wallet connection: {e}")
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
await self._persist_loaded_mnemonic()
|
||||
try:
|
||||
r = await self.client.get("/getinfo", timeout=10)
|
||||
r.raise_for_status()
|
||||
@@ -106,6 +101,7 @@ class PhoenixdWallet(Wallet):
|
||||
unhashed_description: bytes | None = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
|
||||
try:
|
||||
msats_amount = amount
|
||||
data: dict[str, Any] = {
|
||||
@@ -313,7 +309,7 @@ class PhoenixdWallet(Wallet):
|
||||
and message_json.get("type") == "payment_received"
|
||||
):
|
||||
logger.info(
|
||||
f"payment-received: {message_json['paymentHash']}"
|
||||
f'payment-received: {message_json["paymentHash"]}'
|
||||
)
|
||||
yield message_json["paymentHash"]
|
||||
|
||||
@@ -323,49 +319,3 @@ class PhoenixdWallet(Wallet):
|
||||
"retrying in 5 seconds"
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
def _load_mnemonic_from_seed_file(self):
|
||||
data_dir = settings.phoenixd_data_dir
|
||||
if not data_dir:
|
||||
return
|
||||
|
||||
seed_path = Path(data_dir).expanduser() / "seed.dat"
|
||||
if not seed_path.is_file():
|
||||
return
|
||||
|
||||
try:
|
||||
mnemonic = seed_path.read_text(encoding="utf-8").strip()
|
||||
if mnemonic == settings.phoenixd_mnemonic:
|
||||
return
|
||||
except OSError as exc:
|
||||
logger.warning(f"Failed to read Phoenixd seed file '{seed_path}': {exc}")
|
||||
return
|
||||
|
||||
if not mnemonic:
|
||||
logger.warning(f"Phoenixd seed file '{seed_path}' is empty.")
|
||||
return
|
||||
|
||||
if not mnemonic_is_valid(mnemonic):
|
||||
logger.warning(
|
||||
f"Phoenixd seed file '{seed_path}' does not contain a valid "
|
||||
"BIP39 mnemonic."
|
||||
)
|
||||
return
|
||||
|
||||
settings.phoenixd_mnemonic = mnemonic
|
||||
self._seed_mnemonic_to_persist = mnemonic
|
||||
|
||||
async def _persist_loaded_mnemonic(self):
|
||||
if not self._seed_mnemonic_to_persist:
|
||||
return
|
||||
|
||||
logger.info("Updating 'PHOENIXD_MNEMONIC' mnemonic settings.")
|
||||
try:
|
||||
from lnbits.core.crud.settings import set_settings_field
|
||||
|
||||
await set_settings_field(
|
||||
"phoenixd_mnemonic", self._seed_mnemonic_to_persist
|
||||
)
|
||||
self._seed_mnemonic_to_persist = None
|
||||
except Exception as exc:
|
||||
logger.warning(f"Failed to persist Phoenixd mnemonic: {exc}")
|
||||
|
||||
Generated
+8
-12
@@ -6,7 +6,7 @@
|
||||
"": {
|
||||
"name": "lnbits",
|
||||
"dependencies": {
|
||||
"axios": "^1.15.0",
|
||||
"axios": "^1.13.5",
|
||||
"chart.js": "^4.5.1",
|
||||
"moment": "^2.30.1",
|
||||
"nostr-tools": "^2.18.2",
|
||||
@@ -734,14 +734,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
|
||||
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
@@ -1555,13 +1555,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/pyright": {
|
||||
"version": "1.1.289",
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
"terser": "^5.44.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.15.0",
|
||||
"axios": "^1.13.5",
|
||||
"chart.js": "^4.5.1",
|
||||
"moment": "^2.30.1",
|
||||
"nostr-tools": "^2.18.2",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "lnbits"
|
||||
version = "1.5.4"
|
||||
version = "1.5.3"
|
||||
requires-python = ">=3.10,<3.13"
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
||||
|
||||
@@ -135,8 +135,6 @@ 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
|
||||
@@ -161,43 +159,3 @@ 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
|
||||
|
||||
+1
-3
@@ -40,9 +40,7 @@ from tests.helpers import (
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
|
||||
ADMIN_USER_ID = uuid4().hex
|
||||
# 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 = Settings()
|
||||
_PURE_SETTINGS_FIELDS = tuple(
|
||||
sorted(
|
||||
{
|
||||
|
||||
+1
-3
@@ -28,7 +28,7 @@ from lnbits.core.models.extensions_builder import (
|
||||
PublicPageFields,
|
||||
SettingsFields,
|
||||
)
|
||||
from lnbits.wallets import get_funding_source, set_funding_source
|
||||
from lnbits.wallets import get_funding_source
|
||||
|
||||
|
||||
class DbTestModel(BaseModel):
|
||||
@@ -182,8 +182,6 @@ 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,7 +9,15 @@ from loguru import logger
|
||||
from lnbits.wallets import get_funding_source
|
||||
|
||||
funding_source = get_funding_source()
|
||||
is_boltz_wallet = funding_source.__class__.__name__ == "BoltzWallet"
|
||||
|
||||
|
||||
def is_boltz_wallet():
|
||||
print(
|
||||
"### funding_source.__class__.__name__ 2",
|
||||
get_funding_source().__class__.__name__,
|
||||
)
|
||||
return get_funding_source().__class__.__name__ == "BoltzWallet"
|
||||
|
||||
|
||||
docker_lightning_cli = [
|
||||
"docker",
|
||||
|
||||
@@ -21,7 +21,8 @@ async def test_create_invoice(from_wallet):
|
||||
)
|
||||
|
||||
# we cannot know the preimage of the swap yet
|
||||
if not is_boltz_wallet:
|
||||
funding_source = get_funding_source()
|
||||
if not is_boltz_wallet():
|
||||
assert payment.preimage
|
||||
|
||||
invoice = decode(payment.bolt11)
|
||||
@@ -42,7 +43,8 @@ async def test_create_internal_invoice(from_wallet):
|
||||
)
|
||||
|
||||
# we cannot know the preimage of the swap yet
|
||||
if not is_boltz_wallet:
|
||||
funding_source = get_funding_source()
|
||||
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,12 +252,7 @@ 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 is not None
|
||||
), "Webhook should have been called."
|
||||
assert (
|
||||
int(updated_payment.webhook_status) >= 400
|
||||
), "Webhook should have been called and failed."
|
||||
assert updated_payment.webhook_status == "404"
|
||||
|
||||
|
||||
@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.tasks.internal_invoice_queue_put",
|
||||
"lnbits.core.services.payments.internal_invoice_queue_put",
|
||||
mocker.AsyncMock(),
|
||||
)
|
||||
|
||||
|
||||
@@ -1263,7 +1263,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "lnbits"
|
||||
version = "1.5.4"
|
||||
version = "1.5.3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
@@ -2022,11 +2022,11 @@ email = [
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
version = "2.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2114,7 +2114,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.3"
|
||||
version = "9.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
@@ -2125,9 +2125,9 @@ dependencies = [
|
||||
{ name = "pygments" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2213,11 +2213,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.26"
|
||||
version = "0.0.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user