diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e391a186b..1a64c9c57 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 ] diff --git a/lnbits/app.py b/lnbits/app.py index 6fceb9364..e27824cfb 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -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" + ) ) diff --git a/lnbits/commands.py b/lnbits/commands.py index 03ab04f1b..a8471fe17 100644 --- a/lnbits/commands.py +++ b/lnbits/commands.py @@ -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: diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index dc8991f61..10b9a02fd 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -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 diff --git a/lnbits/core/helpers.py b/lnbits/core/helpers.py index a5502a506..12eb97a1c 100644 --- a/lnbits/core/helpers.py +++ b/lnbits/core/helpers.py @@ -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: diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index 09f930a0d..6c3743cb0 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -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( """ diff --git a/lnbits/core/services.py b/lnbits/core/services.py index 3b9f1b1dd..c12525653 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -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 diff --git a/lnbits/core/tasks.py b/lnbits/core/tasks.py index 5f7f67bb6..f2a8ce6ad 100644 --- a/lnbits/core/tasks.py +++ b/lnbits/core/tasks.py @@ -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) diff --git a/lnbits/core/views/admin_api.py b/lnbits/core/views/admin_api.py index ea37fc6f5..a14583b9b 100644 --- a/lnbits/core/views/admin_api.py +++ b/lnbits/core/views/admin_api.py @@ -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( diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 704428a2f..b48f3da65 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -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}." + ), ), ) diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index 7b36b4b7b..b9694f0e7 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -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
-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
+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", } diff --git a/lnbits/db.py b/lnbits/db.py index 9c692f519..281188c0d 100644 --- a/lnbits/db.py +++ b/lnbits/db.py @@ -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]] = [] diff --git a/lnbits/decorators.py b/lnbits/decorators.py index 70f653d46..c23f03a7b 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -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 diff --git a/lnbits/extension_manager.py b/lnbits/extension_manager.py index 957037474..d4a17a202 100644 --- a/lnbits/extension_manager.py +++ b/lnbits/extension_manager.py @@ -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( diff --git a/lnbits/helpers.py b/lnbits/helpers.py index 065d0d9b0..59252d795 100644 --- a/lnbits/helpers.py +++ b/lnbits/helpers.py @@ -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 = [] diff --git a/lnbits/middleware.py b/lnbits/middleware.py index 2944702e4..83b2dcd1b 100644 --- a/lnbits/middleware.py +++ b/lnbits/middleware.py @@ -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: diff --git a/lnbits/server.py b/lnbits/server.py index c000d4dd5..d566702a6 100644 --- a/lnbits/server.py +++ b/lnbits/server.py @@ -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("=") diff --git a/lnbits/settings.py b/lnbits/settings.py index 1c5db56dc..9315c46f0 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -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)}" ) diff --git a/lnbits/tasks.py b/lnbits/tasks.py index 2f95a42be..db4c1f1a8 100644 --- a/lnbits/tasks.py +++ b/lnbits/tasks.py @@ -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 diff --git a/lnbits/utils/exchange_rates.py b/lnbits/utils/exchange_rates.py index 2801146b5..b1ceb26fd 100644 --- a/lnbits/utils/exchange_rates.py +++ b/lnbits/utils/exchange_rates.py @@ -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) diff --git a/lnbits/wallets/cliche.py b/lnbits/wallets/cliche.py index e6d6af7cb..36894d0ee 100644 --- a/lnbits/wallets/cliche.py +++ b/lnbits/wallets/cliche.py @@ -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 diff --git a/lnbits/wallets/corelightning.py b/lnbits/wallets/corelightning.py index e29b08094..7615222ed 100644 --- a/lnbits/wallets/corelightning.py +++ b/lnbits/wallets/corelightning.py @@ -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) diff --git a/lnbits/wallets/eclair.py b/lnbits/wallets/eclair.py index 62f1b5ccc..ac6c5e42b 100644 --- a/lnbits/wallets/eclair.py +++ b/lnbits/wallets/eclair.py @@ -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) diff --git a/lnbits/wallets/fake.py b/lnbits/wallets/fake.py index 62f2bc44c..9fcc2e913 100644 --- a/lnbits/wallets/fake.py +++ b/lnbits/wallets/fake.py @@ -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) diff --git a/lnbits/wallets/lndgrpc.py b/lnbits/wallets/lndgrpc.py index 0cbe7dde8..b74211904 100644 --- a/lnbits/wallets/lndgrpc.py +++ b/lnbits/wallets/lndgrpc.py @@ -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) diff --git a/lnbits/wallets/lndrest.py b/lnbits/wallets/lndrest.py index d8d51fd64..119fa10ee 100644 --- a/lnbits/wallets/lndrest.py +++ b/lnbits/wallets/lndrest.py @@ -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) diff --git a/lnbits/wallets/lnpay.py b/lnbits/wallets/lnpay.py index 919af8f78..9c7e3a0e7 100644 --- a/lnbits/wallets/lnpay.py +++ b/lnbits/wallets/lnpay.py @@ -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, ) diff --git a/lnbits/wallets/lntips.py b/lnbits/wallets/lntips.py index f19f9c4fd..048727061 100644 --- a/lnbits/wallets/lntips.py +++ b/lnbits/wallets/lntips.py @@ -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) diff --git a/lnbits/wallets/spark.py b/lnbits/wallets/spark.py index 2e3121094..a7c51ce42 100644 --- a/lnbits/wallets/spark.py +++ b/lnbits/wallets/spark.py @@ -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": diff --git a/lnbits/wallets/void.py b/lnbits/wallets/void.py index 1d0ff90a9..22eba7695 100644 --- a/lnbits/wallets/void.py +++ b/lnbits/wallets/void.py @@ -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) diff --git a/pyproject.toml b/pyproject.toml index b0c4ae8d9..f454da7f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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. diff --git a/tests/core/views/test_api.py b/tests/core/views/test_api.py index 7ff95c7e9..009b71e50 100644 --- a/tests/core/views/test_api.py +++ b/tests/core/views/test_api.py @@ -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() diff --git a/tests/helpers.py b/tests/helpers.py index 15754189e..4a3b3da9d 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -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: diff --git a/tools/conv.py b/tools/conv.py index 36bf0a808..1d1a42483 100644 --- a/tools/conv.py +++ b/tools/conv.py @@ -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, )