mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-01-18 13:27:20 +01:00
[CHORE] string formatting default length 88 (#1887)
* [CHORE] string formatting default length 88 uses blacks default off 88 and enabled autostringformatting * formatting * nitpicks jackstar fix
This commit is contained in:
parent
2ab18544c3
commit
4e6f229db2
@ -17,14 +17,14 @@ repos:
|
||||
rev: 23.7.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.0.283
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [ --fix, --exit-non-zero-on-fix ]
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: '50c5478ed9e10bf360335449280cf2a67f4edb7a'
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [css, javascript, html, json]
|
||||
args: ['lnbits']
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.0.283
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [ --fix, --exit-non-zero-on-fix ]
|
||||
|
@ -413,9 +413,11 @@ def get_db_vendor_name():
|
||||
return (
|
||||
"PostgreSQL"
|
||||
if db_url and db_url.startswith("postgres://")
|
||||
else "CockroachDB"
|
||||
if db_url and db_url.startswith("cockroachdb://")
|
||||
else "SQLite"
|
||||
else (
|
||||
"CockroachDB"
|
||||
if db_url and db_url.startswith("cockroachdb://")
|
||||
else "SQLite"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
@ -61,7 +61,8 @@ async def migrate_databases():
|
||||
)
|
||||
elif conn.type in {POSTGRES, COCKROACH}:
|
||||
exists = await conn.fetchone(
|
||||
"SELECT * FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'dbversions'"
|
||||
"SELECT * FROM information_schema.tables WHERE table_schema = 'public'"
|
||||
" AND table_name = 'dbversions'"
|
||||
)
|
||||
|
||||
if not exists:
|
||||
|
@ -58,7 +58,9 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
|
||||
)
|
||||
wallets = await (conn or db).fetchall(
|
||||
"""
|
||||
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
|
||||
SELECT *, COALESCE((
|
||||
SELECT balance FROM balances WHERE wallet = wallets.id
|
||||
), 0) AS balance_msat
|
||||
FROM wallets
|
||||
WHERE "user" = ?
|
||||
""",
|
||||
@ -89,9 +91,9 @@ async def add_installed_extension(
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None:
|
||||
meta = {
|
||||
"installed_release": dict(ext.installed_release)
|
||||
if ext.installed_release
|
||||
else None,
|
||||
"installed_release": (
|
||||
dict(ext.installed_release) if ext.installed_release else None
|
||||
),
|
||||
"dependencies": ext.dependencies,
|
||||
}
|
||||
|
||||
@ -99,9 +101,11 @@ async def add_installed_extension(
|
||||
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO installed_extensions (id, version, name, short_description, icon, stars, meta) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (id) DO
|
||||
UPDATE SET (version, name, active, short_description, icon, stars, meta) = (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO installed_extensions
|
||||
(id, version, name, short_description, icon, stars, meta)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET
|
||||
(version, name, active, short_description, icon, stars, meta) =
|
||||
(?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
ext.id,
|
||||
@ -270,9 +274,8 @@ async def get_wallet(
|
||||
) -> Optional[Wallet]:
|
||||
row = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
|
||||
FROM wallets
|
||||
WHERE id = ?
|
||||
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0)
|
||||
AS balance_msat FROM wallets WHERE id = ?
|
||||
""",
|
||||
(wallet_id,),
|
||||
)
|
||||
@ -287,9 +290,8 @@ async def get_wallet_for_key(
|
||||
) -> Optional[Wallet]:
|
||||
row = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
|
||||
FROM wallets
|
||||
WHERE adminkey = ? OR inkey = ?
|
||||
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0)
|
||||
AS balance_msat FROM wallets WHERE adminkey = ? OR inkey = ?
|
||||
""",
|
||||
(key, key),
|
||||
)
|
||||
@ -544,9 +546,11 @@ async def create_payment(
|
||||
pending,
|
||||
memo,
|
||||
fee,
|
||||
json.dumps(extra)
|
||||
if extra and extra != {} and type(extra) is dict
|
||||
else None,
|
||||
(
|
||||
json.dumps(extra)
|
||||
if extra and extra != {} and type(extra) is dict
|
||||
else None
|
||||
),
|
||||
webhook,
|
||||
db.datetime_to_timestamp(expiration_date),
|
||||
),
|
||||
@ -608,7 +612,8 @@ async def update_payment_extra(
|
||||
) -> None:
|
||||
"""
|
||||
Only update the `extra` field for the payment.
|
||||
Old values in the `extra` JSON object will be kept unless the new `extra` overwrites them.
|
||||
Old values in the `extra` JSON object will be kept
|
||||
unless the new `extra` overwrites them.
|
||||
"""
|
||||
|
||||
amount_clause = "AND amount < 0" if outgoing else "AND amount > 0"
|
||||
@ -662,7 +667,10 @@ async def check_internal(
|
||||
async def check_internal_pending(
|
||||
payment_hash: str, conn: Optional[Connection] = None
|
||||
) -> bool:
|
||||
"""Returns False if the internal payment is not pending anymore (and thus paid), otherwise True"""
|
||||
"""
|
||||
Returns False if the internal payment is not pending anymore
|
||||
(and thus paid), otherwise True
|
||||
"""
|
||||
row = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT pending FROM apipayments
|
||||
|
@ -51,7 +51,8 @@ async def stop_extension_background_work(ext_id: str, user: str):
|
||||
"""
|
||||
Stop background work for extension (like asyncio.Tasks, WebSockets, etc).
|
||||
Extensions SHOULD expose a DELETE enpoint at the root level of their API.
|
||||
This function tries first to call the endpoint using `http` and if if fails it tries using `https`.
|
||||
This function tries first to call the endpoint using `http`
|
||||
and if it fails it tries using `https`.
|
||||
"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
|
@ -239,7 +239,8 @@ async def m007_set_invoice_expiries(db):
|
||||
invoice.date + invoice.expiry
|
||||
)
|
||||
logger.info(
|
||||
f"Migration: {i+1}/{len(rows)} setting expiry of invoice {invoice.payment_hash} to {expiration_date}"
|
||||
f"Migration: {i+1}/{len(rows)} setting expiry of invoice"
|
||||
f" {invoice.payment_hash} to {expiration_date}"
|
||||
)
|
||||
await db.execute(
|
||||
"""
|
||||
|
@ -115,16 +115,17 @@ async def pay_invoice(
|
||||
) -> str:
|
||||
"""
|
||||
Pay a Lightning invoice.
|
||||
First, we create a temporary payment in the database with fees set to the reserve fee.
|
||||
We then check whether the balance of the payer would go negative.
|
||||
We then attempt to pay the invoice through the backend.
|
||||
If the payment is successful, we update the payment in the database with the payment details.
|
||||
First, we create a temporary payment in the database with fees set to the reserve
|
||||
fee. We then check whether the balance of the payer would go negative.
|
||||
We then attempt to pay the invoice through the backend. If the payment is
|
||||
successful, we update the payment in the database with the payment details.
|
||||
If the payment is unsuccessful, we delete the temporary payment.
|
||||
If the payment is still in flight, we hope that some other process will regularly check for the payment.
|
||||
If the payment is still in flight, we hope that some other process
|
||||
will regularly check for the payment.
|
||||
"""
|
||||
invoice = bolt11.decode(payment_request)
|
||||
fee_reserve_msat = fee_reserve(invoice.amount_msat)
|
||||
async with (db.reuse_conn(conn) if conn else db.connect()) as conn:
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||
temp_id = invoice.payment_hash
|
||||
internal_id = f"internal_{invoice.payment_hash}"
|
||||
|
||||
@ -151,11 +152,13 @@ async def pay_invoice(
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
# we check if an internal invoice exists that has already been paid (not pending anymore)
|
||||
# we check if an internal invoice exists that has already been paid
|
||||
# (not pending anymore)
|
||||
if not await check_internal_pending(invoice.payment_hash, conn=conn):
|
||||
raise PaymentFailure("Internal invoice already paid.")
|
||||
|
||||
# check_internal() returns the checking_id of the invoice we're waiting for (pending only)
|
||||
# check_internal() returns the checking_id of the invoice we're waiting for
|
||||
# (pending only)
|
||||
internal_checking_id = await check_internal(invoice.payment_hash, conn=conn)
|
||||
if internal_checking_id:
|
||||
# perform additional checks on the internal payment
|
||||
@ -202,7 +205,8 @@ async def pay_invoice(
|
||||
logger.debug("balance is too low, deleting temporary payment")
|
||||
if not internal_checking_id and wallet.balance_msat > -fee_reserve_msat:
|
||||
raise PaymentFailure(
|
||||
f"You must reserve at least ({round(fee_reserve_msat/1000)} sat) to cover potential routing fees."
|
||||
f"You must reserve at least ({round(fee_reserve_msat/1000)} sat) to"
|
||||
" cover potential routing fees."
|
||||
)
|
||||
raise PermissionError("Insufficient balance.")
|
||||
|
||||
@ -232,7 +236,8 @@ async def pay_invoice(
|
||||
|
||||
if payment.checking_id and payment.checking_id != temp_id:
|
||||
logger.warning(
|
||||
f"backend sent unexpected checking_id (expected: {temp_id} got: {payment.checking_id})"
|
||||
f"backend sent unexpected checking_id (expected: {temp_id} got:"
|
||||
f" {payment.checking_id})"
|
||||
)
|
||||
|
||||
logger.debug(f"backend: pay_invoice finished {temp_id}")
|
||||
@ -267,7 +272,8 @@ async def pay_invoice(
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"didn't receive checking_id from backend, payment may be stuck in database: {temp_id}"
|
||||
"didn't receive checking_id from backend, payment may be stuck in"
|
||||
f" database: {temp_id}"
|
||||
)
|
||||
|
||||
return invoice.payment_hash
|
||||
@ -301,7 +307,8 @@ async def redeem_lnurl_withdraw(
|
||||
)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
f"failed to create invoice on redeem_lnurl_withdraw from {lnurl}. params: {res}"
|
||||
f"failed to create invoice on redeem_lnurl_withdraw "
|
||||
f"from {lnurl}. params: {res}"
|
||||
)
|
||||
return None
|
||||
|
||||
@ -420,7 +427,8 @@ async def check_transaction_status(
|
||||
return status
|
||||
|
||||
|
||||
# WARN: this same value must be used for balance check and passed to WALLET.pay_invoice(), it may cause a vulnerability if the values differ
|
||||
# WARN: this same value must be used for balance check and passed to
|
||||
# WALLET.pay_invoice(), it may cause a vulnerability if the values differ
|
||||
def fee_reserve(amount_msat: int) -> int:
|
||||
reserve_min = settings.lnbits_reserve_fee_min
|
||||
reserve_percent = settings.lnbits_reserve_fee_percent
|
||||
|
@ -48,7 +48,8 @@ async def killswitch_task():
|
||||
await switch_to_voidwallet()
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
logger.error(
|
||||
f"Cannot fetch lnbits status manifest. {settings.lnbits_status_manifest}"
|
||||
"Cannot fetch lnbits status manifest."
|
||||
f" {settings.lnbits_status_manifest}"
|
||||
)
|
||||
await asyncio.sleep(settings.lnbits_killswitch_interval * 60)
|
||||
|
||||
@ -80,8 +81,8 @@ async def watchdog_task():
|
||||
|
||||
def register_task_listeners():
|
||||
"""
|
||||
Registers an invoice listener queue for the core tasks.
|
||||
Incoming payaments in this queue will eventually trigger the signals sent to all other extensions
|
||||
Registers an invoice listener queue for the core tasks. Incoming payments in this
|
||||
queue will eventually trigger the signals sent to all other extensions
|
||||
and fulfill other core tasks such as dispatching webhooks.
|
||||
"""
|
||||
invoice_paid_queue = asyncio.Queue(5)
|
||||
@ -93,7 +94,8 @@ def register_task_listeners():
|
||||
|
||||
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
||||
"""
|
||||
This worker dispatches events to all extensions, dispatches webhooks and balance notifys.
|
||||
This worker dispatches events to all extensions,
|
||||
dispatches webhooks and balance notifys.
|
||||
"""
|
||||
while True:
|
||||
payment = await invoice_paid_queue.get()
|
||||
@ -135,11 +137,15 @@ async def dispatch_webhook(payment: Payment):
|
||||
"""
|
||||
Dispatches the webhook to the webhook url.
|
||||
"""
|
||||
logger.debug("sending webhook", payment.webhook)
|
||||
|
||||
if not payment.webhook:
|
||||
return await mark_webhook_sent(payment, -1)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
data = payment.dict()
|
||||
try:
|
||||
logger.debug("sending webhook", payment.webhook)
|
||||
r = await client.post(payment.webhook, json=data, timeout=40) # type: ignore
|
||||
r = await client.post(payment.webhook, json=data, timeout=40)
|
||||
await mark_webhook_sent(payment, r.status_code)
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
await mark_webhook_sent(payment, -1)
|
||||
|
@ -126,10 +126,10 @@ async def api_download_backup() -> FileResponse:
|
||||
p = urlparse(db_url)
|
||||
command = (
|
||||
f"pg_dump --host={p.hostname} "
|
||||
f'--dbname={p.path.replace("/", "")} '
|
||||
f"--dbname={p.path.replace('/', '')} "
|
||||
f"--username={p.username} "
|
||||
f"--no-password "
|
||||
f"--format=c "
|
||||
"--no-password "
|
||||
"--format=c "
|
||||
f"--file={pg_backup_filename}"
|
||||
)
|
||||
proc = Popen(
|
||||
|
@ -234,8 +234,9 @@ async def api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
|
||||
internal=data.internal,
|
||||
conn=conn,
|
||||
)
|
||||
# NOTE: we get the checking_id with a seperate query because create_invoice does not return it
|
||||
# and it would be a big hustle to change its return type (used across extensions)
|
||||
# NOTE: we get the checking_id with a seperate query because create_invoice
|
||||
# does not return it and it would be a big hustle to change its return type
|
||||
# (used across extensions)
|
||||
payment_db = await get_standalone_payment(payment_hash, conn=conn)
|
||||
assert payment_db is not None, "payment not found"
|
||||
checking_id = payment_db.checking_id
|
||||
@ -309,12 +310,13 @@ async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
|
||||
"/api/v1/payments",
|
||||
summary="Create or pay an invoice",
|
||||
description="""
|
||||
This endpoint can be used both to generate and pay a BOLT11 invoice.
|
||||
To generate a new invoice for receiving funds into the authorized account,
|
||||
specify at least the first four fields in the POST body: `out: false`, `amount`, `unit`, and `memo`.
|
||||
To pay an arbitrary invoice from the funds already in the authorized account,
|
||||
specify `out: true` and use the `bolt11` field to supply the BOLT11 invoice to be paid.
|
||||
""",
|
||||
This endpoint can be used both to generate and pay a BOLT11 invoice.
|
||||
To generate a new invoice for receiving funds into the authorized account,
|
||||
specify at least the first four fields in the POST body: `out: false`,
|
||||
`amount`, `unit`, and `memo`. To pay an arbitrary invoice from the funds
|
||||
already in the authorized account, specify `out: true` and use the `bolt11`
|
||||
field to supply the BOLT11 invoice to be paid.
|
||||
""",
|
||||
status_code=HTTPStatus.CREATED,
|
||||
)
|
||||
async def api_payments_create(
|
||||
@ -379,8 +381,10 @@ async def api_payments_pay_lnurl(
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=(
|
||||
f"{domain} returned an invalid invoice. Expected {data.amount} msat, "
|
||||
f"got {invoice.amount_msat}.",
|
||||
(
|
||||
f"{domain} returned an invalid invoice. Expected"
|
||||
f" {data.amount} msat, got {invoice.amount_msat}."
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@ -388,8 +392,10 @@ async def api_payments_pay_lnurl(
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=(
|
||||
f"{domain} returned an invalid invoice. Expected description_hash == "
|
||||
f"{data.description_hash}, got {invoice.description_hash}.",
|
||||
(
|
||||
f"{domain} returned an invalid invoice. Expected description_hash"
|
||||
f" == {data.description_hash}, got {invoice.description_hash}."
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -132,12 +132,12 @@ async def extensions_install(
|
||||
"isAvailable": ext.id in all_extensions,
|
||||
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
|
||||
"isActive": ext.id not in inactive_extensions,
|
||||
"latestRelease": dict(ext.latest_release)
|
||||
if ext.latest_release
|
||||
else None,
|
||||
"installedRelease": dict(ext.installed_release)
|
||||
if ext.installed_release
|
||||
else None,
|
||||
"latestRelease": (
|
||||
dict(ext.latest_release) if ext.latest_release else None
|
||||
),
|
||||
"installedRelease": (
|
||||
dict(ext.installed_release) if ext.installed_release else None
|
||||
),
|
||||
},
|
||||
installable_exts,
|
||||
)
|
||||
@ -160,13 +160,13 @@ async def extensions_install(
|
||||
"/wallet",
|
||||
response_class=HTMLResponse,
|
||||
description="""
|
||||
Args:
|
||||
|
||||
just **wallet_name**: create a new user, then create a new wallet for user with wallet_name<br>
|
||||
just **user_id**: return the first user wallet or create one if none found (with default wallet_name)<br>
|
||||
**user_id** and **wallet_name**: create a new wallet for user with wallet_name<br>
|
||||
**user_id** and **wallet_id**: return that wallet if user is the owner<br>
|
||||
nothing: create everything<br>
|
||||
just **wallet_name**: create a new user, then create a new wallet
|
||||
for user with wallet_name
|
||||
just **user_id**: return the first user wallet or create one if none found
|
||||
(with default wallet_name)
|
||||
**user_id** and **wallet_name**: create a new wallet for user with wallet_name
|
||||
**user_id** and **wallet_id**: return that wallet if user is the owner
|
||||
nothing: create everything
|
||||
""",
|
||||
)
|
||||
async def wallet(
|
||||
@ -210,7 +210,8 @@ async def wallet(
|
||||
else:
|
||||
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name)
|
||||
logger.info(
|
||||
f"Created new wallet {wallet_name if wallet_name else '(no name)'} for user {user.id}"
|
||||
f"Created new wallet {wallet_name if wallet_name else '(no name)'} for"
|
||||
f" user {user.id}"
|
||||
)
|
||||
|
||||
return RedirectResponse(
|
||||
@ -219,7 +220,9 @@ async def wallet(
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"Access {'user '+ user.id + ' ' if user else ''} {'wallet ' + wallet_name if wallet_name else ''}"
|
||||
"Access "
|
||||
f"{'user '+ user.id + ' ' if user else ''} "
|
||||
f"{'wallet ' + wallet_name if wallet_name else ''}"
|
||||
)
|
||||
userwallet = user.get_wallet(wallet_id)
|
||||
if not userwallet:
|
||||
@ -255,7 +258,9 @@ async def lnurl_full_withdraw(request: Request):
|
||||
"k1": "0",
|
||||
"minWithdrawable": 1000 if wallet.withdrawable_balance else 0,
|
||||
"maxWithdrawable": wallet.withdrawable_balance,
|
||||
"defaultDescription": f"{settings.lnbits_site_title} balance withdraw from {wallet.id[0:5]}",
|
||||
"defaultDescription": (
|
||||
f"{settings.lnbits_site_title} balance withdraw from {wallet.id[0:5]}"
|
||||
),
|
||||
"balanceCheck": url_for("/withdraw", external=True, usr=user.id, wal=wallet.id),
|
||||
}
|
||||
|
||||
@ -362,9 +367,11 @@ async def manifest(usr: str):
|
||||
"name": settings.lnbits_site_title + " Wallet",
|
||||
"icons": [
|
||||
{
|
||||
"src": settings.lnbits_custom_logo
|
||||
if settings.lnbits_custom_logo
|
||||
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png",
|
||||
"src": (
|
||||
settings.lnbits_custom_logo
|
||||
if settings.lnbits_custom_logo
|
||||
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png"
|
||||
),
|
||||
"type": "image/png",
|
||||
"sizes": "900x900",
|
||||
}
|
||||
|
@ -421,8 +421,8 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
Generic helper class for filtering and sorting data.
|
||||
For usage in an api endpoint, use the `parse_filters` dependency.
|
||||
|
||||
When constructing this class manually always make sure to pass a model so that the values can be validated.
|
||||
Otherwise, make sure to validate the inputs manually.
|
||||
When constructing this class manually always make sure to pass a model so that
|
||||
the values can be validated. Otherwise, make sure to validate the inputs manually.
|
||||
"""
|
||||
|
||||
filters: List[Filter[TFilterModel]] = []
|
||||
|
@ -49,16 +49,16 @@ class KeyChecker(SecurityBase):
|
||||
if self._api_key
|
||||
else request.headers.get("X-API-KEY") or request.query_params["api-key"]
|
||||
)
|
||||
# FIXME: Find another way to validate the key. A fetch from DB should be avoided here.
|
||||
# Also, we should not return the wallet here - thats silly.
|
||||
# Possibly store it in a Redis DB
|
||||
self.wallet = await get_wallet_for_key(key_value, self._key_type) # type: ignore
|
||||
if not self.wallet:
|
||||
# FIXME: Find another way to validate the key. A fetch from DB should be
|
||||
# avoided here. Also, we should not return the wallet here - thats
|
||||
# silly. Possibly store it in a Redis DB
|
||||
wallet = await get_wallet_for_key(key_value, self._key_type)
|
||||
self.wallet = wallet # type: ignore
|
||||
if not wallet:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.UNAUTHORIZED,
|
||||
detail="Invalid key or expired key.",
|
||||
)
|
||||
|
||||
except KeyError:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="`X-API-KEY` header missing."
|
||||
@ -156,7 +156,8 @@ async def get_key_type(
|
||||
if exc.status_code == HTTPStatus.BAD_REQUEST:
|
||||
raise
|
||||
elif exc.status_code == HTTPStatus.UNAUTHORIZED:
|
||||
# we pass this in case it is not an invoice key, nor an admin key, and then return NOT_FOUND at the end of this block
|
||||
# we pass this in case it is not an invoice key, nor an admin key,
|
||||
# and then return NOT_FOUND at the end of this block
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
@ -426,7 +426,10 @@ class InstallableExtension(BaseModel):
|
||||
logger.success(f"Extension {self.name} ({self.installed_version}) installed.")
|
||||
|
||||
def nofiy_upgrade(self) -> None:
|
||||
"""Update the list of upgraded extensions. The middleware will perform redirects based on this"""
|
||||
"""
|
||||
Update the list of upgraded extensions. The middleware will perform
|
||||
redirects based on this
|
||||
"""
|
||||
|
||||
clean_upgraded_exts = list(
|
||||
filter(
|
||||
|
@ -93,9 +93,11 @@ def get_current_extension_name() -> str:
|
||||
|
||||
def generate_filter_params_openapi(model: Type[FilterModel], keep_optional=False):
|
||||
"""
|
||||
Generate openapi documentation for Filters. This is intended to be used along parse_filters (see example)
|
||||
Generate openapi documentation for Filters. This is intended to be used along
|
||||
parse_filters (see example)
|
||||
:param model: Filter model
|
||||
:param keep_optional: If false, all parameters will be optional, otherwise inferred from model
|
||||
:param keep_optional: If false, all parameters will be optional,
|
||||
otherwise inferred from model
|
||||
"""
|
||||
fields = list(model.__fields__.values())
|
||||
params = []
|
||||
|
@ -18,7 +18,8 @@ from lnbits.settings import settings
|
||||
class InstalledExtensionMiddleware:
|
||||
# This middleware class intercepts calls made to the extensions API and:
|
||||
# - it blocks the calls if the extension has been disabled or uninstalled.
|
||||
# - it redirects the calls to the latest version of the extension if the extension has been upgraded.
|
||||
# - it redirects the calls to the latest version of the extension
|
||||
# if the extension has been upgraded.
|
||||
# - otherwise it has no effect
|
||||
def __init__(self, app: ASGIApp) -> None:
|
||||
self.app = app
|
||||
@ -89,9 +90,10 @@ class InstalledExtensionMiddleware:
|
||||
self, headers: List[Any], msg: str, status_code: HTTPStatus
|
||||
) -> Union[HTMLResponse, JSONResponse]:
|
||||
"""
|
||||
Build an HTTP response containing the `msg` as HTTP body and the `status_code` as HTTP code.
|
||||
If the `accept` HTTP header is present int the request and contains the value of `text/html`
|
||||
then return an `HTMLResponse`, otherwise return an `JSONResponse`.
|
||||
Build an HTTP response containing the `msg` as HTTP body and the `status_code`
|
||||
as HTTP code. If the `accept` HTTP header is present int the request and
|
||||
contains the value of `text/html` then return an `HTMLResponse`,
|
||||
otherwise return an `JSONResponse`.
|
||||
"""
|
||||
accept_header: str = next(
|
||||
(
|
||||
@ -129,8 +131,8 @@ class CustomGZipMiddleware(GZipMiddleware):
|
||||
|
||||
|
||||
class ExtensionsRedirectMiddleware:
|
||||
# Extensions are allowed to specify redirect paths.
|
||||
# A call to a path outside the scope of the extension can be redirected to one of the extension's endpoints.
|
||||
# Extensions are allowed to specify redirect paths. A call to a path outside the
|
||||
# scope of the extension can be redirected to one of the extension's endpoints.
|
||||
# Eg: redirect `GET /.well-known` to `GET /lnurlp/api/v1/well-known`
|
||||
|
||||
def __init__(self, app: ASGIApp) -> None:
|
||||
@ -231,7 +233,8 @@ def add_ip_block_middleware(app: FastAPI):
|
||||
status_code=403, # Forbidden
|
||||
content={"detail": "IP is blocked"},
|
||||
)
|
||||
# this try: except: block is not needed on latest FastAPI (await call_next(request) is enough)
|
||||
# this try: except: block is not needed on latest FastAPI
|
||||
# (await call_next(request) is enough)
|
||||
# https://stackoverflow.com/questions/71222144/runtimeerror-no-response-returned-in-fastapi-when-refresh-request
|
||||
# TODO: remove after https://github.com/lnbits/lnbits/pull/1609 is merged
|
||||
try:
|
||||
|
@ -46,7 +46,8 @@ def main(
|
||||
|
||||
set_cli_settings(host=host, port=port, forwarded_allow_ips=forwarded_allow_ips)
|
||||
|
||||
# this beautiful beast parses all command line arguments and passes them to the uvicorn server
|
||||
# this beautiful beast parses all command line arguments and
|
||||
# passes them to the uvicorn server
|
||||
d = dict()
|
||||
for a in ctx.args:
|
||||
item = a.split("=")
|
||||
|
@ -113,7 +113,9 @@ class SecuritySettings(LNbitsSettings):
|
||||
lnbits_watchdog_interval: int = Field(default=60)
|
||||
lnbits_watchdog_delta: int = Field(default=1_000_000)
|
||||
lnbits_status_manifest: str = Field(
|
||||
default="https://raw.githubusercontent.com/lnbits/lnbits-status/main/manifest.json"
|
||||
default=(
|
||||
"https://raw.githubusercontent.com/lnbits/lnbits-status/main/manifest.json"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ -376,7 +378,8 @@ def send_admin_user_to_saas():
|
||||
logger.success("sent super_user to saas application")
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"error sending super_user to saas: {settings.lnbits_saas_callback}. Error: {str(e)}"
|
||||
"error sending super_user to saas:"
|
||||
f" {settings.lnbits_saas_callback}. Error: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
|
@ -87,8 +87,8 @@ invoice_listeners: Dict[str, asyncio.Queue] = SseListenersDict("invoice_listener
|
||||
|
||||
def register_invoice_listener(send_chan: asyncio.Queue, name: Optional[str] = None):
|
||||
"""
|
||||
A method intended for extensions (and core/tasks.py) to call when they want to be notified about
|
||||
new invoice payments incoming. Will emit all incoming payments.
|
||||
A method intended for extensions (and core/tasks.py) to call when they want to be
|
||||
notified about new invoice payments incoming. Will emit all incoming payments.
|
||||
"""
|
||||
name_unique = f"{name or 'no_name'}_{str(uuid.uuid4())[:8]}"
|
||||
logger.trace(f"sse: registering invoice listener {name_unique}")
|
||||
@ -147,7 +147,8 @@ async def check_pending_payments():
|
||||
while True:
|
||||
async with db.connect() as conn:
|
||||
logger.info(
|
||||
f"Task: checking all pending payments (incoming={incoming}, outgoing={outgoing}) of last 15 days"
|
||||
f"Task: checking all pending payments (incoming={incoming},"
|
||||
f" outgoing={outgoing}) of last 15 days"
|
||||
)
|
||||
start_time = time.time()
|
||||
pending_payments = await get_payments(
|
||||
@ -163,7 +164,8 @@ async def check_pending_payments():
|
||||
await payment.check_status(conn=conn)
|
||||
|
||||
logger.info(
|
||||
f"Task: pending check finished for {len(pending_payments)} payments (took {time.time() - start_time:0.3f} s)"
|
||||
f"Task: pending check finished for {len(pending_payments)} payments"
|
||||
f" (took {time.time() - start_time:0.3f} s)"
|
||||
)
|
||||
# we delete expired invoices once upon the first pending check
|
||||
if incoming:
|
||||
@ -171,7 +173,8 @@ async def check_pending_payments():
|
||||
start_time = time.time()
|
||||
await delete_expired_invoices(conn=conn)
|
||||
logger.info(
|
||||
f"Task: expired invoice deletion finished (took {time.time() - start_time:0.3f} s)"
|
||||
"Task: expired invoice deletion finished (took"
|
||||
f" {time.time() - start_time:0.3f} s)"
|
||||
)
|
||||
|
||||
# after the first check we will only check outgoing, not incoming
|
||||
|
@ -260,12 +260,15 @@ async def btc_price(currency: str) -> float:
|
||||
rate = float(provider.getter(data, replacements))
|
||||
await send_channel.put(rate)
|
||||
except (
|
||||
TypeError, # CoinMate returns HTTPStatus 200 but no data when a currency pair is not found
|
||||
KeyError, # Kraken's response dictionary doesn't include keys we look up for
|
||||
# CoinMate returns HTTPStatus 200 but no data when a pair is not found
|
||||
TypeError,
|
||||
# Kraken's response dictionary doesn't include keys we look up for
|
||||
KeyError,
|
||||
httpx.ConnectTimeout,
|
||||
httpx.ConnectError,
|
||||
httpx.ReadTimeout,
|
||||
httpx.HTTPStatusError, # Some providers throw a 404 when a currency pair is not found
|
||||
# Some providers throw a 404 when a currency pair is not found
|
||||
httpx.HTTPStatusError,
|
||||
):
|
||||
await send_channel.put(None)
|
||||
|
||||
|
@ -55,13 +55,16 @@ class ClicheWallet(Wallet):
|
||||
description_hash_str = (
|
||||
description_hash.hex()
|
||||
if description_hash
|
||||
else hashlib.sha256(unhashed_description).hexdigest()
|
||||
if unhashed_description
|
||||
else None
|
||||
else (
|
||||
hashlib.sha256(unhashed_description).hexdigest()
|
||||
if unhashed_description
|
||||
else None
|
||||
)
|
||||
)
|
||||
ws = create_connection(self.endpoint)
|
||||
ws.send(
|
||||
f"create-invoice --msatoshi {amount*1000} --description_hash {description_hash_str}"
|
||||
f"create-invoice --msatoshi {amount*1000} --description_hash"
|
||||
f" {description_hash_str}"
|
||||
)
|
||||
r = ws.recv()
|
||||
else:
|
||||
@ -172,7 +175,8 @@ class ClicheWallet(Wallet):
|
||||
continue
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
f"lost connection to cliche's invoices stream: '{exc}', retrying in 5 seconds"
|
||||
f"lost connection to cliche's invoices stream: '{exc}', retrying in"
|
||||
" 5 seconds"
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
continue
|
||||
|
@ -44,9 +44,8 @@ class CoreLightningWallet(Wallet):
|
||||
self.ln = LightningRpc(self.rpc)
|
||||
|
||||
# check if description_hash is supported (from corelightning>=v0.11.0)
|
||||
self.supports_description_hash = (
|
||||
"deschashonly" in self.ln.help("invoice")["help"][0]["command"] # type: ignore
|
||||
)
|
||||
command = self.ln.help("invoice")["help"][0]["command"] # type: ignore
|
||||
self.supports_description_hash = "deschashonly" in command
|
||||
|
||||
# check last payindex so we can listen from that point on
|
||||
self.last_pay_index = 0
|
||||
@ -79,20 +78,21 @@ class CoreLightningWallet(Wallet):
|
||||
try:
|
||||
if description_hash and not unhashed_description:
|
||||
raise Unsupported(
|
||||
"'description_hash' unsupported by CoreLightning, provide 'unhashed_description'"
|
||||
"'description_hash' unsupported by CoreLightning, provide"
|
||||
" 'unhashed_description'"
|
||||
)
|
||||
if unhashed_description and not self.supports_description_hash:
|
||||
raise Unsupported("unhashed_description")
|
||||
r: dict = self.ln.invoice( # type: ignore
|
||||
msatoshi=msat,
|
||||
label=label,
|
||||
description=unhashed_description.decode()
|
||||
if unhashed_description
|
||||
else memo,
|
||||
description=(
|
||||
unhashed_description.decode() if unhashed_description else memo
|
||||
),
|
||||
exposeprivatechannels=True,
|
||||
deschashonly=True
|
||||
if unhashed_description
|
||||
else False, # we can't pass None here
|
||||
deschashonly=(
|
||||
True if unhashed_description else False
|
||||
), # we can't pass None here
|
||||
expiry=kwargs.get("expiry"),
|
||||
)
|
||||
|
||||
@ -101,7 +101,10 @@ class CoreLightningWallet(Wallet):
|
||||
|
||||
return InvoiceResponse(True, r["payment_hash"], r["bolt11"], "")
|
||||
except RpcError as exc:
|
||||
error_message = f"CoreLightning method '{exc.method}' failed with '{exc.error.get('message') or exc.error}'." # type: ignore
|
||||
error_message = (
|
||||
f"CoreLightning method '{exc.method}' failed with"
|
||||
f" '{exc.error.get('message') or exc.error}'." # type: ignore
|
||||
)
|
||||
return InvoiceResponse(False, None, None, error_message)
|
||||
except Exception as e:
|
||||
return InvoiceResponse(False, None, None, str(e))
|
||||
@ -114,11 +117,12 @@ class CoreLightningWallet(Wallet):
|
||||
return PaymentResponse(False, None, None, None, "invoice already paid")
|
||||
|
||||
fee_limit_percent = fee_limit_msat / invoice.amount_msat * 100
|
||||
|
||||
# so fee_limit_percent is applied even on payments with fee < 5000 millisatoshi
|
||||
# (which is default value of exemptfee)
|
||||
payload = {
|
||||
"bolt11": bolt11,
|
||||
"maxfeepercent": f"{fee_limit_percent:.11}",
|
||||
"exemptfee": 0, # so fee_limit_percent is applied even on payments with fee < 5000 millisatoshi (which is default value of exemptfee)
|
||||
"exemptfee": 0,
|
||||
}
|
||||
try:
|
||||
wrapped = async_wrap(_pay_invoice)
|
||||
@ -127,7 +131,10 @@ class CoreLightningWallet(Wallet):
|
||||
try:
|
||||
error_message = exc.error["attempts"][-1]["fail_reason"] # type: ignore
|
||||
except Exception:
|
||||
error_message = f"CoreLightning method '{exc.method}' failed with '{exc.error.get('message') or exc.error}'." # type: ignore
|
||||
error_message = (
|
||||
f"CoreLightning method '{exc.method}' failed with"
|
||||
f" '{exc.error.get('message') or exc.error}'." # type: ignore
|
||||
)
|
||||
return PaymentResponse(False, None, None, None, error_message)
|
||||
except Exception as exc:
|
||||
return PaymentResponse(False, None, None, None, str(exc))
|
||||
@ -192,6 +199,7 @@ class CoreLightningWallet(Wallet):
|
||||
yield paid["payment_hash"]
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
f"lost connection to corelightning invoices stream: '{exc}', retrying in 5 seconds"
|
||||
f"lost connection to corelightning invoices stream: '{exc}', "
|
||||
"retrying in 5 seconds"
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
@ -225,6 +225,7 @@ class EclairWallet(Wallet):
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
f"lost connection to eclair invoices stream: '{exc}', retrying in 5 seconds"
|
||||
f"lost connection to eclair invoices stream: '{exc}'"
|
||||
"retrying in 5 seconds"
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
@ -31,7 +31,8 @@ class FakeWallet(Wallet):
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
logger.info(
|
||||
"FakeWallet funding source is for using LNbits as a centralised, stand-alone payment system with brrrrrr."
|
||||
"FakeWallet funding source is for using LNbits as a centralised,"
|
||||
" stand-alone payment system with brrrrrr."
|
||||
)
|
||||
return StatusResponse(None, 1000000000)
|
||||
|
||||
|
@ -100,7 +100,8 @@ class LndWallet(Wallet):
|
||||
def __init__(self):
|
||||
if not imports_ok: # pragma: nocover
|
||||
raise ImportError(
|
||||
"The `grpcio` and `protobuf` library must be installed to use `GRPC LndWallet`. Alternatively try using the LndRESTWallet."
|
||||
"The `grpcio` and `protobuf` library must be installed to use `GRPC"
|
||||
" LndWallet`. Alternatively try using the LndRESTWallet."
|
||||
)
|
||||
|
||||
endpoint = settings.lnd_grpc_endpoint
|
||||
@ -305,6 +306,7 @@ class LndWallet(Wallet):
|
||||
yield checking_id
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
f"lost connection to lnd invoices stream: '{exc}', retrying in 5 seconds"
|
||||
f"lost connection to lnd invoices stream: '{exc}', "
|
||||
"retrying in 5 seconds"
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
@ -48,7 +48,8 @@ class LndRestWallet(Wallet):
|
||||
|
||||
if not cert:
|
||||
logger.warning(
|
||||
"no certificate for lndrest provided, this only works if you have a publicly issued certificate"
|
||||
"no certificate for lndrest provided, this only works if you have a"
|
||||
" publicly issued certificate"
|
||||
)
|
||||
|
||||
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||
@ -223,6 +224,7 @@ class LndRestWallet(Wallet):
|
||||
yield payment_hash
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
f"lost connection to lnd invoices stream: '{exc}', retrying in 5 seconds"
|
||||
f"lost connection to lnd invoices stream: '{exc}', retrying in 5"
|
||||
" seconds"
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
@ -50,7 +50,8 @@ class LNPayWallet(Wallet):
|
||||
data = r.json()
|
||||
if data["statusType"]["name"] != "active":
|
||||
return StatusResponse(
|
||||
f"Wallet {data['user_label']} (data['id']) not active, but {data['statusType']['name']}",
|
||||
f"Wallet {data['user_label']} (data['id']) not active, but"
|
||||
f" {data['statusType']['name']}",
|
||||
0,
|
||||
)
|
||||
|
||||
|
@ -164,6 +164,7 @@ class LnTipsWallet(Wallet):
|
||||
# since the backend is expected to drop the connection after 90s
|
||||
if last_connected is None or time.time() - last_connected < 10:
|
||||
logger.error(
|
||||
f"lost connection to {self.endpoint}/api/v1/invoicestream, retrying in 5 seconds"
|
||||
f"lost connection to {self.endpoint}/api/v1/invoicestream, retrying"
|
||||
" in 5 seconds"
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
@ -46,7 +46,8 @@ class SparkWallet(Wallet):
|
||||
async def call(*args, **kwargs):
|
||||
if args and kwargs:
|
||||
raise TypeError(
|
||||
f"must supply either named arguments or a list of arguments, not both: {args} {kwargs}"
|
||||
"must supply either named arguments or a list of arguments, not"
|
||||
f" both: {args} {kwargs}"
|
||||
)
|
||||
elif args:
|
||||
params = args
|
||||
@ -161,7 +162,8 @@ class SparkWallet(Wallet):
|
||||
|
||||
if len(pays) > 1:
|
||||
raise SparkError(
|
||||
f"listpays({payment_hash}) returned an unexpected response: {listpays}"
|
||||
f"listpays({payment_hash}) returned an unexpected response:"
|
||||
f" {listpays}"
|
||||
)
|
||||
|
||||
if pay["status"] == "failed":
|
||||
|
@ -19,10 +19,9 @@ class VoidWallet(Wallet):
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
logger.warning(
|
||||
(
|
||||
"This backend does nothing, it is here just as a placeholder, you must configure an "
|
||||
"actual backend before being able to do anything useful with LNbits."
|
||||
)
|
||||
"This backend does nothing, it is here just as a placeholder, you must"
|
||||
" configure an actual backend before being able to do anything useful with"
|
||||
" LNbits."
|
||||
)
|
||||
return StatusResponse(None, 0)
|
||||
|
||||
|
@ -103,11 +103,9 @@ testpaths = [
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
# line-length = 150
|
||||
# previously experimental-string-processing = true
|
||||
# this should autoformat string poperly but does not work
|
||||
line-length = 88
|
||||
# use upcoming new features
|
||||
# preview = true
|
||||
target-versions = ["py39"]
|
||||
extend-exclude = """(
|
||||
lnbits/static
|
||||
| lnbits/extensions
|
||||
@ -116,14 +114,14 @@ extend-exclude = """(
|
||||
)"""
|
||||
|
||||
[tool.ruff]
|
||||
# Same as Black.
|
||||
line-length = 150
|
||||
# Same as Black. + 10% rule of black
|
||||
line-length = 88
|
||||
|
||||
# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
|
||||
# (`I`) is for `isort`.
|
||||
select = ["E", "F", "I"]
|
||||
ignore = [
|
||||
"E402", # Module level import not at top of file
|
||||
"E501", # Line length
|
||||
]
|
||||
|
||||
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||
|
@ -210,10 +210,10 @@ async def test_pay_invoice_adminkey(client, invoice, adminkey_headers_from):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_payments(client, from_wallet, adminkey_headers_from):
|
||||
# Because sqlite only stores timestamps with milliseconds we have to wait a second to ensure
|
||||
# a different timestamp than previous invoices
|
||||
# due to this limitation both payments (normal and paginated) are tested at the same time as they are almost
|
||||
# identical anyways
|
||||
# Because sqlite only stores timestamps with milliseconds we have to wait a second
|
||||
# to ensure a different timestamp than previous invoices due to this limitation
|
||||
# both payments (normal and paginated) are tested at the same time as they are
|
||||
# almost identical anyways
|
||||
if DB_TYPE == SQLITE:
|
||||
await asyncio.sleep(1)
|
||||
ts = time()
|
||||
|
@ -34,7 +34,10 @@ docker_lightning = f"{docker_cmd} {docker_prefix}-lnd-1-1"
|
||||
docker_lightning_cli = f"{docker_lightning} lncli --network regtest --rpcserver=lnd-1"
|
||||
|
||||
docker_bitcoin = f"{docker_cmd} {docker_prefix}-bitcoind-1-1"
|
||||
docker_bitcoin_cli = f"{docker_bitcoin} bitcoin-cli -rpcuser={docker_bitcoin_rpc} -rpcpassword={docker_bitcoin_rpc} -regtest"
|
||||
docker_bitcoin_cli = (
|
||||
f"{docker_bitcoin} bitcoin-cli"
|
||||
f" -rpcuser={docker_bitcoin_rpc} -rpcpassword={docker_bitcoin_rpc} -regtest"
|
||||
)
|
||||
|
||||
|
||||
def run_cmd(cmd: str) -> str:
|
||||
|
@ -55,7 +55,8 @@ def check_db_versions(sqdb):
|
||||
version = dbpost[key]
|
||||
if value != version:
|
||||
raise Exception(
|
||||
f"sqlite database version ({value}) of {key} doesn't match postgres database version {version}"
|
||||
f"sqlite database version ({value}) of {key} doesn't match postgres"
|
||||
f" database version {version}"
|
||||
)
|
||||
|
||||
connection = postgres.connection
|
||||
@ -174,7 +175,10 @@ parser.add_argument(
|
||||
dest="sqlite_path",
|
||||
const=True,
|
||||
nargs="?",
|
||||
help=f"SQLite DB folder *or* single extension db file to migrate. Default: {sqfolder}",
|
||||
help=(
|
||||
"SQLite DB folder *or* single extension db file to migrate. Default:"
|
||||
f" {sqfolder}"
|
||||
),
|
||||
default=sqfolder,
|
||||
type=str,
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user