feat: use uv instead of poetry for CI, docker and development (#3325)

Co-authored-by: arcbtc <ben@arc.wales>
This commit is contained in:
dni ⚡ 2025-08-21 16:17:19 +02:00 committed by GitHub
parent 15984fa49b
commit 5ba06d42d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
88 changed files with 4265 additions and 1303 deletions

View file

@ -21,29 +21,17 @@ runs:
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: ${{ inputs.python-version }} python-version: ${{ inputs.python-version }}
# cache poetry install via pip
cache: "pip" cache: "pip"
- name: Set up Poetry - name: Install uv
uses: abatilo/actions-poetry@v2 uses: astral-sh/setup-uv@v6
- name: Setup a local virtual environment (if no poetry.toml file)
shell: bash
run: |
poetry config virtualenvs.create true --local
poetry config virtualenvs.in-project true --local
- uses: actions/cache@v4
name: Define a cache for the virtual environment based on the dependencies lock file
with: with:
path: ./.venv enable-cache: true
key: venv-${{ hashFiles('poetry.lock') }} python-version: ${{ inputs.python-version }}
- name: Install the project dependencies - name: Install the project dependencies
shell: bash shell: bash
run: | run: uv sync --locked --all-extras --dev
poetry env use python${{ inputs.python-version }}
poetry install --all-extras
- name: Use Node.js ${{ inputs.node-version }} - name: Use Node.js ${{ inputs.node-version }}
if: ${{ (inputs.npm == 'true') }} if: ${{ (inputs.npm == 'true') }}

View file

@ -20,7 +20,7 @@ jobs:
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"] db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
uses: ./.github/workflows/tests.yml uses: ./.github/workflows/tests.yml
with: with:
custom-pytest: "poetry run pytest tests/api" custom-pytest: "uv run pytest tests/api"
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
db-url: ${{ matrix.db-url }} db-url: ${{ matrix.db-url }}
secrets: secrets:
@ -34,7 +34,7 @@ jobs:
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"] db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
uses: ./.github/workflows/tests.yml uses: ./.github/workflows/tests.yml
with: with:
custom-pytest: "poetry run pytest tests/wallets" custom-pytest: "uv run pytest tests/wallets"
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
db-url: ${{ matrix.db-url }} db-url: ${{ matrix.db-url }}
secrets: secrets:
@ -48,7 +48,7 @@ jobs:
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"] db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
uses: ./.github/workflows/tests.yml uses: ./.github/workflows/tests.yml
with: with:
custom-pytest: "poetry run pytest tests/unit" custom-pytest: "uv run pytest tests/unit"
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
db-url: ${{ matrix.db-url }} db-url: ${{ matrix.db-url }}
secrets: secrets:
@ -77,7 +77,7 @@ jobs:
python-version: ["3.10"] python-version: ["3.10"]
backend-wallet-class: ["LndRestWallet", "LndWallet", "CoreLightningWallet", "CoreLightningRestWallet", "LNbitsWallet", "EclairWallet"] backend-wallet-class: ["LndRestWallet", "LndWallet", "CoreLightningWallet", "CoreLightningRestWallet", "LNbitsWallet", "EclairWallet"]
with: with:
custom-pytest: "poetry run pytest tests/regtest" custom-pytest: "uv run pytest tests/regtest"
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
backend-wallet-class: ${{ matrix.backend-wallet-class }} backend-wallet-class: ${{ matrix.backend-wallet-class }}
secrets: secrets:

View file

@ -25,7 +25,7 @@ jobs:
LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly, satspay, tipjar, tpos, lnurlp, withdraw" LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly, satspay, tipjar, tpos, lnurlp, withdraw"
LNBITS_BACKEND_WALLET_CLASS: FakeWallet LNBITS_BACKEND_WALLET_CLASS: FakeWallet
run: | run: |
poetry run lnbits & uv run lnbits &
sleep 10 sleep 10
- name: setup java version - name: setup java version

View file

@ -14,18 +14,17 @@ on:
- 'flake.nix' - 'flake.nix'
- 'flake.lock' - 'flake.lock'
- 'pyproject.toml' - 'pyproject.toml'
- 'poetry.lock' - 'uv.lock'
- '.github/workflows/nix.yml' - '.github/workflows/nix.yml'
pull_request: pull_request:
paths: paths:
- 'flake.nix' - 'flake.nix'
- 'flake.lock' - 'flake.lock'
- 'pyproject.toml' - 'pyproject.toml'
- 'poetry.lock' - 'uv.lock'
jobs: jobs:
nix: nix:
if: false # temporarly disable nix support until the `poetry2nix` issue is resolved
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View file

@ -32,6 +32,10 @@ jobs:
run: | run: |
docker build -t lnbits/lnbits . docker build -t lnbits/lnbits .
- uses: ./.github/actions/prepare
with:
python-version: ${{ inputs.python-version }}
- name: Setup Regtest - name: Setup Regtest
run: | run: |
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
@ -40,10 +44,6 @@ jobs:
./tests ./tests
sudo chmod -R a+rwx . sudo chmod -R a+rwx .
- uses: ./.github/actions/prepare
with:
python-version: ${{ inputs.python-version }}
- name: Run pytest - name: Run pytest
uses: pavelzw/pytest-action@v2 uses: pavelzw/pytest-action@v2
env: env:

View file

@ -2,26 +2,20 @@ FROM python:3.12-slim-bookworm AS builder
RUN apt-get clean RUN apt-get clean
RUN apt-get update RUN apt-get update
RUN apt-get install -y curl pkg-config build-essential libnss-myhostname RUN apt-get install -y curl pkg-config build-essential libnss-myhostname automake
RUN curl -sSL https://install.python-poetry.org | python3 - RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:$PATH" ENV PATH="/root/.local/bin:$PATH"
WORKDIR /app WORKDIR /app
# Only copy the files required to install the dependencies # Only copy the files required to install the dependencies
COPY pyproject.toml poetry.lock ./ COPY pyproject.toml uv.lock ./
RUN touch README.md RUN touch README.md
RUN mkdir data RUN mkdir data
ENV POETRY_NO_INTERACTION=1 \ RUN uv sync --all-extras
POETRY_VIRTUALENVS_IN_PROJECT=1 \
POETRY_VIRTUALENVS_CREATE=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache
ARG POETRY_INSTALL_ARGS="--only main"
RUN poetry install --no-root ${POETRY_INSTALL_ARGS}
FROM python:3.12-slim-bookworm FROM python:3.12-slim-bookworm
@ -34,26 +28,19 @@ RUN apt-get update && apt-get -y upgrade && \
apt-get -y install postgresql-client-14 postgresql-client-common && \ apt-get -y install postgresql-client-14 postgresql-client-common && \
apt-get clean all && rm -rf /var/lib/apt/lists/* apt-get clean all && rm -rf /var/lib/apt/lists/*
RUN curl -sSL https://install.python-poetry.org | python3 - RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:$PATH" ENV PATH="/root/.local/bin:$PATH"
ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_IN_PROJECT=1 \
POETRY_VIRTUALENVS_CREATE=1 \
VIRTUAL_ENV=/app/.venv \
PATH="/app/.venv/bin:$PATH"
WORKDIR /app WORKDIR /app
COPY . . COPY . .
COPY --from=builder /app/.venv .venv COPY --from=builder /app/.venv .venv
ARG POETRY_INSTALL_ARGS="--only main" RUN uv sync --all-extras
RUN poetry install ${POETRY_INSTALL_ARGS}
ENV LNBITS_PORT="5000" ENV LNBITS_PORT="5000"
ENV LNBITS_HOST="0.0.0.0" ENV LNBITS_HOST="0.0.0.0"
EXPOSE 5000 EXPOSE 5000
CMD ["sh", "-c", "poetry run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"] CMD ["sh", "-c", "uv run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]

View file

@ -8,8 +8,7 @@ RUN apt-get update && apt-get -y upgrade && \
apt-get install -y netcat-openbsd apt-get install -y netcat-openbsd
# Reinstall dependencies just in case (needed for CMD usage) # Reinstall dependencies just in case (needed for CMD usage)
ARG POETRY_INSTALL_ARGS="--only main" RUN uv sync --all-extras
RUN poetry install ${POETRY_INSTALL_ARGS}
# LNbits + boltzd configuration # LNbits + boltzd configuration
ENV LNBITS_PORT="5000" ENV LNBITS_PORT="5000"

View file

@ -9,34 +9,34 @@ check: mypy pyright checkblack checkruff checkprettier checkbundle
test: test-unit test-wallets test-api test-regtest test: test-unit test-wallets test-api test-regtest
prettier: prettier:
poetry run ./node_modules/.bin/prettier --write . uv run ./node_modules/.bin/prettier --write .
pyright: pyright:
poetry run ./node_modules/.bin/pyright uv run ./node_modules/.bin/pyright
mypy: mypy:
poetry run mypy uv run mypy
black: black:
poetry run black . uv run black .
ruff: ruff:
poetry run ruff check . --fix uv run ruff check . --fix
checkruff: checkruff:
poetry run ruff check . uv run ruff check .
checkprettier: checkprettier:
poetry run ./node_modules/.bin/prettier --check . uv run ./node_modules/.bin/prettier --check .
checkblack: checkblack:
poetry run black --check . uv run black --check .
checkeditorconfig: checkeditorconfig:
editorconfig-checker editorconfig-checker
dev: dev:
poetry run lnbits --reload uv run lnbits --reload
docker: docker:
docker build -t lnbits/lnbits . docker build -t lnbits/lnbits .
@ -46,27 +46,27 @@ test-wallets:
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \ LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
DEBUG=true \ DEBUG=true \
poetry run pytest tests/wallets uv run pytest tests/wallets
test-unit: test-unit:
LNBITS_DATA_FOLDER="./tests/data" \ LNBITS_DATA_FOLDER="./tests/data" \
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \ LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
DEBUG=true \ DEBUG=true \
poetry run pytest tests/unit uv run pytest tests/unit
test-api: test-api:
LNBITS_DATA_FOLDER="./tests/data" \ LNBITS_DATA_FOLDER="./tests/data" \
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \ LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
DEBUG=true \ DEBUG=true \
poetry run pytest tests/api uv run pytest tests/api
test-regtest: test-regtest:
LNBITS_DATA_FOLDER="./tests/data" \ LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
DEBUG=true \ DEBUG=true \
poetry run pytest tests/regtest uv run pytest tests/regtest
test-migration: test-migration:
LNBITS_ADMIN_UI=True \ LNBITS_ADMIN_UI=True \
@ -74,18 +74,18 @@ test-migration:
HOST=0.0.0.0 \ HOST=0.0.0.0 \
PORT=5002 \ PORT=5002 \
LNBITS_DATA_FOLDER="./tests/data" \ LNBITS_DATA_FOLDER="./tests/data" \
timeout 5s poetry run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi timeout 5s uv run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
HOST=0.0.0.0 \ HOST=0.0.0.0 \
PORT=5002 \ PORT=5002 \
LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \ LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \
LNBITS_ADMIN_UI=False \ LNBITS_ADMIN_UI=False \
timeout 5s poetry run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi timeout 5s uv run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
LNBITS_DATA_FOLDER="./tests/data" \ LNBITS_DATA_FOLDER="./tests/data" \
LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \ LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \
poetry run python tools/conv.py uv run python tools/conv.py
migration: migration:
poetry run python tools/conv.py uv run python tools/conv.py
openapi: openapi:
LNBITS_ADMIN_UI=False \ LNBITS_ADMIN_UI=False \
@ -94,9 +94,9 @@ openapi:
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
HOST=0.0.0.0 \ HOST=0.0.0.0 \
PORT=5003 \ PORT=5003 \
poetry run lnbits & uv run lnbits &
sleep 15 sleep 15
curl -s http://0.0.0.0:5003/openapi.json | poetry run openapi-spec-validator --errors=all - curl -s http://0.0.0.0:5003/openapi.json | uv run openapi-spec-validator --errors=all -
# kill -9 %1 # kill -9 %1
bak: bak:
@ -109,7 +109,7 @@ sass:
bundle: bundle:
npm install npm install
npm run bundle npm run bundle
poetry run ./node_modules/.bin/prettier -w ./lnbits/static/vendor.json uv run ./node_modules/.bin/prettier -w ./lnbits/static/vendor.json
checkbundle: checkbundle:
cp lnbits/static/bundle.min.js lnbits/static/bundle.min.js.old cp lnbits/static/bundle.min.js lnbits/static/bundle.min.js.old
@ -126,8 +126,8 @@ checkbundle:
install-pre-commit-hook: install-pre-commit-hook:
@echo "Installing pre-commit hook to git" @echo "Installing pre-commit hook to git"
@echo "Uninstall the hook with poetry run pre-commit uninstall" @echo "Uninstall the hook with uv run pre-commit uninstall"
poetry run pre-commit install uv run pre-commit install
pre-commit: pre-commit:
poetry run pre-commit run --all-files uv run pre-commit run --all-files

View file

@ -23,4 +23,4 @@ if ! nc -z localhost 9002; then
fi fi
echo "Starting LNbits on $LNBITS_HOST:$LNBITS_PORT..." echo "Starting LNbits on $LNBITS_HOST:$LNBITS_PORT..."
exec poetry run lnbits --port "$LNBITS_PORT" --host "$LNBITS_HOST" --forwarded-allow-ips='*' exec uv run lnbits --port "$LNBITS_PORT" --host "$LNBITS_HOST" --forwarded-allow-ips='*'

View file

@ -11,13 +11,13 @@ Thanks for contributing :)
# Run # Run
Follow the [Basic installation: Option 1 (recommended): poetry](https://docs.lnbits.org/guide/installation.html#option-1-recommended-poetry) Follow the [Option 2 (recommended): UV](https://docs.lnbits.org/guide/installation.html)
guide to install poetry and other dependencies. guide to install uv and other dependencies.
Then you can start LNbits uvicorn server with: Then you can start LNbits uvicorn server with:
```bash ```bash
poetry run lnbits uv run lnbits
``` ```
Or you can use the following to start uvicorn with hot reloading enabled: Or you can use the following to start uvicorn with hot reloading enabled:
@ -25,7 +25,7 @@ Or you can use the following to start uvicorn with hot reloading enabled:
```bash ```bash
make dev make dev
# or # or
poetry run lnbits --reload uv run lnbits --reload
``` ```
You might need the following extra dependencies on clean installation of Debian: You might need the following extra dependencies on clean installation of Debian:
@ -50,7 +50,7 @@ make install-pre-commit-hook
This project has unit tests that help prevent regressions. Before you can run the tests, you must install a few dependencies: This project has unit tests that help prevent regressions. Before you can run the tests, you must install a few dependencies:
```bash ```bash
poetry install uv sync --all-extras --dev
npm i npm i
``` ```
@ -69,7 +69,7 @@ make format
Run mypy checks: Run mypy checks:
```bash ```bash
poetry run mypy make mypy
``` ```
Run everything: Run everything:

View file

@ -42,14 +42,10 @@ mv templates/example templates/mysuperplugin # Rename templates folder.
DO NOT ADD NEW DEPENDENCIES. Try to use the dependencies that are available in `pyproject.toml`. Getting the LNbits project to accept a new dependency is time consuming and uncertain, and may result in your extension NOT being made available to others. DO NOT ADD NEW DEPENDENCIES. Try to use the dependencies that are available in `pyproject.toml`. Getting the LNbits project to accept a new dependency is time consuming and uncertain, and may result in your extension NOT being made available to others.
If for some reason your extensions must have a new python package to work, and its nees are not met in `pyproject.toml`, you can add a new package using `poerty`: If for some reason your extensions must have a new python package to work, and its nees are not met in `pyproject.toml`, you can add a new package using `poerty` or `uv`:
```sh
$ poetry add <package>
```
**But we need an extra step to make sure LNbits doesn't break in production.** **But we need an extra step to make sure LNbits doesn't break in production.**
Dependencies need to be added to `pyproject.toml`, then tested by running on `poetry` compatibility can be tested with `nix build .#checks.x86_64-linux.vmTest`. Dependencies need to be added to `pyproject.toml`, then tested by running on `uv` and `poetry` compatibility can be tested with `nix build .#checks.x86_64-linux.vmTest`.
## SQLite to PostgreSQL migration ## SQLite to PostgreSQL migration

View file

@ -53,7 +53,7 @@ $ sudo nano .env
Now start LNbits once in the terminal window Now start LNbits once in the terminal window
``` ```
$ poetry run lnbits $ uv run lnbits
``` ```
You can now `cat` the Super User ID: You can now `cat` the Super User ID:

View file

@ -23,15 +23,15 @@ LNBITS_ADMIN_UI=true HOST=0.0.0.0 PORT=5000 ./LNbits-latest.AppImage # most syst
LNbits will create a folder for db and extension files in the folder the AppImage runs from. LNbits will create a folder for db and extension files in the folder the AppImage runs from.
## Option 2: Poetry (recommended for developers) ## Option 2: UV (recommended for developers)
It is recommended to use the latest version of Poetry. Make sure you have Python version `3.12` installed. It is recommended to use the latest version of UV. Make sure you have Python version `3.12` installed.
### Install Python 3.12 ### Install Python 3.12
## Option 2 (recommended): Poetry ## Option 2 (recommended): UV
It is recommended to use the latest version of Poetry. Make sure you have Python version 3.9 or higher installed. It is recommended to use the latest version of UV. Make sure you have Python version 3.10 or higher installed.
### Verify Python version ### Verify Python version
@ -39,11 +39,19 @@ It is recommended to use the latest version of Poetry. Make sure you have Python
python3 --version python3 --version
``` ```
### Install Poetry ### Install UV
```sh
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
```
### (old) Install Poetry
```sh ```sh
# If path 'export PATH="$HOME/.local/bin:$PATH"' fails, use the path echoed by the install # If path 'export PATH="$HOME/.local/bin:$PATH"' fails, use the path echoed by the install
curl -sSL https://install.python-poetry.org | python3 - && export PATH="$HOME/.local/bin:$PATH" curl -sSL https://install.python-poetry.org | python3 -
export PATH="$HOME/.local/bin:$PATH"
``` ```
### install LNbits ### install LNbits
@ -51,9 +59,13 @@ curl -sSL https://install.python-poetry.org | python3 - && export PATH="$HOME/.l
```sh ```sh
git clone https://github.com/lnbits/lnbits.git git clone https://github.com/lnbits/lnbits.git
cd lnbits cd lnbits
poetry env use 3.12
git checkout main git checkout main
poetry install --only main uv sync --all-extras
# or poetry
# poetry env use 3.12
# poetry install --only main
cp .env.example .env cp .env.example .env
# Optional: to set funding source amongst other options via the env `nano .env` # Optional: to set funding source amongst other options via the env `nano .env`
``` ```
@ -61,8 +73,11 @@ cp .env.example .env
#### Running the server #### Running the server
```sh ```sh
poetry run lnbits uv run lnbits
# To change port/host pass 'poetry run lnbits --port 9000 --host 0.0.0.0' # To change port/host pass 'uv run lnbits --port 9000 --host 0.0.0.0'
# or poetry
# poetry run lnbits
# adding --debug in the start-up command above to help your troubleshooting and generate a more verbose output # adding --debug in the start-up command above to help your troubleshooting and generate a more verbose output
# Note that you have to add the line DEBUG=true in your .env file, too. # Note that you have to add the line DEBUG=true in your .env file, too.
``` ```
@ -71,7 +86,7 @@ poetry run lnbits
```sh ```sh
# A very useful terminal client for getting the supersuer ID, updating extensions, etc # A very useful terminal client for getting the supersuer ID, updating extensions, etc
poetry run lnbits-cli --help uv run lnbits-cli --help
``` ```
#### Updating the server #### Updating the server
@ -85,15 +100,18 @@ cd lnbits
git pull --rebase git pull --rebase
# Check your poetry version with # Check your poetry version with
poetry env list # poetry env list
# If version is less 3.12, update it by running # If version is less 3.12, update it by running
poetry env use python3.12 # poetry env use python3.12
poetry env remove python3.9 # poetry env remove python3.9
poetry env list # poetry env list
# Run install and start LNbits with # Run install and start LNbits with
poetry install --only main # poetry install --only main
poetry run lnbits # poetry run lnbits
uv sync --all-extras
uv run lnbits
# use LNbits admin UI Extensions page function "Update All" do get extensions onto proper level # use LNbits admin UI Extensions page function "Update All" do get extensions onto proper level
``` ```
@ -108,13 +126,13 @@ chmod +x lnbits.sh &&
Now visit `0.0.0.0:5000` to make a super-user account. Now visit `0.0.0.0:5000` to make a super-user account.
`./lnbits.sh` can be used to run, but for more control `cd lnbits` and use `poetry run lnbits` (see previous option). `./lnbits.sh` can be used to run, but for more control `cd lnbits` and use `uv run lnbits` (see previous option).
## Option 3: Nix ## Option 3: Nix
```sh ```sh
# Install nix. If you have installed via another manager, remove and use this install (from https://nixos.org/download) # Install nix. If you have installed via another manager, remove and use this install (from https://nixos.org/download)
sh <(curl -L https://nixos.org/nix/install) --daemon sh <(c&url -L https://nixos.org/nix/install) --daemon
# Enable nix-command and flakes experimental features for nix: # Enable nix-command and flakes experimental features for nix:
echo 'experimental-features = nix-command flakes' >> /etc/nix/nix.conf echo 'experimental-features = nix-command flakes' >> /etc/nix/nix.conf
@ -332,7 +350,7 @@ LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
# START LNbits # START LNbits
# STOP LNbits # STOP LNbits
poetry run python tools/conv.py uv run python tools/conv.py
# or # or
make migration make migration
``` ```
@ -357,8 +375,8 @@ Description=LNbits
[Service] [Service]
# replace with the absolute path of your lnbits installation # replace with the absolute path of your lnbits installation
WorkingDirectory=/home/lnbits/lnbits WorkingDirectory=/home/lnbits/lnbits
# same here. run `which poetry` if you can't find the poetry binary # same here. run `which uv` if you can't find the poetry binary
ExecStart=/home/lnbits/.local/bin/poetry run lnbits ExecStart=/home/lnbits/.local/bin/uv run lnbits
# replace with the user that you're running lnbits on # replace with the user that you're running lnbits on
User=lnbits User=lnbits
Restart=always Restart=always

View file

@ -72,7 +72,7 @@ You can also use an AES-encrypted macaroon (more info) instead by using
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn - `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
To encrypt your macaroon, run `poetry run lnbits-cli encrypt macaroon`. To encrypt your macaroon, run `uv run lnbits-cli encrypt macaroon`.
### LNbits ### LNbits

149
flake.lock generated
View file

@ -1,15 +1,39 @@
{ {
"nodes": { "nodes": {
"build-system-pkgs": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"pyproject-nix": [
"pyproject-nix"
],
"uv2nix": "uv2nix"
},
"locked": {
"lastModified": 1755484659,
"narHash": "sha256-2FfbqsaHVQd12XFFUAinIMAuGO3853LONmva1gT3vKw=",
"owner": "pyproject-nix",
"repo": "build-system-pkgs",
"rev": "9778e87c2361810ff15e287ca5895c9da4a0e900",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "build-system-pkgs",
"type": "github"
}
},
"flake-utils": { "flake-utils": {
"inputs": { "inputs": {
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1710146030, "lastModified": 1731533236,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -18,71 +42,49 @@
"type": "github" "type": "github"
} }
}, },
"nix-github-actions": {
"inputs": {
"nixpkgs": [
"poetry2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1703863825,
"narHash": "sha256-rXwqjtwiGKJheXB43ybM8NwWB8rO2dSRrEqes0S7F5Y=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "5163432afc817cf8bd1f031418d1869e4c9d5547",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1735563628, "lastModified": 1751274312,
"narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=", "narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=",
"owner": "nixos", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798", "rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nixos", "owner": "NixOS",
"ref": "nixos-24.05", "ref": "nixos-24.11",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
}, },
"poetry2nix": { "pyproject-nix": {
"inputs": { "inputs": {
"flake-utils": "flake-utils",
"nix-github-actions": "nix-github-actions",
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
], ]
"systems": "systems_2",
"treefmt-nix": "treefmt-nix"
}, },
"locked": { "locked": {
"lastModified": 1724134185, "lastModified": 1754923840,
"narHash": "sha256-nDqpGjz7cq3ThdC98BPe1ANCNlsJds/LLZ3/MdIXjA0=", "narHash": "sha256-QSKpYg+Ts9HYF155ltlj40iBex39c05cpOF8gjoE2EM=",
"owner": "nix-community", "owner": "pyproject-nix",
"repo": "poetry2nix", "repo": "pyproject.nix",
"rev": "5ee730a8752264e463c0eaf06cc060fd07f6dae9", "rev": "023cd4be230eacae52635be09eef100c37ef78da",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nix-community", "owner": "pyproject-nix",
"repo": "poetry2nix", "repo": "pyproject.nix",
"type": "github" "type": "github"
} }
}, },
"root": { "root": {
"inputs": { "inputs": {
"build-system-pkgs": "build-system-pkgs",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"poetry2nix": "poetry2nix" "pyproject-nix": "pyproject-nix",
"uv2nix": "uv2nix_2"
} }
}, },
"systems": { "systems": {
@ -100,38 +102,51 @@
"type": "github" "type": "github"
} }
}, },
"systems_2": { "uv2nix": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"id": "systems",
"type": "indirect"
}
},
"treefmt-nix": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
"poetry2nix", "build-system-pkgs",
"nixpkgs" "nixpkgs"
],
"pyproject-nix": [
"build-system-pkgs",
"pyproject-nix"
] ]
}, },
"locked": { "locked": {
"lastModified": 1719749022, "lastModified": 1755210905,
"narHash": "sha256-ddPKHcqaKCIFSFc/cvxS14goUhCOAwsM1PbMr0ZtHMg=", "narHash": "sha256-WnoFEk79ysjL85TNP7bvImzhxvQw9B6uNtnLd4oJntw=",
"owner": "numtide", "owner": "pyproject-nix",
"repo": "treefmt-nix", "repo": "uv2nix",
"rev": "8df5ff62195d4e67e2264df0b7f5e8c9995fd0bd", "rev": "87bcba013ef304bbfd67c8e8a257aee634ed5a4c",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "numtide", "owner": "pyproject-nix",
"repo": "treefmt-nix", "repo": "uv2nix",
"type": "github"
}
},
"uv2nix_2": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"pyproject-nix": [
"pyproject-nix"
]
},
"locked": {
"lastModified": 1755485731,
"narHash": "sha256-k8kxwVs8Oze6q/jAaRa3RvZbb50I/K0b5uptlsh0HXI=",
"owner": "pyproject-nix",
"repo": "uv2nix",
"rev": "bebbd80bf56110fcd20b425589814af28f1939eb",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "uv2nix",
"type": "github" "type": "github"
} }
} }

195
flake.nix
View file

@ -1,70 +1,149 @@
{ {
description = "LNbits, free and open-source Lightning wallet and accounts system"; description = "LNbits, free and open-source Lightning wallet and accounts system (uv2nix)";
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.05"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
poetry2nix = { flake-utils.url = "github:numtide/flake-utils";
url = "github:nix-community/poetry2nix";
inputs.nixpkgs.follows = "nixpkgs"; pyproject-nix.url = "github:pyproject-nix/pyproject.nix";
}; uv2nix.url = "github:pyproject-nix/uv2nix";
build-system-pkgs.url = "github:pyproject-nix/build-system-pkgs";
pyproject-nix.inputs.nixpkgs.follows = "nixpkgs";
uv2nix.inputs.nixpkgs.follows = "nixpkgs";
build-system-pkgs.inputs.nixpkgs.follows = "nixpkgs";
uv2nix.inputs.pyproject-nix.follows = "pyproject-nix";
build-system-pkgs.inputs.pyproject-nix.follows = "pyproject-nix";
}; };
outputs = { self, nixpkgs, poetry2nix }@inputs:
let outputs = { self, nixpkgs, flake-utils, uv2nix, pyproject-nix, build-system-pkgs, ... }:
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]
forSystems = systems: f: (system:
nixpkgs.lib.genAttrs systems let
(system: f system (import nixpkgs { inherit system; overlays = [ poetry2nix.overlays.default self.overlays.default ]; })); pkgs = import nixpkgs { inherit system; };
forAllSystems = forSystems supportedSystems; lib = pkgs.lib;
projectName = "lnbits";
in python = pkgs.python312;
{
overlays = { # Read uv.lock / pyproject via uv2nix
default = final: prev: { workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
${projectName} = self.packages.${prev.stdenv.hostPlatform.system}.${projectName};
}; # Prefer wheels when available
}; uvLockedOverlay = workspace.mkPyprojectOverlay { sourcePreference = "wheel"; };
packages = forAllSystems (system: pkgs: {
default = self.packages.${system}.${projectName}; # Helper for extending lists safely (works if a is null)
${projectName} = pkgs.poetry2nix.mkPoetryApplication { plus = a: b: lib.unique (((if a == null then [] else a)) ++ b);
projectDir = ./.;
meta.rev = self.dirtyRev or self.rev; # Extra build inputs for troublesome sdists
meta.mainProgram = projectName; myOverrides = (final: prev: {
overrides = pkgs.poetry2nix.overrides.withDefaults (final: prev: { # embit needs setuptools at build time
coincurve = prev.coincurve.override { preferWheel = true; }; embit = prev.embit.overrideAttrs (old: {
protobuf = prev.protobuf.override { preferWheel = true; }; nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools ];
ruff = prev.ruff.override { preferWheel = true; };
wallycore = prev.wallycore.override { preferWheel = true; };
tlv8 = prev.tlv8.overrideAttrs (old: {
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
prev.setuptools
];
}); });
pynostr = prev.pynostr.overrideAttrs (old: {
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ # http-ece (pywebpush dep) needs setuptools
prev.setuptools-scm "http-ece" = prev."http-ece".overrideAttrs (old: {
nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools ];
});
# pyqrcode needs setuptools
pyqrcode = prev.pyqrcode.overrideAttrs (old: {
nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools ];
});
# tlv8 needs setuptools
tlv8 = prev.tlv8.overrideAttrs (old: {
nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools ];
});
# secp256k1 Python binding:
# - setuptools, pkg-config
# - cffi + pycparser
# - system libsecp256k1 for headers/libs
secp256k1 = prev.secp256k1.overrideAttrs (old: {
nativeBuildInputs = plus (old.nativeBuildInputs or []) [
prev.setuptools
pkgs.pkg-config
prev.cffi
prev.pycparser
]; ];
buildInputs = plus (old.buildInputs or []) [ pkgs.secp256k1 ];
propagatedBuildInputs = plus (old.propagatedBuildInputs or []) [ prev.cffi prev.pycparser ];
env = (old.env or { }) // { PKG_CONFIG = "${pkgs.pkg-config}/bin/pkg-config"; };
});
# pynostr uses setuptools-scm for versioning
pynostr = prev.pynostr.overrideAttrs (old: {
nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools-scm ];
}); });
}); });
};
}); # Compose Python package set honoring uv.lock
nixosModules = { pythonSet =
default = { pkgs, lib, config, ... }: { (pkgs.callPackage pyproject-nix.build.packages { inherit python; })
imports = [ "${./nix/modules/${projectName}-service.nix}" ]; .overrideScope (lib.composeManyExtensions [
nixpkgs.overlays = [ self.overlays.default ]; build-system-pkgs.overlays.default
}; uvLockedOverlay
}; myOverrides
checks = forAllSystems (system: pkgs: ]);
let
vmTests = import ./nix/tests { projectName = "lnbits";
makeTest = (import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }).makeTest;
inherit inputs pkgs; # Build a venv from the locked spec (this installs the resolved wheels)
runtimeVenv = pythonSet.mkVirtualEnv "${projectName}-env" workspace.deps.default;
# Wrapper so `nix run` behaves like `uv run` (use local source tree for templates/static/extensions)
lnbitsApp = pkgs.writeShellApplication {
name = "lnbits";
text = ''
export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
export REQUESTS_CA_BUNDLE=$SSL_CERT_FILE
export PYTHONPATH="$PWD:${PYTHONPATH:-}"
exec ${runtimeVenv}/bin/lnbits "$@"
'';
};
lnbitsCliApp = pkgs.writeShellApplication {
name = "lnbits-cli";
text = ''
export PYTHONPATH="$PWD:${PYTHONPATH:-}"
exec ${runtimeVenv}/bin/lnbits-cli "$@"
'';
}; };
in in
pkgs.lib.optionalAttrs pkgs.stdenv.isLinux vmTests # vmTests can only be ran on Linux, so append them only if on Linux.
//
{ {
# Other checks here... # nix build → produces the venv in ./result
} packages.default = runtimeVenv;
); packages.${projectName} = runtimeVenv;
};
# nix run . → launches via wrapper that imports from source tree
apps.default = { type = "app"; program = "${lnbitsApp}/bin/lnbits"; };
apps.${projectName} = self.apps.${system}.default;
apps."${projectName}-cli" = { type = "app"; program = "${lnbitsCliApp}/bin/lnbits-cli"; };
# dev shell with locked deps + tools
devShells.default = pkgs.mkShell {
packages = [
runtimeVenv
pkgs.uv
pkgs.ruff
pkgs.black
pkgs.mypy
pkgs.pre-commit
pkgs.openapi-generator-cli
];
};
overlays.default = final: prev: {
${projectName} = self.packages.${final.stdenv.hostPlatform.system}.${projectName};
replaceVars = prev.replaceVars or (path: vars: prev.substituteAll ({ src = path; } // vars));
};
nixosModules.default = { pkgs, lib, config, ... }: {
imports = [ "${./nix/modules/lnbits-service.nix}" ];
nixpkgs.overlays = [ self.overlays.default ];
};
checks = { };
});
} }

View file

@ -13,10 +13,9 @@ if [ ! -d lnbits/data ]; then
# Install Python 3.10 and distutils non-interactively # Install Python 3.10 and distutils non-interactively
sudo apt install -y python3.10 python3.10-distutils sudo apt install -y python3.10 python3.10-distutils
# Install Poetry # Install UV
curl -sSL https://install.python-poetry.org | python3.10 - curl -LsSf https://astral.sh/uv/install.sh | sh
# Add Poetry to PATH for the current session
export PATH="/home/$USER/.local/bin:$PATH" export PATH="/home/$USER/.local/bin:$PATH"
if [ ! -d lnbits/wallets ]; then if [ ! -d lnbits/wallets ]; then
@ -42,13 +41,13 @@ elif [ ! -d lnbits/wallets ]; then
cd lnbits || { echo "Failed to cd into lnbits ... FAIL"; exit 1; } cd lnbits || { echo "Failed to cd into lnbits ... FAIL"; exit 1; }
fi fi
# Install the dependencies using Poetry # Install the dependencies using UV
poetry env use python3.9 uv sync --all-extras
poetry install --only main
# Set environment variables for LNbits # Set environment variables for LNbits
export LNBITS_ADMIN_UI=true export LNBITS_ADMIN_UI=true
export HOST=0.0.0.0 export HOST=0.0.0.0
# Run LNbits # Run LNbits
poetry run lnbits uv run lnbits

View file

@ -5,9 +5,9 @@ import os
import shutil import shutil
import sys import sys
import time import time
from collections.abc import Callable
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path from pathlib import Path
from typing import Callable, Optional
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@ -280,7 +280,7 @@ async def check_installed_extensions(app: FastAPI):
async def build_all_installed_extensions_list( # noqa: C901 async def build_all_installed_extensions_list( # noqa: C901
include_deactivated: Optional[bool] = True, include_deactivated: bool | None = True,
) -> list[InstallableExtension]: ) -> list[InstallableExtension]:
""" """
Returns a list of all the installed extensions plus the extensions that Returns a list of all the installed extensions plus the extensions that

View file

@ -5,7 +5,6 @@ import time
from functools import wraps from functools import wraps
from getpass import getpass from getpass import getpass
from pathlib import Path from pathlib import Path
from typing import Optional
from uuid import uuid4 from uuid import uuid4
import click import click
@ -96,7 +95,7 @@ def decrypt():
""" """
def get_super_user() -> Optional[str]: def get_super_user() -> str | None:
"""Get the superuser""" """Get the superuser"""
superuser_file = Path(settings.lnbits_data_folder, ".super_user") superuser_file = Path(settings.lnbits_data_folder, ".super_user")
if not superuser_file.exists() or not superuser_file.is_file(): if not superuser_file.exists() or not superuser_file.is_file():
@ -155,7 +154,7 @@ async def db_versions():
@db.command("cleanup-wallets") @db.command("cleanup-wallets")
@click.argument("days", type=int, required=False) @click.argument("days", type=int, required=False)
@coro @coro
async def database_cleanup_wallets(days: Optional[int] = None): async def database_cleanup_wallets(days: int | None = None):
"""Delete all wallets that never had any transaction""" """Delete all wallets that never had any transaction"""
async with core_db.connect() as conn: async with core_db.connect() as conn:
delta = days or settings.cleanup_wallets_days delta = days or settings.cleanup_wallets_days
@ -212,10 +211,10 @@ async def database_revert_payment(checking_id: str):
@click.option("-v", "--verbose", is_flag=True, help="Detailed log.") @click.option("-v", "--verbose", is_flag=True, help="Detailed log.")
@coro @coro
async def check_invalid_payments( async def check_invalid_payments(
days: Optional[int] = None, days: int | None = None,
limit: Optional[int] = None, limit: int | None = None,
wallet: Optional[str] = None, wallet: str | None = None,
verbose: Optional[bool] = False, verbose: bool | None = False,
): ):
"""Check payments that are settled in the DB but pending on the Funding Source""" """Check payments that are settled in the DB but pending on the Funding Source"""
await check_admin_settings() await check_admin_settings()
@ -303,7 +302,7 @@ async def create_user(username: str, password: str):
@users.command("cleanup-accounts") @users.command("cleanup-accounts")
@click.argument("days", type=int, required=False) @click.argument("days", type=int, required=False)
@coro @coro
async def database_cleanup_accounts(days: Optional[int] = None): async def database_cleanup_accounts(days: int | None = None):
"""Delete all accounts that have no wallets""" """Delete all accounts that have no wallets"""
async with core_db.connect() as conn: async with core_db.connect() as conn:
delta = days or settings.cleanup_wallets_days delta = days or settings.cleanup_wallets_days
@ -353,12 +352,12 @@ async def extensions_list():
) )
@coro @coro
async def extensions_update( # noqa: C901 async def extensions_update( # noqa: C901
extension: Optional[str] = None, extension: str | None = None,
all_extensions: Optional[bool] = False, all_extensions: bool | None = False,
repo_index: Optional[str] = None, repo_index: str | None = None,
source_repo: Optional[str] = None, source_repo: str | None = None,
url: Optional[str] = None, url: str | None = None,
admin_user: Optional[str] = None, admin_user: str | None = None,
): ):
""" """
Update extension to the latest version. Update extension to the latest version.
@ -443,10 +442,10 @@ async def extensions_update( # noqa: C901
@coro @coro
async def extensions_install( async def extensions_install(
extension: str, extension: str,
repo_index: Optional[str] = None, repo_index: str | None = None,
source_repo: Optional[str] = None, source_repo: str | None = None,
url: Optional[str] = None, url: str | None = None,
admin_user: Optional[str] = None, admin_user: str | None = None,
): ):
"""Install a extension""" """Install a extension"""
click.echo(f"Installing {extension}... {repo_index}") click.echo(f"Installing {extension}... {repo_index}")
@ -473,7 +472,7 @@ async def extensions_install(
) )
@coro @coro
async def extensions_uninstall( async def extensions_uninstall(
extension: str, url: Optional[str] = None, admin_user: Optional[str] = None extension: str, url: str | None = None, admin_user: str | None = None
): ):
"""Uninstall a extension""" """Uninstall a extension"""
click.echo(f"Uninstalling '{extension}'...") click.echo(f"Uninstalling '{extension}'...")
@ -562,10 +561,10 @@ if __name__ == "__main__":
async def install_extension( async def install_extension(
extension: str, extension: str,
repo_index: Optional[str] = None, repo_index: str | None = None,
source_repo: Optional[str] = None, source_repo: str | None = None,
url: Optional[str] = None, url: str | None = None,
admin_user: Optional[str] = None, admin_user: str | None = None,
) -> tuple[bool, str]: ) -> tuple[bool, str]:
try: try:
release = await _select_release(extension, repo_index, source_repo) release = await _select_release(extension, repo_index, source_repo)
@ -591,10 +590,10 @@ async def install_extension(
async def update_extension( async def update_extension(
extension: str, extension: str,
repo_index: Optional[str] = None, repo_index: str | None = None,
source_repo: Optional[str] = None, source_repo: str | None = None,
url: Optional[str] = None, url: str | None = None,
admin_user: Optional[str] = None, admin_user: str | None = None,
) -> tuple[bool, str]: ) -> tuple[bool, str]:
try: try:
click.echo(f"Updating '{extension}' extension.") click.echo(f"Updating '{extension}' extension.")
@ -644,9 +643,9 @@ async def update_extension(
async def _select_release( async def _select_release(
extension: str, extension: str,
repo_index: Optional[str] = None, repo_index: str | None = None,
source_repo: Optional[str] = None, source_repo: str | None = None,
) -> Optional[ExtensionRelease]: ) -> ExtensionRelease | None:
all_releases = await InstallableExtension.get_extension_releases(extension) all_releases = await InstallableExtension.get_extension_releases(extension)
if len(all_releases) == 0: if len(all_releases) == 0:
click.echo(f"No repository found for extension '{extension}'.") click.echo(f"No repository found for extension '{extension}'.")
@ -706,7 +705,7 @@ def _get_latest_release_per_repo(all_releases):
async def _call_install_extension( async def _call_install_extension(
data: CreateExtension, url: Optional[str], user_id: Optional[str] = None data: CreateExtension, url: str | None, user_id: str | None = None
): ):
if url: if url:
user_id = user_id or get_super_user() user_id = user_id or get_super_user()
@ -720,7 +719,7 @@ async def _call_install_extension(
async def _call_uninstall_extension( async def _call_uninstall_extension(
extension: str, url: Optional[str], user_id: Optional[str] = None extension: str, url: str | None, user_id: str | None = None
): ):
if url: if url:
user_id = user_id or get_super_user() user_id = user_id or get_super_user()
@ -756,7 +755,7 @@ async def _can_run_operation(url) -> bool:
return True return True
async def _is_lnbits_started(url: Optional[str]): async def _is_lnbits_started(url: str | None):
try: try:
url = url or f"http://{settings.host}:{settings.port}/api/v1/health" url = url or f"http://{settings.host}:{settings.port}/api/v1/health"
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:

View file

@ -1,5 +1,3 @@
from typing import Optional
from lnbits.core.db import db from lnbits.core.db import db
from lnbits.core.models import AuditEntry, AuditFilters from lnbits.core.models import AuditEntry, AuditFilters
from lnbits.core.models.audit import AuditCountStat from lnbits.core.models.audit import AuditCountStat
@ -8,14 +6,14 @@ from lnbits.db import Connection, Filters, Page
async def create_audit_entry( async def create_audit_entry(
entry: AuditEntry, entry: AuditEntry,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> None: ) -> None:
await (conn or db).insert("audit", entry) await (conn or db).insert("audit", entry)
async def get_audit_entries( async def get_audit_entries(
filters: Optional[Filters[AuditFilters]] = None, filters: Filters[AuditFilters] | None = None,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> Page[AuditEntry]: ) -> Page[AuditEntry]:
return await (conn or db).fetch_page( return await (conn or db).fetch_page(
"SELECT * from audit", "SELECT * from audit",
@ -27,7 +25,7 @@ async def get_audit_entries(
async def delete_expired_audit_entries( async def delete_expired_audit_entries(
conn: Optional[Connection] = None, conn: Connection | None = None,
): ):
await (conn or db).execute( await (conn or db).execute(
# Timestamp placeholder is safe from SQL injection (not user input) # Timestamp placeholder is safe from SQL injection (not user input)
@ -40,8 +38,8 @@ async def delete_expired_audit_entries(
async def get_count_stats( async def get_count_stats(
field: str, field: str,
filters: Optional[Filters[AuditFilters]] = None, filters: Filters[AuditFilters] | None = None,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> list[AuditCountStat]: ) -> list[AuditCountStat]:
if field not in ["request_method", "component", "response_code"]: if field not in ["request_method", "component", "response_code"]:
return [] return []
@ -67,8 +65,8 @@ async def get_count_stats(
async def get_long_duration_stats( async def get_long_duration_stats(
filters: Optional[Filters[AuditFilters]] = None, filters: Filters[AuditFilters] | None = None,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> list[AuditCountStat]: ) -> list[AuditCountStat]:
if not filters: if not filters:
filters = Filters() filters = Filters()

View file

@ -1,5 +1,3 @@
from typing import Optional
from lnbits.core.db import db from lnbits.core.db import db
from lnbits.db import Connection from lnbits.db import Connection
@ -7,8 +5,8 @@ from ..models import DbVersion
async def get_db_version( async def get_db_version(
ext_id: str, conn: Optional[Connection] = None ext_id: str, conn: Connection | None = None
) -> Optional[DbVersion]: ) -> DbVersion | None:
return await (conn or db).fetchone( return await (conn or db).fetchone(
"SELECT * FROM dbversions WHERE db = :ext_id", "SELECT * FROM dbversions WHERE db = :ext_id",
{"ext_id": ext_id}, {"ext_id": ext_id},
@ -16,7 +14,7 @@ async def get_db_version(
) )
async def get_db_versions(conn: Optional[Connection] = None) -> list[DbVersion]: async def get_db_versions(conn: Connection | None = None) -> list[DbVersion]:
return await (conn or db).fetchall("SELECT * FROM dbversions", model=DbVersion) return await (conn or db).fetchall("SELECT * FROM dbversions", model=DbVersion)
@ -30,7 +28,7 @@ async def update_migration_version(conn, db_name, version):
) )
async def delete_dbversion(*, ext_id: str, conn: Optional[Connection] = None) -> None: async def delete_dbversion(*, ext_id: str, conn: Connection | None = None) -> None:
await (conn or db).execute( await (conn or db).execute(
""" """
DELETE FROM dbversions WHERE db = :ext DELETE FROM dbversions WHERE db = :ext

View file

@ -1,5 +1,3 @@
from typing import Optional
from lnbits.core.db import db from lnbits.core.db import db
from lnbits.core.models.extensions import ( from lnbits.core.models.extensions import (
InstallableExtension, InstallableExtension,
@ -10,20 +8,20 @@ from lnbits.db import Connection, Database
async def create_installed_extension( async def create_installed_extension(
ext: InstallableExtension, ext: InstallableExtension,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> None: ) -> None:
await (conn or db).insert("installed_extensions", ext) await (conn or db).insert("installed_extensions", ext)
async def update_installed_extension( async def update_installed_extension(
ext: InstallableExtension, ext: InstallableExtension,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> None: ) -> None:
await (conn or db).update("installed_extensions", ext) await (conn or db).update("installed_extensions", ext)
async def update_installed_extension_state( async def update_installed_extension_state(
*, ext_id: str, active: bool, conn: Optional[Connection] = None *, ext_id: str, active: bool, conn: Connection | None = None
) -> None: ) -> None:
await (conn or db).execute( await (conn or db).execute(
""" """
@ -34,7 +32,7 @@ async def update_installed_extension_state(
async def delete_installed_extension( async def delete_installed_extension(
*, ext_id: str, conn: Optional[Connection] = None *, ext_id: str, conn: Connection | None = None
) -> None: ) -> None:
await (conn or db).execute( await (conn or db).execute(
""" """
@ -44,7 +42,7 @@ async def delete_installed_extension(
) )
async def drop_extension_db(ext_id: str, conn: Optional[Connection] = None) -> None: async def drop_extension_db(ext_id: str, conn: Connection | None = None) -> None:
row: dict = await (conn or db).fetchone( row: dict = await (conn or db).fetchone(
"SELECT * FROM dbversions WHERE db = :id", "SELECT * FROM dbversions WHERE db = :id",
{"id": ext_id}, {"id": ext_id},
@ -65,8 +63,8 @@ async def drop_extension_db(ext_id: str, conn: Optional[Connection] = None) -> N
async def get_installed_extension( async def get_installed_extension(
ext_id: str, conn: Optional[Connection] = None ext_id: str, conn: Connection | None = None
) -> Optional[InstallableExtension]: ) -> InstallableExtension | None:
extension = await (conn or db).fetchone( extension = await (conn or db).fetchone(
"SELECT * FROM installed_extensions WHERE id = :id", "SELECT * FROM installed_extensions WHERE id = :id",
{"id": ext_id}, {"id": ext_id},
@ -76,8 +74,8 @@ async def get_installed_extension(
async def get_installed_extensions( async def get_installed_extensions(
active: Optional[bool] = None, active: bool | None = None,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> list[InstallableExtension]: ) -> list[InstallableExtension]:
query = "SELECT * FROM installed_extensions" query = "SELECT * FROM installed_extensions"
if active is not None: if active is not None:
@ -93,8 +91,8 @@ async def get_installed_extensions(
async def get_user_extension( async def get_user_extension(
user_id: str, extension: str, conn: Optional[Connection] = None user_id: str, extension: str, conn: Connection | None = None
) -> Optional[UserExtension]: ) -> UserExtension | None:
return await (conn or db).fetchone( return await (conn or db).fetchone(
""" """
SELECT * FROM extensions SELECT * FROM extensions
@ -106,7 +104,7 @@ async def get_user_extension(
async def get_user_extensions( async def get_user_extensions(
user_id: str, conn: Optional[Connection] = None user_id: str, conn: Connection | None = None
) -> list[UserExtension]: ) -> list[UserExtension]:
return await (conn or db).fetchall( return await (conn or db).fetchall(
"""SELECT * FROM extensions WHERE "user" = :user""", """SELECT * FROM extensions WHERE "user" = :user""",
@ -116,20 +114,20 @@ async def get_user_extensions(
async def create_user_extension( async def create_user_extension(
user_extension: UserExtension, conn: Optional[Connection] = None user_extension: UserExtension, conn: Connection | None = None
) -> None: ) -> None:
await (conn or db).insert("extensions", user_extension) await (conn or db).insert("extensions", user_extension)
async def update_user_extension( async def update_user_extension(
user_extension: UserExtension, conn: Optional[Connection] = None user_extension: UserExtension, conn: Connection | None = None
) -> None: ) -> None:
where = """WHERE extension = :extension AND "user" = :user""" where = """WHERE extension = :extension AND "user" = :user"""
await (conn or db).update("extensions", user_extension, where) await (conn or db).update("extensions", user_extension, where)
async def get_user_active_extensions_ids( async def get_user_active_extensions_ids(
user_id: str, conn: Optional[Connection] = None user_id: str, conn: Connection | None = None
) -> list[str]: ) -> list[str]:
exts = await (conn or db).fetchall( exts = await (conn or db).fetchall(
""" """

View file

@ -1,5 +1,5 @@
from time import time from time import time
from typing import Any, Optional from typing import Any
from lnbits.core.crud.wallets import get_total_balance, get_wallet, get_wallets_ids from lnbits.core.crud.wallets import get_total_balance, get_wallet, get_wallets_ids
from lnbits.core.db import db from lnbits.core.db import db
@ -23,7 +23,7 @@ def update_payment_extra():
pass pass
async def get_payment(checking_id: str, conn: Optional[Connection] = None) -> Payment: async def get_payment(checking_id: str, conn: Connection | None = None) -> Payment:
return await (conn or db).fetchone( return await (conn or db).fetchone(
"SELECT * FROM apipayments WHERE checking_id = :checking_id", "SELECT * FROM apipayments WHERE checking_id = :checking_id",
{"checking_id": checking_id}, {"checking_id": checking_id},
@ -33,10 +33,10 @@ async def get_payment(checking_id: str, conn: Optional[Connection] = None) -> Pa
async def get_standalone_payment( async def get_standalone_payment(
checking_id_or_hash: str, checking_id_or_hash: str,
conn: Optional[Connection] = None, conn: Connection | None = None,
incoming: Optional[bool] = False, incoming: bool | None = False,
wallet_id: Optional[str] = None, wallet_id: str | None = None,
) -> Optional[Payment]: ) -> Payment | None:
clause: str = "checking_id = :checking_id OR payment_hash = :hash" clause: str = "checking_id = :checking_id OR payment_hash = :hash"
values = { values = {
"wallet_id": wallet_id, "wallet_id": wallet_id,
@ -64,8 +64,8 @@ async def get_standalone_payment(
async def get_wallet_payment( async def get_wallet_payment(
wallet_id: str, payment_hash: str, conn: Optional[Connection] = None wallet_id: str, payment_hash: str, conn: Connection | None = None
) -> Optional[Payment]: ) -> Payment | None:
payment = await (conn or db).fetchone( payment = await (conn or db).fetchone(
""" """
SELECT * SELECT *
@ -102,17 +102,17 @@ async def get_latest_payments_by_extension(
async def get_payments_paginated( # noqa: C901 async def get_payments_paginated( # noqa: C901
*, *,
wallet_id: Optional[str] = None, wallet_id: str | None = None,
user_id: Optional[str] = None, user_id: str | None = None,
complete: bool = False, complete: bool = False,
pending: bool = False, pending: bool = False,
failed: bool = False, failed: bool = False,
outgoing: bool = False, outgoing: bool = False,
incoming: bool = False, incoming: bool = False,
since: Optional[int] = None, since: int | None = None,
exclude_uncheckable: bool = False, exclude_uncheckable: bool = False,
filters: Optional[Filters[PaymentFilters]] = None, filters: Filters[PaymentFilters] | None = None,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> Page[Payment]: ) -> Page[Payment]:
""" """
Filters payments to be returned by: Filters payments to be returned by:
@ -176,17 +176,17 @@ async def get_payments_paginated( # noqa: C901
async def get_payments( async def get_payments(
*, *,
wallet_id: Optional[str] = None, wallet_id: str | None = None,
complete: bool = False, complete: bool = False,
pending: bool = False, pending: bool = False,
outgoing: bool = False, outgoing: bool = False,
incoming: bool = False, incoming: bool = False,
since: Optional[int] = None, since: int | None = None,
exclude_uncheckable: bool = False, exclude_uncheckable: bool = False,
filters: Optional[Filters[PaymentFilters]] = None, filters: Filters[PaymentFilters] | None = None,
conn: Optional[Connection] = None, conn: Connection | None = None,
limit: Optional[int] = None, limit: int | None = None,
offset: Optional[int] = None, offset: int | None = None,
) -> list[Payment]: ) -> list[Payment]:
""" """
Filters payments to be returned by complete | pending | outgoing | incoming. Filters payments to be returned by complete | pending | outgoing | incoming.
@ -230,7 +230,7 @@ async def get_payments_status_count() -> PaymentsStatusCount:
async def delete_expired_invoices( async def delete_expired_invoices(
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> None: ) -> None:
# first we delete all invoices older than one month # first we delete all invoices older than one month
@ -259,7 +259,7 @@ async def create_payment(
checking_id: str, checking_id: str,
data: CreatePayment, data: CreatePayment,
status: PaymentState = PaymentState.PENDING, status: PaymentState = PaymentState.PENDING,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> Payment: ) -> Payment:
# we don't allow the creation of the same invoice twice # we don't allow the creation of the same invoice twice
# note: this can be removed if the db uniqueness constraints are set appropriately # note: this can be removed if the db uniqueness constraints are set appropriately
@ -290,7 +290,7 @@ async def create_payment(
async def update_payment_checking_id( async def update_payment_checking_id(
checking_id: str, new_checking_id: str, conn: Optional[Connection] = None checking_id: str, new_checking_id: str, conn: Connection | None = None
) -> None: ) -> None:
await (conn or db).execute( await (conn or db).execute(
"UPDATE apipayments SET checking_id = :new_id WHERE checking_id = :old_id", "UPDATE apipayments SET checking_id = :new_id WHERE checking_id = :old_id",
@ -300,8 +300,8 @@ async def update_payment_checking_id(
async def update_payment( async def update_payment(
payment: Payment, payment: Payment,
new_checking_id: Optional[str] = None, new_checking_id: str | None = None,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> None: ) -> None:
await (conn or db).update( await (conn or db).update(
"apipayments", payment, "WHERE checking_id = :checking_id" "apipayments", payment, "WHERE checking_id = :checking_id"
@ -311,9 +311,9 @@ async def update_payment(
async def get_payments_history( async def get_payments_history(
wallet_id: Optional[str] = None, wallet_id: str | None = None,
group: DateTrunc = "day", group: DateTrunc = "day",
filters: Optional[Filters] = None, filters: Filters | None = None,
) -> list[PaymentHistoryPoint]: ) -> list[PaymentHistoryPoint]:
if not filters: if not filters:
filters = Filters() filters = Filters()
@ -376,9 +376,9 @@ async def get_payments_history(
async def get_payment_count_stats( async def get_payment_count_stats(
field: PaymentCountField, field: PaymentCountField,
filters: Optional[Filters[PaymentFilters]] = None, filters: Filters[PaymentFilters] | None = None,
user_id: Optional[str] = None, user_id: str | None = None,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> list[PaymentCountStat]: ) -> list[PaymentCountStat]:
if not filters: if not filters:
@ -409,9 +409,9 @@ async def get_payment_count_stats(
async def get_daily_stats( async def get_daily_stats(
filters: Optional[Filters[PaymentFilters]] = None, filters: Filters[PaymentFilters] | None = None,
user_id: Optional[str] = None, user_id: str | None = None,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> tuple[list[PaymentDailyStats], list[PaymentDailyStats]]: ) -> tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
if not filters: if not filters:
@ -459,9 +459,9 @@ async def get_daily_stats(
async def get_wallets_stats( async def get_wallets_stats(
filters: Optional[Filters[PaymentFilters]] = None, filters: Filters[PaymentFilters] | None = None,
user_id: Optional[str] = None, user_id: str | None = None,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> list[PaymentWalletStats]: ) -> list[PaymentWalletStats]:
if not filters: if not filters:
@ -508,7 +508,7 @@ async def get_wallets_stats(
async def delete_wallet_payment( async def delete_wallet_payment(
checking_id: str, wallet_id: str, conn: Optional[Connection] = None checking_id: str, wallet_id: str, conn: Connection | None = None
) -> None: ) -> None:
await (conn or db).execute( await (conn or db).execute(
"DELETE FROM apipayments WHERE checking_id = :checking_id AND wallet = :wallet", "DELETE FROM apipayments WHERE checking_id = :checking_id AND wallet = :wallet",
@ -517,8 +517,8 @@ async def delete_wallet_payment(
async def check_internal( async def check_internal(
payment_hash: str, conn: Optional[Connection] = None payment_hash: str, conn: Connection | None = None
) -> Optional[Payment]: ) -> Payment | None:
""" """
Returns the checking_id of the internal payment if it exists, Returns the checking_id of the internal payment if it exists,
otherwise None otherwise None
@ -534,7 +534,7 @@ async def check_internal(
async def is_internal_status_success( async def is_internal_status_success(
payment_hash: str, conn: Optional[Connection] = None payment_hash: str, conn: Connection | None = None
) -> bool: ) -> bool:
""" """
Returns True if the internal payment was found and is successful, Returns True if the internal payment was found and is successful,
@ -563,7 +563,7 @@ async def mark_webhook_sent(payment_hash: str, status: str) -> None:
async def _only_user_wallets_statement( async def _only_user_wallets_statement(
user_id: str, conn: Optional[Connection] = None user_id: str, conn: Connection | None = None
) -> str: ) -> str:
wallet_ids = await get_wallets_ids(user_id=user_id, conn=conn) or [ wallet_ids = await get_wallets_ids(user_id=user_id, conn=conn) or [
"no-wallets-for-user" "no-wallets-for-user"

View file

@ -1,5 +1,5 @@
import json import json
from typing import Any, Optional from typing import Any
from loguru import logger from loguru import logger
@ -14,7 +14,7 @@ from lnbits.settings import (
) )
async def get_super_settings() -> Optional[SuperSettings]: async def get_super_settings() -> SuperSettings | None:
data = await get_settings_by_tag("core") data = await get_settings_by_tag("core")
if data: if data:
super_user = await get_settings_field("super_user") super_user = await get_settings_field("super_user")
@ -24,7 +24,7 @@ async def get_super_settings() -> Optional[SuperSettings]:
return None return None
async def get_admin_settings(is_super_user: bool = False) -> Optional[AdminSettings]: async def get_admin_settings(is_super_user: bool = False) -> AdminSettings | None:
sets = await get_super_settings() sets = await get_super_settings()
if not sets: if not sets:
return None return None
@ -41,7 +41,7 @@ async def get_admin_settings(is_super_user: bool = False) -> Optional[AdminSetti
async def update_admin_settings( async def update_admin_settings(
data: EditableSettings, tag: Optional[str] = "core" data: EditableSettings, tag: str | None = "core"
) -> None: ) -> None:
editable_settings = await get_settings_by_tag("core") or {} editable_settings = await get_settings_by_tag("core") or {}
editable_settings.update(data.dict(exclude_unset=True)) editable_settings.update(data.dict(exclude_unset=True))
@ -61,7 +61,7 @@ async def update_super_user(super_user: str) -> SuperSettings:
return settings return settings
async def delete_admin_settings(tag: Optional[str] = "core") -> None: async def delete_admin_settings(tag: str | None = "core") -> None:
await db.execute( await db.execute(
"DELETE FROM system_settings WHERE tag = :tag", "DELETE FROM system_settings WHERE tag = :tag",
{"tag": tag}, {"tag": tag},
@ -93,8 +93,8 @@ async def create_admin_settings(super_user: str, new_settings: dict) -> SuperSet
async def get_settings_field( async def get_settings_field(
id_: str, tag: Optional[str] = "core" id_: str, tag: str | None = "core"
) -> Optional[SettingsField]: ) -> SettingsField | None:
row: dict = await db.fetchone( row: dict = await db.fetchone(
""" """
@ -108,9 +108,7 @@ async def get_settings_field(
return SettingsField(id=row["id"], value=json.loads(row["value"]), tag=row["tag"]) return SettingsField(id=row["id"], value=json.loads(row["value"]), tag=row["tag"])
async def set_settings_field( async def set_settings_field(id_: str, value: Any | None, tag: str | None = "core"):
id_: str, value: Optional[Any], tag: Optional[str] = "core"
):
value = json.dumps(value) if value is not None else None value = json.dumps(value) if value is not None else None
await db.execute( await db.execute(
""" """
@ -122,7 +120,7 @@ async def set_settings_field(
) )
async def get_settings_by_tag(tag: str) -> Optional[dict[str, Any]]: async def get_settings_by_tag(tag: str) -> dict[str, Any] | None:
rows: list[dict] = await db.fetchall( rows: list[dict] = await db.fetchall(
"SELECT * FROM system_settings WHERE tag = :tag", {"tag": tag} "SELECT * FROM system_settings WHERE tag = :tag", {"tag": tag}
) )

View file

@ -1,5 +1,3 @@
from typing import Optional
import shortuuid import shortuuid
from lnbits.core.db import db from lnbits.core.db import db
@ -19,7 +17,7 @@ async def create_tinyurl(domain: str, endless: bool, wallet: str):
return await get_tinyurl(tinyurl_id) return await get_tinyurl(tinyurl_id)
async def get_tinyurl(tinyurl_id: str) -> Optional[TinyURL]: async def get_tinyurl(tinyurl_id: str) -> TinyURL | None:
return await db.fetchone( return await db.fetchone(
"SELECT * FROM tiny_url WHERE id = :tinyurl", "SELECT * FROM tiny_url WHERE id = :tinyurl",
{"tinyurl": tinyurl_id}, {"tinyurl": tinyurl_id},

View file

@ -1,6 +1,6 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from time import time from time import time
from typing import Any, Optional from typing import Any
from uuid import uuid4 from uuid import uuid4
from lnbits.core.crud.extensions import get_user_active_extensions_ids from lnbits.core.crud.extensions import get_user_active_extensions_ids
@ -18,8 +18,8 @@ from ..models import (
async def create_account( async def create_account(
account: Optional[Account] = None, account: Account | None = None,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> Account: ) -> Account:
if account: if account:
account.validate_fields() account.validate_fields()
@ -36,7 +36,7 @@ async def update_account(account: Account) -> Account:
return account return account
async def delete_account(user_id: str, conn: Optional[Connection] = None) -> None: async def delete_account(user_id: str, conn: Connection | None = None) -> None:
await (conn or db).execute( await (conn or db).execute(
"DELETE from accounts WHERE id = :user", "DELETE from accounts WHERE id = :user",
{"user": user_id}, {"user": user_id},
@ -44,8 +44,8 @@ async def delete_account(user_id: str, conn: Optional[Connection] = None) -> Non
async def get_accounts( async def get_accounts(
filters: Optional[Filters[AccountFilters]] = None, filters: Filters[AccountFilters] | None = None,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> Page[AccountOverview]: ) -> Page[AccountOverview]:
where_clauses = [] where_clauses = []
values: dict[str, Any] = {} values: dict[str, Any] = {}
@ -92,9 +92,7 @@ async def get_accounts(
) )
async def get_account( async def get_account(user_id: str, conn: Connection | None = None) -> Account | None:
user_id: str, conn: Optional[Connection] = None
) -> Optional[Account]:
if len(user_id) == 0: if len(user_id) == 0:
return None return None
return await (conn or db).fetchone( return await (conn or db).fetchone(
@ -106,7 +104,7 @@ async def get_account(
async def delete_accounts_no_wallets( async def delete_accounts_no_wallets(
time_delta: int, time_delta: int,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> None: ) -> None:
delta = int(time()) - time_delta delta = int(time()) - time_delta
await (conn or db).execute( await (conn or db).execute(
@ -125,8 +123,8 @@ async def delete_accounts_no_wallets(
async def get_account_by_username( async def get_account_by_username(
username: str, conn: Optional[Connection] = None username: str, conn: Connection | None = None
) -> Optional[Account]: ) -> Account | None:
if len(username) == 0: if len(username) == 0:
return None return None
return await (conn or db).fetchone( return await (conn or db).fetchone(
@ -137,8 +135,8 @@ async def get_account_by_username(
async def get_account_by_pubkey( async def get_account_by_pubkey(
pubkey: str, conn: Optional[Connection] = None pubkey: str, conn: Connection | None = None
) -> Optional[Account]: ) -> Account | None:
return await (conn or db).fetchone( return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey", "SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
{"pubkey": pubkey.lower()}, {"pubkey": pubkey.lower()},
@ -147,8 +145,8 @@ async def get_account_by_pubkey(
async def get_account_by_email( async def get_account_by_email(
email: str, conn: Optional[Connection] = None email: str, conn: Connection | None = None
) -> Optional[Account]: ) -> Account | None:
if len(email) == 0: if len(email) == 0:
return None return None
return await (conn or db).fetchone( return await (conn or db).fetchone(
@ -159,8 +157,8 @@ async def get_account_by_email(
async def get_account_by_username_or_email( async def get_account_by_username_or_email(
username_or_email: str, conn: Optional[Connection] = None username_or_email: str, conn: Connection | None = None
) -> Optional[Account]: ) -> Account | None:
return await (conn or db).fetchone( return await (conn or db).fetchone(
""" """
SELECT * FROM accounts SELECT * FROM accounts
@ -171,7 +169,7 @@ async def get_account_by_username_or_email(
) )
async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[User]: async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
account = await get_account(user_id, conn) account = await get_account(user_id, conn)
if not account: if not account:
return None return None
@ -179,8 +177,8 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
async def get_user_from_account( async def get_user_from_account(
account: Account, conn: Optional[Connection] = None account: Account, conn: Connection | None = None
) -> Optional[User]: ) -> User | None:
extensions = await get_user_active_extensions_ids(account.id, conn) extensions = await get_user_active_extensions_ids(account.id, conn)
wallets = await get_wallets(account.id, False, conn=conn) wallets = await get_wallets(account.id, False, conn=conn)
return User( return User(
@ -207,7 +205,7 @@ async def update_user_access_control_list(user_acls: UserAcls):
async def get_user_access_control_lists( async def get_user_access_control_lists(
user_id: str, conn: Optional[Connection] = None user_id: str, conn: Connection | None = None
) -> UserAcls: ) -> UserAcls:
user_acls = await (conn or db).fetchone( user_acls = await (conn or db).fetchone(
"SELECT id, access_control_list FROM accounts WHERE id = :id", "SELECT id, access_control_list FROM accounts WHERE id = :id",

View file

@ -1,6 +1,5 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from time import time from time import time
from typing import Optional
from uuid import uuid4 from uuid import uuid4
from lnbits.core.db import db from lnbits.core.db import db
@ -14,8 +13,8 @@ from ..models import Wallet
async def create_wallet( async def create_wallet(
*, *,
user_id: str, user_id: str,
wallet_name: Optional[str] = None, wallet_name: str | None = None,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> Wallet: ) -> Wallet:
wallet_id = uuid4().hex wallet_id = uuid4().hex
wallet = Wallet( wallet = Wallet(
@ -32,7 +31,7 @@ async def create_wallet(
async def update_wallet( async def update_wallet(
wallet: Wallet, wallet: Wallet,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> Wallet: ) -> Wallet:
wallet.updated_at = datetime.now(timezone.utc) wallet.updated_at = datetime.now(timezone.utc)
await (conn or db).update("wallets", wallet) await (conn or db).update("wallets", wallet)
@ -44,7 +43,7 @@ async def delete_wallet(
user_id: str, user_id: str,
wallet_id: str, wallet_id: str,
deleted: bool = True, deleted: bool = True,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> None: ) -> None:
now = int(time()) now = int(time())
await (conn or db).execute( await (conn or db).execute(
@ -58,9 +57,7 @@ async def delete_wallet(
) )
async def force_delete_wallet( async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
wallet_id: str, conn: Optional[Connection] = None
) -> None:
await (conn or db).execute( await (conn or db).execute(
"DELETE FROM wallets WHERE id = :wallet", "DELETE FROM wallets WHERE id = :wallet",
{"wallet": wallet_id}, {"wallet": wallet_id},
@ -68,8 +65,8 @@ async def force_delete_wallet(
async def delete_wallet_by_id( async def delete_wallet_by_id(
wallet_id: str, conn: Optional[Connection] = None wallet_id: str, conn: Connection | None = None
) -> Optional[int]: ) -> int | None:
now = int(time()) now = int(time())
result = await (conn or db).execute( result = await (conn or db).execute(
# Timestamp placeholder is safe from SQL injection (not user input) # Timestamp placeholder is safe from SQL injection (not user input)
@ -83,13 +80,13 @@ async def delete_wallet_by_id(
return result.rowcount return result.rowcount
async def remove_deleted_wallets(conn: Optional[Connection] = None) -> None: async def remove_deleted_wallets(conn: Connection | None = None) -> None:
await (conn or db).execute("DELETE FROM wallets WHERE deleted = true") await (conn or db).execute("DELETE FROM wallets WHERE deleted = true")
async def delete_unused_wallets( async def delete_unused_wallets(
time_delta: int, time_delta: int,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> None: ) -> None:
delta = int(time()) - time_delta delta = int(time()) - time_delta
await (conn or db).execute( await (conn or db).execute(
@ -107,8 +104,8 @@ async def delete_unused_wallets(
async def get_wallet( async def get_wallet(
wallet_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None wallet_id: str, deleted: bool | None = None, conn: Connection | None = None
) -> Optional[Wallet]: ) -> Wallet | None:
query = """ query = """
SELECT *, COALESCE(( SELECT *, COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id SELECT balance FROM balances WHERE wallet_id = wallets.id
@ -125,7 +122,7 @@ async def get_wallet(
async def get_wallets( async def get_wallets(
user_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None user_id: str, deleted: bool | None = None, conn: Connection | None = None
) -> list[Wallet]: ) -> list[Wallet]:
query = """ query = """
SELECT *, COALESCE(( SELECT *, COALESCE((
@ -144,9 +141,9 @@ async def get_wallets(
async def get_wallets_paginated( async def get_wallets_paginated(
user_id: str, user_id: str,
deleted: Optional[bool] = None, deleted: bool | None = None,
filters: Optional[Filters[WalletsFilters]] = None, filters: Filters[WalletsFilters] | None = None,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> Page[Wallet]: ) -> Page[Wallet]:
if deleted is None: if deleted is None:
deleted = False deleted = False
@ -166,7 +163,7 @@ async def get_wallets_paginated(
async def get_wallets_ids( async def get_wallets_ids(
user_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None user_id: str, deleted: bool | None = None, conn: Connection | None = None
) -> list[str]: ) -> list[str]:
query = """SELECT id FROM wallets WHERE "user" = :user""" query = """SELECT id FROM wallets WHERE "user" = :user"""
if deleted is not None: if deleted is not None:
@ -186,8 +183,8 @@ async def get_wallets_count():
async def get_wallet_for_key( async def get_wallet_for_key(
key: str, key: str,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> Optional[Wallet]: ) -> Wallet | None:
return await (conn or db).fetchone( return await (conn or db).fetchone(
""" """
SELECT *, COALESCE(( SELECT *, COALESCE((
@ -201,7 +198,7 @@ async def get_wallet_for_key(
) )
async def get_total_balance(conn: Optional[Connection] = None): async def get_total_balance(conn: Connection | None = None):
result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances") result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances")
row = result.mappings().first() row = result.mappings().first()
return row.get("balance", 0) or 0 return row.get("balance", 0) or 0

View file

@ -1,5 +1,3 @@
from typing import Optional
from lnbits.core.db import db from lnbits.core.db import db
from ..models import WebPushSubscription from ..models import WebPushSubscription
@ -7,7 +5,7 @@ from ..models import WebPushSubscription
async def get_webpush_subscription( async def get_webpush_subscription(
endpoint: str, user: str endpoint: str, user: str
) -> Optional[WebPushSubscription]: ) -> WebPushSubscription | None:
return await db.fetchone( return await db.fetchone(
""" """
SELECT * FROM webpush_subscriptions SELECT * FROM webpush_subscriptions

View file

@ -1,6 +1,6 @@
import importlib import importlib
import re import re
from typing import Any, Optional from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
from uuid import UUID from uuid import UUID
@ -20,7 +20,7 @@ from lnbits.settings import settings
async def migrate_extension_database( async def migrate_extension_database(
ext: InstallableExtension, current_version: Optional[DbVersion] = None ext: InstallableExtension, current_version: DbVersion | None = None
): ):
try: try:
@ -38,7 +38,7 @@ async def run_migration(
db: Connection, db: Connection,
migrations_module: Any, migrations_module: Any,
db_name: str, db_name: str,
current_version: Optional[DbVersion] = None, current_version: DbVersion | None = None,
): ):
matcher = re.compile(r"^m(\d\d\d)_") matcher = re.compile(r"^m(\d\d\d)_")

View file

@ -1,5 +1,4 @@
from time import time from time import time
from typing import Optional
from lnurl import LnAddress, Lnurl, LnurlPayResponse from lnurl import LnAddress, Lnurl, LnurlPayResponse
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@ -9,9 +8,9 @@ class CreateLnurlPayment(BaseModel):
res: LnurlPayResponse | None = None res: LnurlPayResponse | None = None
lnurl: Lnurl | LnAddress | None = None lnurl: Lnurl | LnAddress | None = None
amount: int amount: int
comment: Optional[str] = None comment: str | None = None
unit: Optional[str] = None unit: str | None = None
internal_memo: Optional[str] = None internal_memo: str | None = None
class CreateLnurlWithdraw(BaseModel): class CreateLnurlWithdraw(BaseModel):

View file

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Callable from collections.abc import Callable
from pydantic import BaseModel from pydantic import BaseModel

View file

@ -1,6 +1,5 @@
import asyncio import asyncio
import importlib import importlib
from typing import Optional
from loguru import logger from loguru import logger
@ -145,7 +144,7 @@ async def start_extension_background_work(ext_id: str) -> bool:
async def get_valid_extensions( async def get_valid_extensions(
include_deactivated: Optional[bool] = True, include_deactivated: bool | None = True,
) -> list[Extension]: ) -> list[Extension]:
installed_extensions = await get_installed_extensions() installed_extensions = await get_installed_extensions()
valid_extensions = [Extension.from_installable_ext(e) for e in installed_extensions] valid_extensions = [Extension.from_installable_ext(e) for e in installed_extensions]
@ -164,8 +163,8 @@ async def get_valid_extensions(
async def get_valid_extension( async def get_valid_extension(
ext_id: str, include_deactivated: Optional[bool] = True ext_id: str, include_deactivated: bool | None = True
) -> Optional[Extension]: ) -> Extension | None:
ext = await get_installed_extension(ext_id) ext = await get_installed_extension(ext_id)
if not ext: if not ext:
return None return None

View file

@ -1,7 +1,6 @@
import hashlib import hashlib
import hmac import hmac
import time import time
from typing import Optional
from loguru import logger from loguru import logger
@ -15,7 +14,7 @@ from lnbits.settings import settings
async def handle_fiat_payment_confirmation( async def handle_fiat_payment_confirmation(
payment: Payment, conn: Optional[Connection] = None payment: Payment, conn: Connection | None = None
): ):
try: try:
await _credit_fiat_service_fee_wallet(payment, conn=conn) await _credit_fiat_service_fee_wallet(payment, conn=conn)
@ -29,7 +28,7 @@ async def handle_fiat_payment_confirmation(
async def _credit_fiat_service_fee_wallet( async def _credit_fiat_service_fee_wallet(
payment: Payment, conn: Optional[Connection] = None payment: Payment, conn: Connection | None = None
): ):
fiat_provider_name = payment.fiat_provider fiat_provider_name = payment.fiat_provider
if not fiat_provider_name: if not fiat_provider_name:
@ -66,7 +65,7 @@ async def _credit_fiat_service_fee_wallet(
async def _debit_fiat_service_faucet_wallet( async def _debit_fiat_service_faucet_wallet(
payment: Payment, conn: Optional[Connection] = None payment: Payment, conn: Connection | None = None
): ):
fiat_provider_name = payment.fiat_provider fiat_provider_name = payment.fiat_provider
if not fiat_provider_name: if not fiat_provider_name:
@ -129,8 +128,8 @@ async def handle_stripe_event(event: dict):
def check_stripe_signature( def check_stripe_signature(
payload: bytes, payload: bytes,
sig_header: Optional[str], sig_header: str | None,
secret: Optional[str], secret: str | None,
tolerance_seconds=300, tolerance_seconds=300,
): ):
if not sig_header: if not sig_header:

View file

@ -4,7 +4,6 @@ import smtplib
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from http import HTTPStatus from http import HTTPStatus
from typing import Optional
import httpx import httpx
from loguru import logger from loguru import logger
@ -71,7 +70,7 @@ async def process_next_notification() -> None:
async def send_admin_notification( async def send_admin_notification(
message: str, message: str,
message_type: Optional[str] = None, message_type: str | None = None,
) -> None: ) -> None:
return await send_notification( return await send_notification(
settings.lnbits_telegram_notifications_chat_id, settings.lnbits_telegram_notifications_chat_id,
@ -85,7 +84,7 @@ async def send_admin_notification(
async def send_user_notification( async def send_user_notification(
user_notifications: UserNotifications, user_notifications: UserNotifications,
message: str, message: str,
message_type: Optional[str] = None, message_type: str | None = None,
) -> None: ) -> None:
email_address = ( email_address = (
@ -110,7 +109,7 @@ async def send_notification(
nostr_identifiers: list[str] | None, nostr_identifiers: list[str] | None,
email_addresses: list[str] | None, email_addresses: list[str] | None,
message: str, message: str,
message_type: Optional[str] = None, message_type: str | None = None,
) -> None: ) -> None:
try: try:
if telegram_chat_id and settings.is_telegram_notifications_configured(): if telegram_chat_id and settings.is_telegram_notifications_configured():

View file

@ -1,7 +1,6 @@
import asyncio import asyncio
import time import time
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional
from bolt11 import Bolt11, MilliSatoshi, Tags from bolt11 import Bolt11, MilliSatoshi, Tags
from bolt11 import decode as bolt11_decode from bolt11 import decode as bolt11_decode
@ -58,11 +57,11 @@ async def pay_invoice(
*, *,
wallet_id: str, wallet_id: str,
payment_request: str, payment_request: str,
max_sat: Optional[int] = None, max_sat: int | None = None,
extra: Optional[dict] = None, extra: dict | None = None,
description: str = "", description: str = "",
tag: str = "", tag: str = "",
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> Payment: ) -> Payment:
if settings.lnbits_only_allow_incoming_payments: if settings.lnbits_only_allow_incoming_payments:
raise PaymentError("Only incoming payments allowed.", status="failed") raise PaymentError("Only incoming payments allowed.", status="failed")
@ -110,7 +109,7 @@ async def create_payment_request(
async def create_fiat_invoice( async def create_fiat_invoice(
wallet_id: str, invoice_data: CreateInvoice, conn: Optional[Connection] = None wallet_id: str, invoice_data: CreateInvoice, conn: Connection | None = None
): ):
fiat_provider_name = invoice_data.fiat_provider fiat_provider_name = invoice_data.fiat_provider
if not fiat_provider_name: if not fiat_provider_name:
@ -231,16 +230,16 @@ async def create_invoice(
*, *,
wallet_id: str, wallet_id: str,
amount: float, amount: float,
currency: Optional[str] = "sat", currency: str | None = "sat",
memo: str, memo: str,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
expiry: Optional[int] = None, expiry: int | None = None,
extra: Optional[dict] = None, extra: dict | None = None,
webhook: Optional[str] = None, webhook: str | None = None,
internal: Optional[bool] = False, internal: bool | None = False,
payment_hash: str | None = None, payment_hash: str | None = None,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> Payment: ) -> Payment:
if not amount > 0: if not amount > 0:
raise InvoiceError("Amountless invoices not supported.", status="failed") raise InvoiceError("Amountless invoices not supported.", status="failed")
@ -427,7 +426,7 @@ def service_fee_fiat(amount_msat: int, fiat_provider_name: str) -> int:
async def update_wallet_balance( async def update_wallet_balance(
wallet: Wallet, wallet: Wallet,
amount: int, amount: int,
conn: Optional[Connection] = None, conn: Connection | None = None,
): ):
if amount == 0: if amount == 0:
raise ValueError("Amount cannot be 0.") raise ValueError("Amount cannot be 0.")
@ -486,14 +485,14 @@ async def update_wallet_balance(
async def check_wallet_limits( async def check_wallet_limits(
wallet_id: str, amount_msat: int, conn: Optional[Connection] = None wallet_id: str, amount_msat: int, conn: Connection | None = None
): ):
await check_time_limit_between_transactions(wallet_id, conn) await check_time_limit_between_transactions(wallet_id, conn)
await check_wallet_daily_withdraw_limit(wallet_id, amount_msat, conn) await check_wallet_daily_withdraw_limit(wallet_id, amount_msat, conn)
async def check_time_limit_between_transactions( async def check_time_limit_between_transactions(
wallet_id: str, conn: Optional[Connection] = None wallet_id: str, conn: Connection | None = None
): ):
limit = settings.lnbits_wallet_limit_secs_between_trans limit = settings.lnbits_wallet_limit_secs_between_trans
if not limit or limit <= 0: if not limit or limit <= 0:
@ -513,7 +512,7 @@ async def check_time_limit_between_transactions(
async def check_wallet_daily_withdraw_limit( async def check_wallet_daily_withdraw_limit(
wallet_id: str, amount_msat: int, conn: Optional[Connection] = None wallet_id: str, amount_msat: int, conn: Connection | None = None
): ):
limit = settings.lnbits_wallet_limit_daily_max_withdraw limit = settings.lnbits_wallet_limit_daily_max_withdraw
if not limit: if not limit:
@ -546,8 +545,8 @@ async def check_wallet_daily_withdraw_limit(
async def calculate_fiat_amounts( async def calculate_fiat_amounts(
amount: float, amount: float,
wallet: Wallet, wallet: Wallet,
currency: Optional[str] = None, currency: str | None = None,
extra: Optional[dict] = None, extra: dict | None = None,
) -> tuple[int, dict]: ) -> tuple[int, dict]:
wallet_currency = wallet.currency or settings.lnbits_default_accounting_currency wallet_currency = wallet.currency or settings.lnbits_default_accounting_currency
fiat_amounts: dict = extra or {} fiat_amounts: dict = extra or {}
@ -582,9 +581,9 @@ async def calculate_fiat_amounts(
async def check_transaction_status( async def check_transaction_status(
wallet_id: str, payment_hash: str, conn: Optional[Connection] = None wallet_id: str, payment_hash: str, conn: Connection | None = None
) -> PaymentStatus: ) -> PaymentStatus:
payment: Optional[Payment] = await get_wallet_payment( payment: Payment | None = await get_wallet_payment(
wallet_id, payment_hash, conn=conn wallet_id, payment_hash, conn=conn
) )
if not payment: if not payment:
@ -598,7 +597,7 @@ async def check_transaction_status(
async def get_payments_daily_stats( async def get_payments_daily_stats(
filters: Filters[PaymentFilters], filters: Filters[PaymentFilters],
user_id: Optional[str] = None, user_id: str | None = None,
) -> list[PaymentDailyStats]: ) -> list[PaymentDailyStats]:
data_in, data_out = await get_daily_stats(filters, user_id=user_id) data_in, data_out = await get_daily_stats(filters, user_id=user_id)
balance_total: float = 0 balance_total: float = 0
@ -647,7 +646,7 @@ async def get_payments_daily_stats(
async def _pay_invoice( async def _pay_invoice(
wallet_id: str, wallet_id: str,
create_payment_model: CreatePayment, create_payment_model: CreatePayment,
conn: Optional[Connection] = None, conn: Connection | None = None,
): ):
async with payment_lock: async with payment_lock:
if wallet_id not in wallets_payments_lock: if wallet_id not in wallets_payments_lock:
@ -670,8 +669,8 @@ async def _pay_invoice(
async def _pay_internal_invoice( async def _pay_internal_invoice(
wallet: Wallet, wallet: Wallet,
create_payment_model: CreatePayment, create_payment_model: CreatePayment,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> Optional[Payment]: ) -> Payment | None:
""" """
Pay an internal payment. Pay an internal payment.
returns None if the payment is not internal. returns None if the payment is not internal.
@ -738,7 +737,7 @@ async def _pay_internal_invoice(
async def _pay_external_invoice( async def _pay_external_invoice(
wallet: Wallet, wallet: Wallet,
create_payment_model: CreatePayment, create_payment_model: CreatePayment,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> Payment: ) -> Payment:
checking_id = create_payment_model.payment_hash checking_id = create_payment_model.payment_hash
amount_msat = create_payment_model.amount_msat amount_msat = create_payment_model.amount_msat
@ -807,7 +806,7 @@ async def _pay_external_invoice(
async def update_payment_success_status( async def update_payment_success_status(
payment: Payment, payment: Payment,
status: PaymentStatus, status: PaymentStatus,
conn: Optional[Connection] = None, conn: Connection | None = None,
) -> Payment: ) -> Payment:
if status.success: if status.success:
service_fee_msat = service_fee(payment.amount, internal=False) service_fee_msat = service_fee(payment.amount, internal=False)
@ -831,7 +830,7 @@ async def _fundingsource_pay_invoice(
async def _verify_external_payment( async def _verify_external_payment(
payment: Payment, conn: Optional[Connection] = None payment: Payment, conn: Connection | None = None
) -> Payment: ) -> Payment:
# fail on pending payments # fail on pending payments
if payment.pending: if payment.pending:
@ -862,7 +861,7 @@ async def _check_wallet_for_payment(
wallet_id: str, wallet_id: str,
tag: str, tag: str,
amount_msat: int, amount_msat: int,
conn: Optional[Connection] = None, conn: Connection | None = None,
): ):
wallet = await get_wallet(wallet_id, conn=conn) wallet = await get_wallet(wallet_id, conn=conn)
if not wallet: if not wallet:
@ -878,7 +877,7 @@ async def _check_wallet_for_payment(
def _validate_payment_request( def _validate_payment_request(
payment_request: str, max_sat: Optional[int] = None payment_request: str, max_sat: int | None = None
) -> Bolt11: ) -> Bolt11:
try: try:
invoice = bolt11_decode(payment_request) invoice = bolt11_decode(payment_request)
@ -901,7 +900,7 @@ def _validate_payment_request(
async def _credit_service_fee_wallet( async def _credit_service_fee_wallet(
wallet: Wallet, payment: Payment, conn: Optional[Connection] = None wallet: Wallet, payment: Payment, conn: Connection | None = None
): ):
service_fee_msat = service_fee(payment.amount, internal=payment.is_internal) service_fee_msat = service_fee(payment.amount, internal=payment.is_internal)
if not settings.lnbits_service_fee_wallet or not service_fee_msat: if not settings.lnbits_service_fee_wallet or not service_fee_msat:
@ -927,7 +926,7 @@ async def _credit_service_fee_wallet(
async def _check_fiat_invoice_limits( async def _check_fiat_invoice_limits(
amount_sat: int, fiat_provider_name: str, conn: Optional[Connection] = None amount_sat: int, fiat_provider_name: str, conn: Connection | None = None
): ):
limits = settings.get_fiat_provider_limits(fiat_provider_name) limits = settings.get_fiat_provider_limits(fiat_provider_name)
if not limits: if not limits:

View file

@ -1,5 +1,4 @@
from pathlib import Path from pathlib import Path
from typing import Optional
from uuid import uuid4 from uuid import uuid4
from loguru import logger from loguru import logger
@ -37,7 +36,7 @@ from .settings import update_cached_settings
async def create_user_account( async def create_user_account(
account: Optional[Account] = None, wallet_name: Optional[str] = None account: Account | None = None, wallet_name: str | None = None
) -> User: ) -> User:
if not settings.new_accounts_allowed: if not settings.new_accounts_allowed:
raise ValueError("Account creation is disabled.") raise ValueError("Account creation is disabled.")
@ -46,9 +45,9 @@ async def create_user_account(
async def create_user_account_no_ckeck( async def create_user_account_no_ckeck(
account: Optional[Account] = None, account: Account | None = None,
wallet_name: Optional[str] = None, wallet_name: str | None = None,
default_exts: Optional[list[str]] = None, default_exts: list[str] | None = None,
) -> User: ) -> User:
if account: if account:
@ -165,12 +164,12 @@ async def check_admin_settings():
settings.first_install = True settings.first_install = True
logger.success( logger.success(
"✔️ Admin UI is enabled. run `poetry run lnbits-cli superuser` " "✔️ Admin UI is enabled. run `uv run lnbits-cli superuser` "
"to get the superuser." "to get the superuser."
) )
async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings: async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
account = None account = None
if super_user: if super_user:
account = await get_account(super_user) account = await get_account(super_user)

View file

@ -1,7 +1,6 @@
import asyncio import asyncio
import traceback import traceback
from collections.abc import Coroutine from collections.abc import Callable, Coroutine
from typing import Callable
from loguru import logger from loguru import logger

View file

@ -5,7 +5,7 @@ from pathlib import Path
from shutil import make_archive, move from shutil import make_archive, move
from subprocess import Popen from subprocess import Popen
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import IO, Optional from typing import IO
from urllib.parse import urlparse from urllib.parse import urlparse
import filetype import filetype
@ -71,10 +71,10 @@ async def api_test_email():
) )
@admin_router.get("/api/v1/settings", response_model=Optional[AdminSettings]) @admin_router.get("/api/v1/settings")
async def api_get_settings( async def api_get_settings(
user: User = Depends(check_admin), user: User = Depends(check_admin),
) -> Optional[AdminSettings]: ) -> AdminSettings | None:
admin_settings = await get_admin_settings(user.super_user) admin_settings = await get_admin_settings(user.super_user)
return admin_settings return admin_settings

View file

@ -1,9 +1,9 @@
import base64 import base64
import importlib import importlib
import json import json
from collections.abc import Callable
from http import HTTPStatus from http import HTTPStatus
from time import time from time import time
from typing import Callable, Optional
from uuid import uuid4 from uuid import uuid4
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException, Request
@ -238,7 +238,7 @@ async def api_delete_user_api_token(
@auth_router.get("/{provider}", description="SSO Provider") @auth_router.get("/{provider}", description="SSO Provider")
async def login_with_sso_provider( async def login_with_sso_provider(
request: Request, provider: str, user_id: Optional[str] = None request: Request, provider: str, user_id: str | None = None
): ):
provider_sso = _new_sso(provider) provider_sso = _new_sso(provider)
if not provider_sso: if not provider_sso:
@ -319,7 +319,7 @@ async def update_pubkey(
data: UpdateUserPubkey, data: UpdateUserPubkey,
user: User = Depends(check_user_exists), user: User = Depends(check_user_exists),
payload: AccessTokenPayload = Depends(access_token_payload), payload: AccessTokenPayload = Depends(access_token_payload),
) -> Optional[User]: ) -> User | None:
if data.user_id != user.id: if data.user_id != user.id:
raise ValueError("Invalid user ID.") raise ValueError("Invalid user ID.")
@ -345,7 +345,7 @@ async def update_password(
data: UpdateUserPassword, data: UpdateUserPassword,
user: User = Depends(check_user_exists), user: User = Depends(check_user_exists),
payload: AccessTokenPayload = Depends(access_token_payload), payload: AccessTokenPayload = Depends(access_token_payload),
) -> Optional[User]: ) -> User | None:
_validate_auth_timeout(payload.auth_time) _validate_auth_timeout(payload.auth_time)
if data.user_id != user.id: if data.user_id != user.id:
raise ValueError("Invalid user ID.") raise ValueError("Invalid user ID.")
@ -419,7 +419,7 @@ async def reset_password(data: ResetUserPassword) -> JSONResponse:
@auth_router.put("/update") @auth_router.put("/update")
async def update( async def update(
data: UpdateUser, user: User = Depends(check_user_exists) data: UpdateUser, user: User = Depends(check_user_exists)
) -> Optional[User]: ) -> User | None:
if data.user_id != user.id: if data.user_id != user.id:
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.") raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
if data.username and not is_valid_username(data.username): if data.username and not is_valid_username(data.username):
@ -461,7 +461,7 @@ async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
return _auth_success_response(account.username, account.id, account.email) return _auth_success_response(account.username, account.id, account.email)
async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] = None): async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = None):
email = userinfo.email email = userinfo.email
if not email or not is_valid_email_address(email): if not email or not is_valid_email_address(email):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.") raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
@ -490,9 +490,9 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] =
def _auth_success_response( def _auth_success_response(
username: Optional[str] = None, username: str | None = None,
user_id: Optional[str] = None, user_id: str | None = None,
email: Optional[str] = None, email: str | None = None,
) -> JSONResponse: ) -> JSONResponse:
payload = AccessTokenPayload( payload = AccessTokenPayload(
sub=username or "", usr=user_id, email=email, auth_time=int(time()) sub=username or "", usr=user_id, email=email, auth_time=int(time())
@ -533,7 +533,7 @@ def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
return response return response
def _new_sso(provider: str) -> Optional[SSOBase]: def _new_sso(provider: str) -> SSOBase | None:
try: try:
if not settings.is_auth_method_allowed(AuthMethods(f"{provider}-auth")): if not settings.is_auth_method_allowed(AuthMethods(f"{provider}-auth")):
return None return None
@ -610,7 +610,7 @@ def _nostr_nip98_event(request: Request) -> dict:
def _check_nostr_event_tags(event: dict): def _check_nostr_event_tags(event: dict):
method: Optional[str] = next((v for k, v in event["tags"] if k == "method"), None) method: str | None = next((v for k, v in event["tags"] if k == "method"), None)
if not method: if not method:
raise ValueError("Tag 'method' is missing.") raise ValueError("Tag 'method' is missing.")
if not method.upper() == "POST": if not method.upper() == "POST":
@ -625,7 +625,7 @@ def _check_nostr_event_tags(event: dict):
raise ValueError(f"Invalid value for tag 'u': '{url}'.") raise ValueError(f"Invalid value for tag 'u': '{url}'.")
def _validate_auth_timeout(auth_time: Optional[int] = 0): def _validate_auth_timeout(auth_time: int | None = 0):
if abs(time() - (auth_time or 0)) > settings.auth_credetials_update_threshold: if abs(time() - (auth_time or 0)) > settings.auth_credetials_update_threshold:
raise HTTPException( raise HTTPException(
HTTPStatus.BAD_REQUEST, HTTPStatus.BAD_REQUEST,

View file

@ -1,5 +1,5 @@
from http import HTTPStatus from http import HTTPStatus
from typing import Annotated, Optional, Union from typing import Annotated
from urllib.parse import urlencode, urlparse from urllib.parse import urlencode, urlparse
import httpx import httpx
@ -161,9 +161,9 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
) )
async def wallet( async def wallet(
request: Request, request: Request,
lnbits_last_active_wallet: Annotated[Union[str, None], Cookie()] = None, lnbits_last_active_wallet: Annotated[str | None, Cookie()] = None,
user: User = Depends(check_user_exists), user: User = Depends(check_user_exists),
wal: Optional[UUID4] = Query(None), wal: UUID4 | None = Query(None),
): ):
if wal: if wal:
wallet = await get_wallet(wal.hex) wallet = await get_wallet(wal.hex)

View file

@ -60,7 +60,7 @@ async def _handle(lnurl: str) -> LnurlResponseModel:
) )
async def api_lnurlscan(code: str) -> LnurlResponseModel: async def api_lnurlscan(code: str) -> LnurlResponseModel:
res = await _handle(code) res = await _handle(code)
if isinstance(res, (LnurlPayResponse, LnurlWithdrawResponse, LnurlAuthResponse)): if isinstance(res, LnurlPayResponse | LnurlWithdrawResponse | LnurlAuthResponse):
check_callback_url(res.callback) check_callback_url(res.callback)
return res return res
@ -169,7 +169,7 @@ async def api_payment_pay_with_nfc(
except (LnurlResponseException, Exception) as exc: except (LnurlResponseException, Exception) as exc:
logger.warning(exc) logger.warning(exc)
return LnurlErrorResponse(reason=str(exc)) return LnurlErrorResponse(reason=str(exc))
if not isinstance(res2, (LnurlSuccessResponse, LnurlErrorResponse)): if not isinstance(res2, LnurlSuccessResponse | LnurlErrorResponse):
return LnurlErrorResponse(reason="Invalid LNURL-withdraw response.") return LnurlErrorResponse(reason="Invalid LNURL-withdraw response.")
return res2 return res2

View file

@ -1,5 +1,4 @@
from http import HTTPStatus from http import HTTPStatus
from typing import Optional
import httpx import httpx
from fastapi import APIRouter, Body, Depends, HTTPException from fastapi import APIRouter, Body, Depends, HTTPException
@ -89,14 +88,14 @@ async def api_get_public_info(node: Node = Depends(require_node)) -> PublicNodeI
@node_router.get("/info") @node_router.get("/info")
async def api_get_info( async def api_get_info(
node: Node = Depends(require_node), node: Node = Depends(require_node),
) -> Optional[NodeInfoResponse]: ) -> NodeInfoResponse | None:
return await node.get_info() return await node.get_info()
@node_router.get("/channels") @node_router.get("/channels")
async def api_get_channels( async def api_get_channels(
node: Node = Depends(require_node), node: Node = Depends(require_node),
) -> Optional[list[NodeChannel]]: ) -> list[NodeChannel] | None:
return await node.get_channels() return await node.get_channels()
@ -104,7 +103,7 @@ async def api_get_channels(
async def api_get_channel( async def api_get_channel(
channel_id: str, channel_id: str,
node: Node = Depends(require_node), node: Node = Depends(require_node),
) -> Optional[NodeChannel]: ) -> NodeChannel | None:
return await node.get_channel(channel_id) return await node.get_channel(channel_id)
@ -113,20 +112,20 @@ async def api_create_channel(
node: Node = Depends(require_node), node: Node = Depends(require_node),
peer_id: str = Body(), peer_id: str = Body(),
funding_amount: int = Body(), funding_amount: int = Body(),
push_amount: Optional[int] = Body(None), push_amount: int | None = Body(None),
fee_rate: Optional[int] = Body(None), fee_rate: int | None = Body(None),
): ):
return await node.open_channel(peer_id, funding_amount, push_amount, fee_rate) return await node.open_channel(peer_id, funding_amount, push_amount, fee_rate)
@super_node_router.delete("/channels") @super_node_router.delete("/channels")
async def api_delete_channel( async def api_delete_channel(
short_id: Optional[str], short_id: str | None,
funding_txid: Optional[str], funding_txid: str | None,
output_index: Optional[int], output_index: int | None,
force: bool = False, force: bool = False,
node: Node = Depends(require_node), node: Node = Depends(require_node),
) -> Optional[list[NodeChannel]]: ) -> list[NodeChannel] | None:
return await node.close_channel( return await node.close_channel(
short_id, short_id,
( (
@ -152,7 +151,7 @@ async def api_set_channel_fees(
async def api_get_payments( async def api_get_payments(
node: Node = Depends(require_node), node: Node = Depends(require_node),
filters: Filters = Depends(parse_filters(NodePaymentsFilters)), filters: Filters = Depends(parse_filters(NodePaymentsFilters)),
) -> Optional[Page[NodePayment]]: ) -> Page[NodePayment] | None:
if not settings.lnbits_node_ui_transactions: if not settings.lnbits_node_ui_transactions:
raise HTTPException( raise HTTPException(
HTTP_503_SERVICE_UNAVAILABLE, HTTP_503_SERVICE_UNAVAILABLE,
@ -165,7 +164,7 @@ async def api_get_payments(
async def api_get_invoices( async def api_get_invoices(
node: Node = Depends(require_node), node: Node = Depends(require_node),
filters: Filters = Depends(parse_filters(NodeInvoiceFilters)), filters: Filters = Depends(parse_filters(NodeInvoiceFilters)),
) -> Optional[Page[NodeInvoice]]: ) -> Page[NodeInvoice] | None:
if not settings.lnbits_node_ui_transactions: if not settings.lnbits_node_ui_transactions:
raise HTTPException( raise HTTPException(
HTTP_503_SERVICE_UNAVAILABLE, HTTP_503_SERVICE_UNAVAILABLE,
@ -192,25 +191,25 @@ async def api_disconnect_peer(peer_id: str, node: Node = Depends(require_node)):
class NodeRank(BaseModel): class NodeRank(BaseModel):
capacity: Optional[int] capacity: int | None
channelcount: Optional[int] channelcount: int | None
age: Optional[int] age: int | None
growth: Optional[int] growth: int | None
availability: Optional[int] availability: int | None
# Same for public and private api # Same for public and private api
@node_router.get( @node_router.get(
"/rank", "/rank",
description="Retrieve node ranks from https://1ml.com", description="Retrieve node ranks from https://1ml.com",
response_model=Optional[NodeRank], response_model=NodeRank | None,
) )
@public_node_router.get( @public_node_router.get(
"/rank", "/rank",
description="Retrieve node ranks from https://1ml.com", description="Retrieve node ranks from https://1ml.com",
response_model=Optional[NodeRank], response_model=NodeRank | None,
) )
async def api_get_1ml_stats(node: Node = Depends(require_node)) -> Optional[NodeRank]: async def api_get_1ml_stats(node: Node = Depends(require_node)) -> NodeRank | None:
node_id = await node.get_id() node_id = await node.get_id()
headers = {"User-Agent": settings.user_agent} headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client: async with httpx.AsyncClient(headers=headers) as client:

View file

@ -1,6 +1,5 @@
from hashlib import sha256 from hashlib import sha256
from http import HTTPStatus from http import HTTPStatus
from typing import Optional
from fastapi import ( from fastapi import (
APIRouter, APIRouter,
@ -275,7 +274,7 @@ async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONRespo
# TODO: refactor this route into a public and admin one # TODO: refactor this route into a public and admin one
@payment_router.get("/{payment_hash}") @payment_router.get("/{payment_hash}")
async def api_payment(payment_hash, x_api_key: Optional[str] = Header(None)): async def api_payment(payment_hash, x_api_key: str | None = Header(None)):
# We use X_Api_Key here because we want this call to work with and without keys # We use X_Api_Key here because we want this call to work with and without keys
# If a valid key is given, we also return the field "details", otherwise not # If a valid key is given, we also return the field "details", otherwise not
wallet = await get_wallet_for_key(x_api_key) if isinstance(x_api_key, str) else None wallet = await get_wallet_for_key(x_api_key) if isinstance(x_api_key, str) else None

View file

@ -2,7 +2,6 @@ import base64
import json import json
import time import time
from http import HTTPStatus from http import HTTPStatus
from typing import Optional
from uuid import uuid4 from uuid import uuid4
import shortuuid import shortuuid
@ -223,7 +222,7 @@ async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
@users_router.post("/user/{user_id}/wallet", name="Create a new wallet for user") @users_router.post("/user/{user_id}/wallet", name="Create a new wallet for user")
async def api_users_create_user_wallet( async def api_users_create_user_wallet(
user_id: str, name: Optional[str] = Body(None), currency: Optional[str] = Body(None) user_id: str, name: str | None = Body(None), currency: str | None = Body(None)
): ):
if currency and currency not in allowed_currencies(): if currency and currency not in allowed_currencies():
raise ValueError(f"Currency '{currency}' not allowed.") raise ValueError(f"Currency '{currency}' not allowed.")

View file

@ -1,5 +1,4 @@
from http import HTTPStatus from http import HTTPStatus
from typing import Optional
from uuid import uuid4 from uuid import uuid4
from fastapi import ( from fastapi import (
@ -110,11 +109,11 @@ async def api_put_stored_paylinks(
@wallet_router.patch("") @wallet_router.patch("")
async def api_update_wallet( async def api_update_wallet(
name: Optional[str] = Body(None), name: str | None = Body(None),
icon: Optional[str] = Body(None), icon: str | None = Body(None),
color: Optional[str] = Body(None), color: str | None = Body(None),
currency: Optional[str] = Body(None), currency: str | None = Body(None),
pinned: Optional[bool] = Body(None), pinned: bool | None = Body(None),
key_info: WalletTypeInfo = Depends(require_admin_key), key_info: WalletTypeInfo = Depends(require_admin_key),
) -> Wallet: ) -> Wallet:
wallet = await get_wallet(key_info.wallet.id) wallet = await get_wallet(key_info.wallet.id)

View file

@ -1,5 +1,5 @@
from http import HTTPStatus from http import HTTPStatus
from typing import Annotated, Literal, Optional, Union from typing import Annotated, Literal
import jwt import jwt
from fastapi import Cookie, Depends, Query, Request, Security from fastapi import Cookie, Depends, Query, Request, Security
@ -55,8 +55,8 @@ api_key_query = APIKeyQuery(
class KeyChecker(SecurityBase): class KeyChecker(SecurityBase):
def __init__( def __init__(
self, self,
api_key: Optional[str] = None, api_key: str | None = None,
expected_key_type: Optional[KeyType] = None, expected_key_type: KeyType | None = None,
): ):
self.auto_error: bool = True self.auto_error: bool = True
self.expected_key_type = expected_key_type self.expected_key_type = expected_key_type
@ -137,17 +137,17 @@ async def require_invoice_key(
async def check_access_token( async def check_access_token(
header_access_token: Annotated[Union[str, None], Depends(oauth2_scheme)], header_access_token: Annotated[str | None, Depends(oauth2_scheme)],
cookie_access_token: Annotated[Union[str, None], Cookie()] = None, cookie_access_token: Annotated[str | None, Cookie()] = None,
bearer_access_token: Annotated[Union[str, None], Depends(http_bearer)] = None, bearer_access_token: Annotated[str | None, Depends(http_bearer)] = None,
) -> Optional[str]: ) -> str | None:
return header_access_token or cookie_access_token or bearer_access_token return header_access_token or cookie_access_token or bearer_access_token
async def check_user_exists( async def check_user_exists(
r: Request, r: Request,
access_token: Annotated[Optional[str], Depends(check_access_token)], access_token: Annotated[str | None, Depends(check_access_token)],
usr: Optional[UUID4] = None, usr: UUID4 | None = None,
) -> User: ) -> User:
if access_token: if access_token:
account = await _get_account_from_token(access_token, r["path"], r["method"]) account = await _get_account_from_token(access_token, r["path"], r["method"])
@ -176,9 +176,9 @@ async def check_user_exists(
async def optional_user_id( async def optional_user_id(
r: Request, r: Request,
access_token: Annotated[Optional[str], Depends(check_access_token)], access_token: Annotated[str | None, Depends(check_access_token)],
usr: Optional[UUID4] = None, usr: UUID4 | None = None,
) -> Optional[str]: ) -> str | None:
if usr and settings.is_auth_method_allowed(AuthMethods.user_id_only): if usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
return usr.hex return usr.hex
if access_token: if access_token:
@ -189,7 +189,7 @@ async def optional_user_id(
async def access_token_payload( async def access_token_payload(
access_token: Annotated[Optional[str], Depends(check_access_token)], access_token: Annotated[str | None, Depends(check_access_token)],
) -> AccessTokenPayload: ) -> AccessTokenPayload:
if not access_token: if not access_token:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing access token.") raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing access token.")
@ -231,11 +231,11 @@ def parse_filters(model: type[TFilterModel]):
def dependency( def dependency(
request: Request, request: Request,
limit: Optional[int] = None, limit: int | None = None,
offset: Optional[int] = None, offset: int | None = None,
sortby: Optional[str] = None, sortby: str | None = None,
direction: Optional[Literal["asc", "desc"]] = None, direction: Literal["asc", "desc"] | None = None,
search: Optional[str] = Query(None, description="Text based search"), search: str | None = Query(None, description="Text based search"),
): ):
params = request.query_params params = request.query_params
filters = [] filters = []
@ -259,7 +259,7 @@ def parse_filters(model: type[TFilterModel]):
async def check_user_extension_access( async def check_user_extension_access(
user_id: str, ext_id: str, conn: Optional[Connection] = None user_id: str, ext_id: str, conn: Connection | None = None
) -> SimpleStatus: ) -> SimpleStatus:
""" """
Check if the user has access to a particular extension. Check if the user has access to a particular extension.
@ -292,7 +292,7 @@ async def _check_user_extension_access(user_id: str, path: str):
async def _get_account_from_token( async def _get_account_from_token(
access_token: str, path: str, method: str access_token: str, path: str, method: str
) -> Optional[Account]: ) -> Account | None:
try: try:
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"]) payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
return await _get_account_from_jwt_payload( return await _get_account_from_jwt_payload(
@ -310,7 +310,7 @@ async def _get_account_from_token(
async def _get_account_from_jwt_payload( async def _get_account_from_jwt_payload(
payload: AccessTokenPayload, path: str, method: str payload: AccessTokenPayload, path: str, method: str
) -> Optional[Account]: ) -> Account | None:
account = None account = None
if payload.sub: if payload.sub:
account = await get_account_by_username(payload.sub) account = await get_account_by_username(payload.sub)

View file

@ -1,7 +1,6 @@
import sys import sys
import traceback import traceback
from http import HTTPStatus from http import HTTPStatus
from typing import Optional
from fastapi import FastAPI, HTTPException, Request from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
@ -30,7 +29,7 @@ class UnsupportedError(Exception):
pass pass
def render_html_error(request: Request, exc: Exception) -> Optional[Response]: def render_html_error(request: Request, exc: Exception) -> Response | None:
# Only the browser sends "text/html" request # Only the browser sends "text/html" request
# not fail proof, but everything else get's a JSON response # not fail proof, but everything else get's a JSON response

View file

@ -2,7 +2,6 @@ import asyncio
import json import json
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional
from urllib.parse import urlencode from urllib.parse import urlencode
import httpx import httpx
@ -49,7 +48,7 @@ class StripeWallet(FiatProvider):
logger.warning(f"Error closing stripe wallet connection: {e}") logger.warning(f"Error closing stripe wallet connection: {e}")
async def status( async def status(
self, only_check_settings: Optional[bool] = False self, only_check_settings: bool | None = False
) -> FiatStatusResponse: ) -> FiatStatusResponse:
if only_check_settings: if only_check_settings:
if self._settings_fields != self._settings_connection_fields(): if self._settings_fields != self._settings_connection_fields():
@ -76,7 +75,7 @@ class StripeWallet(FiatProvider):
amount: float, amount: float,
payment_hash: str, payment_hash: str,
currency: str, currency: str,
memo: Optional[str] = None, memo: str | None = None,
**kwargs, **kwargs,
) -> FiatInvoiceResponse: ) -> FiatInvoiceResponse:
amount_cents = int(amount * 100) amount_cents = int(amount * 100)

View file

@ -3,7 +3,7 @@ import json
import re import re
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Optional from typing import Any
from urllib import request from urllib import request
from urllib.parse import urlparse from urllib.parse import urlparse
@ -39,7 +39,7 @@ def urlsafe_short_hash() -> str:
return shortuuid.uuid() return shortuuid.uuid()
def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> str: def url_for(endpoint: str, external: bool | None = False, **params: Any) -> str:
base = f"http://{settings.host}:{settings.port}" if external else "" base = f"http://{settings.host}:{settings.port}" if external else ""
url_params = "?" url_params = "?"
for key, value in params.items(): for key, value in params.items():
@ -52,7 +52,7 @@ def static_url_for(static: str, path: str) -> str:
return f"/{static}/{path}?v={settings.server_startup_time}" return f"/{static}/{path}?v={settings.server_startup_time}"
def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templates: def template_renderer(additional_folders: list | None = None) -> Jinja2Templates:
folders = ["lnbits/templates", "lnbits/core/templates"] folders = ["lnbits/templates", "lnbits/core/templates"]
if additional_folders: if additional_folders:
additional_folders += [ additional_folders += [
@ -211,7 +211,7 @@ def is_valid_pubkey(pubkey: str) -> bool:
return False return False
def create_access_token(data: dict, token_expire_minutes: Optional[int] = None) -> str: def create_access_token(data: dict, token_expire_minutes: int | None = None) -> str:
minutes = token_expire_minutes or settings.auth_token_expire_minutes minutes = token_expire_minutes or settings.auth_token_expire_minutes
expire = datetime.now(timezone.utc) + timedelta(minutes=minutes) expire = datetime.now(timezone.utc) + timedelta(minutes=minutes)
to_encode = {k: v for k, v in data.items() if v is not None} to_encode = {k: v for k, v in data.items() if v is not None}
@ -219,9 +219,7 @@ def create_access_token(data: dict, token_expire_minutes: Optional[int] = None)
return jwt.encode(to_encode, settings.auth_secret_key, "HS256") return jwt.encode(to_encode, settings.auth_secret_key, "HS256")
def encrypt_internal_message( def encrypt_internal_message(m: str | None = None, urlsafe: bool = False) -> str | None:
m: Optional[str] = None, urlsafe: bool = False
) -> Optional[str]:
""" """
Encrypt message with the internal secret key Encrypt message with the internal secret key
@ -234,9 +232,7 @@ def encrypt_internal_message(
return AESCipher(key=settings.auth_secret_key).encrypt(m.encode(), urlsafe=urlsafe) return AESCipher(key=settings.auth_secret_key).encrypt(m.encode(), urlsafe=urlsafe)
def decrypt_internal_message( def decrypt_internal_message(m: str | None = None, urlsafe: bool = False) -> str | None:
m: Optional[str] = None, urlsafe: bool = False
) -> Optional[str]:
""" """
Decrypt message with the internal secret key Decrypt message with the internal secret key
@ -249,7 +245,7 @@ def decrypt_internal_message(
return AESCipher(key=settings.auth_secret_key).decrypt(m, urlsafe=urlsafe) return AESCipher(key=settings.auth_secret_key).decrypt(m, urlsafe=urlsafe)
def filter_dict_keys(data: dict, filter_keys: Optional[list[str]]) -> dict: def filter_dict_keys(data: dict, filter_keys: list[str] | None) -> dict:
if not filter_keys: if not filter_keys:
# return shallow clone of the dict even if there are no filters # return shallow clone of the dict even if there are no filters
return {**data} return {**data}
@ -270,7 +266,7 @@ def version_parse(v: str):
def is_lnbits_version_ok( def is_lnbits_version_ok(
min_lnbits_version: Optional[str], max_lnbits_version: Optional[str] min_lnbits_version: str | None, max_lnbits_version: str | None
) -> bool: ) -> bool:
if min_lnbits_version and ( if min_lnbits_version and (
version_parse(min_lnbits_version) > version_parse(settings.version) version_parse(min_lnbits_version) > version_parse(settings.version)
@ -347,7 +343,7 @@ def path_segments(path: str) -> list[str]:
return segments[0:] return segments[0:]
def normalize_path(path: Optional[str]) -> str: def normalize_path(path: str | None) -> str:
path = path or "" path = path or ""
return "/" + "/".join(path_segments(path)) return "/" + "/".join(path_segments(path))

View file

@ -2,7 +2,7 @@ import asyncio
import json import json
from datetime import datetime, timezone from datetime import datetime, timezone
from http import HTTPStatus from http import HTTPStatus
from typing import Any, Optional, Union from typing import Any
from fastapi import FastAPI, Request, Response from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
@ -62,7 +62,7 @@ class InstalledExtensionMiddleware:
def _response_by_accepted_type( def _response_by_accepted_type(
self, scope: Scope, headers: list[Any], msg: str, status_code: HTTPStatus self, scope: Scope, headers: list[Any], msg: str, status_code: HTTPStatus
) -> Union[HTMLResponse, JSONResponse]: ) -> HTMLResponse | JSONResponse:
""" """
Build an HTTP response containing the `msg` as HTTP body and the `status_code` 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 as HTTP code. If the `accept` HTTP header is present int the request and
@ -127,7 +127,7 @@ class AuditMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next) -> Response: async def dispatch(self, request: Request, call_next) -> Response:
start_time = datetime.now(timezone.utc) start_time = datetime.now(timezone.utc)
request_details = await self._request_details(request) request_details = await self._request_details(request)
response: Optional[Response] = None response: Response | None = None
try: try:
response = await call_next(request) response = await call_next(request)
@ -140,13 +140,13 @@ class AuditMiddleware(BaseHTTPMiddleware):
async def _log_audit( async def _log_audit(
self, self,
request: Request, request: Request,
response: Optional[Response], response: Response | None,
duration: float, duration: float,
request_details: Optional[str], request_details: str | None,
): ):
try: try:
http_method = request.scope.get("method", None) http_method = request.scope.get("method", None)
path: Optional[str] = getattr(request.scope.get("route", {}), "path", None) path: str | None = getattr(request.scope.get("route", {}), "path", None)
response_code = str(response.status_code) if response else None response_code = str(response.status_code) if response else None
if not settings.audit_http_request(http_method, path, response_code): if not settings.audit_http_request(http_method, path, response_code):
return None return None
@ -178,7 +178,7 @@ class AuditMiddleware(BaseHTTPMiddleware):
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
async def _request_details(self, request: Request) -> Optional[str]: async def _request_details(self, request: Request) -> str | None:
if not settings.audit_http_request_details(): if not settings.audit_http_request_details():
return None return None

View file

@ -35,7 +35,7 @@ def main(
ssl_certfile: str, ssl_certfile: str,
reload: bool, reload: bool,
): ):
"""Launched with `poetry run lnbits` at root level""" """Launched with `uv run lnbits` at root level"""
# create data dir if it does not exist # create data dir if it does not exist
Path(settings.lnbits_data_folder).mkdir(parents=True, exist_ok=True) Path(settings.lnbits_data_folder).mkdir(parents=True, exist_ok=True)

View file

@ -1,11 +1,7 @@
import asyncio import asyncio
import traceback import traceback
import uuid import uuid
from collections.abc import Coroutine from collections.abc import Callable, Coroutine
from typing import (
Callable,
Optional,
)
from loguru import logger from loguru import logger
@ -84,7 +80,7 @@ invoice_listeners: dict[str, asyncio.Queue] = {}
# TODO: name should not be optional # TODO: name should not be optional
# some extensions still dont use a name, but they should # some extensions still dont use a name, but they should
def register_invoice_listener(send_chan: asyncio.Queue, name: Optional[str] = None): def register_invoice_listener(send_chan: asyncio.Queue, name: str | None = None):
""" """
A method intended for extensions (and core/tasks.py) to call when they want to be 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. notified about new invoice payments incoming. Will emit all incoming payments.

View file

@ -1,6 +1,5 @@
from base64 import b64decode, b64encode, urlsafe_b64decode, urlsafe_b64encode from base64 import b64decode, b64encode, urlsafe_b64decode, urlsafe_b64encode
from hashlib import md5, pbkdf2_hmac, sha256 from hashlib import md5, pbkdf2_hmac, sha256
from typing import Union
from Cryptodome import Random from Cryptodome import Random
from Cryptodome.Cipher import AES from Cryptodome.Cipher import AES
@ -40,7 +39,7 @@ class AESCipher:
AES.decrypt(encrypted, password).toString(Utf8); AES.decrypt(encrypted, password).toString(Utf8);
""" """
def __init__(self, key: Union[bytes, str], block_size: int = 16): def __init__(self, key: bytes | str, block_size: int = 16):
self.block_size = block_size self.block_size = block_size
if isinstance(key, bytes): if isinstance(key, bytes):
self.key = key self.key = key

View file

@ -1,6 +1,5 @@
import asyncio import asyncio
import statistics import statistics
from typing import Optional
import httpx import httpx
import jsonpath_ng.ext as jpx import jsonpath_ng.ext as jpx
@ -250,7 +249,7 @@ async def btc_rates(currency: str) -> list[tuple[str, float]]:
async def fetch_price( async def fetch_price(
provider: ExchangeRateProvider, provider: ExchangeRateProvider,
) -> Optional[tuple[str, float]]: ) -> tuple[str, float] | None:
if currency.lower() in provider.exclude_to: if currency.lower() in provider.exclude_to:
logger.warning(f"Provider {provider.name} does not support {currency}.") logger.warning(f"Provider {provider.name} does not support {currency}.")
return None return None

View file

@ -1,9 +1,9 @@
import asyncio import asyncio
import logging import logging
import sys import sys
from collections.abc import Callable
from hashlib import sha256 from hashlib import sha256
from pathlib import Path from pathlib import Path
from typing import Callable
from loguru import logger from loguru import logger

View file

@ -2,7 +2,6 @@ import base64
import hashlib import hashlib
import json import json
import re import re
from typing import Union
from urllib.parse import urlparse from urllib.parse import urlparse
import secp256k1 import secp256k1
@ -149,7 +148,7 @@ def sign_event(
return event return event
def json_dumps(data: Union[dict, list]) -> str: def json_dumps(data: dict | list) -> str:
""" """
Converts a Python dictionary to a JSON string with compact encoding. Converts a Python dictionary to a JSON string with compact encoding.

View file

@ -2,7 +2,6 @@ import asyncio
import hashlib import hashlib
import json import json
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from typing import Optional
import httpx import httpx
from loguru import logger from loguru import logger
@ -71,9 +70,9 @@ class AlbyWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**_, **_,
) -> InvoiceResponse: ) -> InvoiceResponse:
# https://api.getalby.com/invoices # https://api.getalby.com/invoices

View file

@ -2,7 +2,6 @@ import asyncio
import hashlib import hashlib
import json import json
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from typing import Optional
import httpx import httpx
from loguru import logger from loguru import logger
@ -102,9 +101,9 @@ class BlinkWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**kwargs, **kwargs,
) -> InvoiceResponse: ) -> InvoiceResponse:
# https://dev.blink.sv/api/btc-ln-receive # https://dev.blink.sv/api/btc-ln-receive

View file

@ -1,6 +1,5 @@
import asyncio import asyncio
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from typing import Optional
from bolt11.decode import decode from bolt11.decode import decode
from grpc.aio import AioRpcError from grpc.aio import AioRpcError
@ -107,9 +106,9 @@ class BoltzWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**_, **_,
) -> InvoiceResponse: ) -> InvoiceResponse:
pair = boltzrpc_pb2.Pair(to=boltzrpc_pb2.LBTC) pair = boltzrpc_pb2.Pair(to=boltzrpc_pb2.LBTC)
@ -254,7 +253,7 @@ class BoltzWallet(Wallet):
) )
await asyncio.sleep(5) await asyncio.sleep(5)
async def _fetch_wallet(self, wallet_name: str) -> Optional[str]: async def _fetch_wallet(self, wallet_name: str) -> str | None:
try: try:
request = boltzrpc_pb2.GetWalletRequest(name=wallet_name) request = boltzrpc_pb2.GetWalletRequest(name=wallet_name)
response = await self.rpc.GetWallet(request, metadata=self.metadata) response = await self.rpc.GetWallet(request, metadata=self.metadata)

View file

@ -1,3 +1,2 @@
wget https://raw.githubusercontent.com/BoltzExchange/boltz-client/refs/heads/master/pkg/boltzrpc/boltzrpc.proto -O boltz_grpc_files/boltzrpc.proto wget https://raw.githubusercontent.com/BoltzExchange/boltz-client/refs/heads/master/pkg/boltzrpc/boltzrpc.proto -O boltz_grpc_files/boltzrpc.proto
poetry run python -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. --pyi_out=. boltz_grpc_files/boltzrpc.proto uv run python -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. --pyi_out=. boltz_grpc_files/boltzrpc.proto

View file

@ -9,14 +9,13 @@ if not find_spec("breez_sdk"):
def __init__(self): def __init__(self):
raise RuntimeError( raise RuntimeError(
"Breez SDK is not installed. " "Breez SDK is not installed. "
"Ask admin to run `poetry install -E breez` to install it." "Ask admin to run `uv sync --extra breez` to install it."
) )
else: else:
import asyncio import asyncio
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from pathlib import Path from pathlib import Path
from typing import Optional
from bolt11 import Bolt11Exception from bolt11 import Bolt11Exception
from bolt11 import decode as bolt11_decode from bolt11 import decode as bolt11_decode
@ -66,7 +65,7 @@ else:
): ):
breez_incoming_queue.put_nowait(e.details) breez_incoming_queue.put_nowait(e.details)
def load_bytes(source: str, extension: str) -> Optional[bytes]: def load_bytes(source: str, extension: str) -> bytes | None:
# first check if it can be read from a file # first check if it can be read from a file
if source.split(".")[-1] == extension: if source.split(".")[-1] == extension:
with open(source, "rb") as f: with open(source, "rb") as f:
@ -85,7 +84,7 @@ else:
logger.debug(exc) logger.debug(exc)
return None return None
def load_greenlight_credentials() -> Optional[GreenlightCredentials]: def load_greenlight_credentials() -> GreenlightCredentials | None:
if ( if (
settings.breez_greenlight_device_key settings.breez_greenlight_device_key
and settings.breez_greenlight_device_cert and settings.breez_greenlight_device_cert
@ -168,9 +167,9 @@ else:
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**kwargs, **kwargs,
) -> InvoiceResponse: ) -> InvoiceResponse:
# if description_hash or unhashed_description: # if description_hash or unhashed_description:

View file

@ -8,7 +8,7 @@ if not find_spec("breez_sdk_liquid"):
def __init__(self): def __init__(self):
raise RuntimeError( raise RuntimeError(
"Breez Liquid SDK is not installed. " "Breez Liquid SDK is not installed. "
"Ask admin to run `poetry add -E breez` to install it." "Ask admin to run `uv sync --extra breez` to install it."
) )
else: else:
@ -16,7 +16,6 @@ else:
from asyncio import Queue from asyncio import Queue
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from pathlib import Path from pathlib import Path
from typing import Optional
from bolt11 import decode as bolt11_decode from bolt11 import decode as bolt11_decode
from breez_sdk_liquid import ( from breez_sdk_liquid import (
@ -128,9 +127,9 @@ else:
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**_, **_,
) -> InvoiceResponse: ) -> InvoiceResponse:
try: try:

View file

@ -2,7 +2,6 @@ import asyncio
import hashlib import hashlib
import json import json
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from typing import Optional
from loguru import logger from loguru import logger
from websocket import create_connection from websocket import create_connection
@ -53,9 +52,9 @@ class ClicheWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**_, **_,
) -> InvoiceResponse: ) -> InvoiceResponse:
if unhashed_description or description_hash: if unhashed_description or description_hash:

View file

@ -5,7 +5,6 @@ import os
import ssl import ssl
import uuid import uuid
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from typing import Optional
from urllib.parse import urlparse from urllib.parse import urlparse
import httpx import httpx
@ -174,9 +173,9 @@ class CLNRestWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**kwargs, **kwargs,
) -> InvoiceResponse: ) -> InvoiceResponse:

View file

@ -1,7 +1,7 @@
import asyncio import asyncio
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from secrets import token_urlsafe from secrets import token_urlsafe
from typing import Any, Optional from typing import Any
from bolt11.decode import decode as bolt11_decode from bolt11.decode import decode as bolt11_decode
from bolt11.exceptions import Bolt11Exception from bolt11.exceptions import Bolt11Exception
@ -92,9 +92,9 @@ class CoreLightningWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**kwargs, **kwargs,
) -> InvoiceResponse: ) -> InvoiceResponse:
label = kwargs.get("label", f"lbl{token_urlsafe(16)}") label = kwargs.get("label", f"lbl{token_urlsafe(16)}")

View file

@ -2,7 +2,6 @@ import asyncio
import json import json
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from secrets import token_urlsafe from secrets import token_urlsafe
from typing import Optional
import httpx import httpx
from bolt11 import Bolt11Exception from bolt11 import Bolt11Exception
@ -106,9 +105,9 @@ class CoreLightningRestWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**kwargs, **kwargs,
) -> InvoiceResponse: ) -> InvoiceResponse:
label = kwargs.get("label", f"lbl{token_urlsafe(16)}") label = kwargs.get("label", f"lbl{token_urlsafe(16)}")

View file

@ -5,7 +5,7 @@ import json
import urllib.parse import urllib.parse
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from decimal import Decimal from decimal import Decimal
from typing import Any, Optional from typing import Any
import httpx import httpx
from loguru import logger from loguru import logger
@ -84,9 +84,9 @@ class EclairWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**kwargs, **kwargs,
) -> InvoiceResponse: ) -> InvoiceResponse:
data: dict[str, Any] = { data: dict[str, Any] = {

View file

@ -3,7 +3,6 @@ from collections.abc import AsyncGenerator
from datetime import datetime from datetime import datetime
from hashlib import sha256 from hashlib import sha256
from os import urandom from os import urandom
from typing import Optional
from bolt11 import ( from bolt11 import (
Bolt11, Bolt11,
@ -53,11 +52,11 @@ class FakeWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
expiry: Optional[int] = None, expiry: int | None = None,
payment_secret: Optional[bytes] = None, payment_secret: bytes | None = None,
**_, **_,
) -> InvoiceResponse: ) -> InvoiceResponse:
tags = Tags() tags = Tags()

View file

@ -1,7 +1,6 @@
import asyncio import asyncio
import json import json
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from typing import Optional
import httpx import httpx
from loguru import logger from loguru import logger
@ -71,9 +70,9 @@ class LNbitsWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**kwargs, **kwargs,
) -> InvoiceResponse: ) -> InvoiceResponse:
data: dict = {"out": False, "amount": amount, "memo": memo or ""} data: dict = {"out": False, "amount": amount, "memo": memo or ""}

View file

@ -3,7 +3,6 @@ import base64
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from hashlib import sha256 from hashlib import sha256
from os import environ from os import environ
from typing import Optional
import grpc import grpc
from loguru import logger from loguru import logger
@ -123,9 +122,9 @@ class LndWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**kwargs, **kwargs,
) -> InvoiceResponse: ) -> InvoiceResponse:
data: dict = { data: dict = {
@ -312,9 +311,9 @@ class LndWallet(Wallet):
self, self,
amount: int, amount: int,
payment_hash: str, payment_hash: str,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**kwargs, **kwargs,
) -> InvoiceResponse: ) -> InvoiceResponse:
data: dict = { data: dict = {

View file

@ -3,7 +3,7 @@ import base64
import hashlib import hashlib
import json import json
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from typing import Any, Optional from typing import Any
import httpx import httpx
from loguru import logger from loguru import logger
@ -103,9 +103,9 @@ class LndRestWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**kwargs, **kwargs,
) -> InvoiceResponse: ) -> InvoiceResponse:
_data: dict = { _data: dict = {
@ -327,9 +327,9 @@ class LndRestWallet(Wallet):
self, self,
amount: int, amount: int,
payment_hash: str, payment_hash: str,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**_, **_,
) -> InvoiceResponse: ) -> InvoiceResponse:
data: dict = { data: dict = {

View file

@ -1,7 +1,6 @@
import asyncio import asyncio
import hashlib import hashlib
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from typing import Optional
import httpx import httpx
from loguru import logger from loguru import logger
@ -75,9 +74,9 @@ class LNPayWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**_, **_,
) -> InvoiceResponse: ) -> InvoiceResponse:
data: dict = {"num_satoshis": f"{amount}"} data: dict = {"num_satoshis": f"{amount}"}

View file

@ -3,7 +3,6 @@ import hashlib
import json import json
import time import time
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from typing import Optional
import httpx import httpx
from loguru import logger from loguru import logger
@ -69,9 +68,9 @@ class LnTipsWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**_, **_,
) -> InvoiceResponse: ) -> InvoiceResponse:
data: dict = {"amount": amount, "description_hash": "", "memo": memo or ""} data: dict = {"amount": amount, "description_hash": "", "memo": memo or ""}

View file

@ -1,6 +1,5 @@
import base64 import base64
from getpass import getpass from getpass import getpass
from typing import Optional
from loguru import logger from loguru import logger
@ -8,8 +7,8 @@ from lnbits.utils.crypto import AESCipher
def load_macaroon( def load_macaroon(
macaroon: Optional[str] = None, macaroon: str | None = None,
encrypted_macaroon: Optional[str] = None, encrypted_macaroon: str | None = None,
) -> str: ) -> str:
"""Returns hex version of a macaroon encoded in base64 or the file path.""" """Returns hex version of a macaroon encoded in base64 or the file path."""

View file

@ -4,7 +4,7 @@ import json
import random import random
import time import time
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from typing import Optional, Union, cast from typing import cast
from urllib.parse import parse_qs, unquote, urlparse from urllib.parse import parse_qs, unquote, urlparse
import secp256k1 import secp256k1
@ -130,9 +130,9 @@ class NWCWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**_, **_,
) -> InvoiceResponse: ) -> InvoiceResponse:
desc = "" desc = ""
@ -360,7 +360,7 @@ class NWCConnection:
""" """
return self.shutdown or not settings.lnbits_running return self.shutdown or not settings.lnbits_running
async def _send(self, data: list[Union[str, dict]]): async def _send(self, data: list[str | dict]):
""" """
Sends data to the NWC relay. Sends data to the NWC relay.
@ -396,7 +396,7 @@ class NWCConnection:
async def _close_subscription_by_subid( async def _close_subscription_by_subid(
self, sub_id: str, send_event: bool = True self, sub_id: str, send_event: bool = True
) -> Optional[dict]: ) -> dict | None:
""" """
Closes a subscription by its sub_id. Closes a subscription by its sub_id.
@ -427,7 +427,7 @@ class NWCConnection:
async def _close_subscription_by_eventid( async def _close_subscription_by_eventid(
self, event_id, send_event=True self, event_id, send_event=True
) -> Optional[dict]: ) -> dict | None:
""" """
Closes a subscription associated to an event_id. Closes a subscription associated to an event_id.
@ -514,7 +514,7 @@ class NWCConnection:
if subscription: # Check if the subscription exists first if subscription: # Check if the subscription exists first
subscription["future"].set_exception(Exception(info)) subscription["future"].set_exception(Exception(info))
async def _on_event_message(self, msg: list[Union[str, dict]]): # noqa: C901 async def _on_event_message(self, msg: list[str | dict]): # noqa: C901
""" """
Handles EVENT messages from the relay. Handles EVENT messages from the relay.
""" """

View file

@ -1,6 +1,5 @@
import asyncio import asyncio
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from typing import Optional
import httpx import httpx
from loguru import logger from loguru import logger
@ -71,9 +70,9 @@ class OpenNodeWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**_, **_,
) -> InvoiceResponse: ) -> InvoiceResponse:
if description_hash or unhashed_description: if description_hash or unhashed_description:

View file

@ -4,7 +4,7 @@ import hashlib
import json import json
import urllib.parse import urllib.parse
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from typing import Any, Optional from typing import Any
import httpx import httpx
from httpx import RequestError, TimeoutException from httpx import RequestError, TimeoutException
@ -96,9 +96,9 @@ class PhoenixdWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**kwargs, **kwargs,
) -> InvoiceResponse: ) -> InvoiceResponse:

View file

@ -3,7 +3,6 @@ import hashlib
import json import json
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from secrets import token_urlsafe from secrets import token_urlsafe
from typing import Optional
import httpx import httpx
from loguru import logger from loguru import logger
@ -111,9 +110,9 @@ class SparkWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**kwargs, **kwargs,
) -> InvoiceResponse: ) -> InvoiceResponse:
label = f"lbs{token_urlsafe(16)}" label = f"lbs{token_urlsafe(16)}"

View file

@ -2,7 +2,7 @@ import asyncio
import time import time
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from decimal import Decimal from decimal import Decimal
from typing import Any, Optional from typing import Any
import httpx import httpx
from loguru import logger from loguru import logger
@ -116,7 +116,7 @@ class StrikeWallet(Wallet):
self.failed_payments: dict[str, str] = {} self.failed_payments: dict[str, str] = {}
# balance cache # balance cache
self._cached_balance: Optional[int] = None self._cached_balance: int | None = None
self._cached_balance_ts: float = 0.0 self._cached_balance_ts: float = 0.0
self._cache_ttl = 30 # seconds self._cache_ttl = 30 # seconds
@ -199,8 +199,8 @@ class StrikeWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: bytes | None = None, # Add this parameter unhashed_description: bytes | None = None, # Add this parameter
**kwargs, **kwargs,
) -> InvoiceResponse: ) -> InvoiceResponse:
@ -424,10 +424,10 @@ class StrikeWallet(Wallet):
async def get_invoices( async def get_invoices(
self, self,
filters: Optional[str] = None, filters: str | None = None,
orderby: Optional[str] = None, orderby: str | None = None,
skip: Optional[int] = None, skip: int | None = None,
top: Optional[int] = None, top: int | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
try: try:
params: dict[str, Any] = {} params: dict[str, Any] = {}
@ -450,7 +450,7 @@ class StrikeWallet(Wallet):
async def _get_payment_status_by_quote_id( async def _get_payment_status_by_quote_id(
self, checking_id: str, quote_id: str self, checking_id: str, quote_id: str
) -> Optional[PaymentStatus]: ) -> PaymentStatus | None:
resp = await self._get(f"/payment-quotes/{quote_id}") resp = await self._get(f"/payment-quotes/{quote_id}")
resp.raise_for_status() resp.raise_for_status()

View file

@ -1,7 +1,6 @@
import asyncio import asyncio
import hashlib import hashlib
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from typing import Optional
import httpx import httpx
from bolt11 import decode as bolt11_decode from bolt11 import decode as bolt11_decode
@ -60,9 +59,9 @@ class ZBDWallet(Wallet):
async def create_invoice( async def create_invoice(
self, self,
amount: int, amount: int,
memo: Optional[str] = None, memo: str | None = None,
description_hash: Optional[bytes] = None, description_hash: bytes | None = None,
unhashed_description: Optional[bytes] = None, unhashed_description: bytes | None = None,
**_, **_,
) -> InvoiceResponse: ) -> InvoiceResponse:
# https://api.zebedee.io/v0/charges # https://api.zebedee.io/v0/charges

1011
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,78 +1,98 @@
[tool.poetry] [project]
name = "lnbits" name = "lnbits"
version = "1.3.0-rc4" version = "1.3.0-rc4"
requires-python = ">=3.10,<3.13"
description = "LNbits, free and open-source Lightning wallet and accounts system." description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = ["Alan Bits <alan@lnbits.com>"] authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
urls = { Homepage = "https://lnbits.com", Repository = "https://github.com/lnbits/lnbits" }
readme = "README.md" readme = "README.md"
repository = "https://github.com/lnbits/lnbits" dependencies = [
homepage = "https://lnbits.com" "bech32==1.2.0",
"click==8.2.1",
"ecdsa==0.19.1",
"fastapi==0.116.1",
"starlette==0.47.1",
"httpx==0.27.0",
"jinja2==3.1.6",
"lnurl==0.7.3",
"pydantic==1.10.22",
"pyqrcode==1.2.1",
"shortuuid==1.0.13",
"sse-starlette==2.3.6",
"typing-extensions==4.14.0",
"uvicorn==0.34.3",
"sqlalchemy==1.4.54",
"aiosqlite==0.21.0",
"asyncpg==0.30.0",
"uvloop==0.21.0",
"websockets==15.0.1",
"loguru==0.7.3",
"grpcio==1.69.0",
"protobuf==5.29.5",
"pyln-client==25.5",
"pywebpush==2.0.3",
"slowapi==0.1.9",
"websocket-client==1.8.0",
"pycryptodomex==3.23.0",
"packaging==25.0",
"bolt11==2.1.1",
"pyjwt==2.10.1",
"itsdangerous==2.2.0",
"fastapi-sso==0.18.0",
# needed for boltz, lnurldevice, watchonly extensions
"embit==0.8.0",
# needed for lnurlp, nostrclient, nostrmarket
"secp256k1==0.14.0",
# keep for backwards compatibility with lnurlp
"environs==14.2.0",
# needed for scheduler extension
"python-crontab==3.2.0",
"pynostr==0.6.2",
"python-multipart==0.0.20",
"filetype==1.2.0",
"nostr-sdk==0.42.1",
"bcrypt==4.3.0",
"jsonpath-ng==1.7.0",
]
[project.scripts]
lnbits = "lnbits.server:main"
lnbits-cli = "lnbits.commands:main"
[project.optional-dependencies]
breez = ["breez-sdk==0.8.0", "breez-sdk-liquid==0.9.1"]
liquid = ["wallycore==1.4.0"]
migration = ["psycopg2-binary==2.9.10"]
[tool.uv]
dev-dependencies = [
"black>=25.1.0,<26.0.0",
"mypy>=1.11.2,<2.0.0",
"types-protobuf>=6.30.2.20250516,<7.0.0",
"pre-commit>=4.2.0,<5.0.0",
"openapi-spec-validator>=0.7.1,<1.0.0",
"ruff>=0.12.0,<1.0.0",
"types-passlib>=1.7.7.20240327,<2.0.0",
"openai>=1.39.0,<2.0.0",
"json5>=0.12.0,<1.0.0",
"asgi-lifespan>=2.1.0,<3.0.0",
"anyio>=4.7.0,<5.0.0",
"pytest>=8.3.4,<9.0.0",
"pytest-cov>=6.0.0,<7.0.0",
"pytest-md>=0.2.0,<0.3.0",
"pytest-httpserver>=1.1.0,<2.0.0",
"pytest-mock>=3.14.0,<4.0.0",
"types-mock>=5.1.0.20240425,<6.0.0",
"mock>=5.1.0,<6.0.0",
"grpcio-tools>=1.69.0,<2.0.0"
]
[tool.poetry]
packages = [ packages = [
{include = "lnbits"}, {include = "lnbits"},
{include = "lnbits/py.typed"}, {include = "lnbits/py.typed"},
] ]
[tool.poetry.dependencies]
python = "~3.12 | ~3.11 | ~3.10"
bech32 = "1.2.0"
click = "8.2.1"
ecdsa = "0.19.1"
fastapi = "0.116.1"
starlette = "0.47.1"
httpx = "0.27.0"
jinja2 = "3.1.6"
lnurl = "0.7.3"
pydantic = "1.10.22"
pyqrcode = "1.2.1"
shortuuid = "1.0.13"
sse-starlette = "2.3.6"
typing-extensions = "4.14.0"
uvicorn = "0.34.3"
sqlalchemy = "1.4.54"
aiosqlite = "0.21.0"
asyncpg = "0.30.0"
uvloop = "0.21.0"
websockets = "15.0.1"
loguru = "0.7.3"
grpcio = "1.69.0"
protobuf = "5.29.5"
pyln-client = "25.5"
pywebpush = "2.0.3"
slowapi = "0.1.9"
websocket-client = "1.8.0"
pycryptodomex = "3.23.0"
packaging = "25.0"
bolt11 = "2.1.1"
pyjwt = "2.10.1"
itsdangerous = "2.2.0"
fastapi-sso = "0.18.0"
# needed for boltz, lnurldevice, watchonly extensions
embit = "0.8.0"
# needed for cashu, lnurlp, nostrclient, nostrmarket, nostrrelay extensions
secp256k1 = "0.14.0"
# keep for backwards compatibility with lnurlp and cashu
environs = "14.2.0"
# needed for scheduler extension
python-crontab = "3.2.0"
# needed for liquid support boltz
wallycore = {version = "1.4.0", optional = true}
# needed for breez funding source
breez-sdk = {version = "0.8.0", optional = true}
breez-sdk-liquid = {version = "0.9.1", optional = true}
# needed for migration tests
psycopg2-binary = {version = "2.9.10", optional = true}
jsonpath-ng = "^1.7.0"
pynostr = "^0.6.2"
python-multipart = "^0.0.20"
filetype = "^1.2.0"
nostr-sdk = "^0.42.1"
bcrypt = "^4.3.0"
[tool.poetry.extras]
breez = ["breez-sdk", "breez-sdk-liquid"]
liquid = ["wallycore"]
migration = ["psycopg2-binary"]
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
black = "^25.1.0" black = "^25.1.0"
mypy = "^1.17.1" mypy = "^1.17.1"
@ -94,14 +114,6 @@ types-mock = "^5.1.0.20240425"
mock = "^5.1.0" mock = "^5.1.0"
grpcio-tools = "^1.69.0" grpcio-tools = "^1.69.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
lnbits = "lnbits.server:main"
lnbits-cli = "lnbits.commands:main"
[tool.pyright] [tool.pyright]
include = [ include = [
"lnbits", "lnbits",
@ -255,3 +267,10 @@ extend-immutable-calls = [
"fastapi.Body", "fastapi.Body",
"lnbits.decorators.parse_filters" "lnbits.decorators.parse_filters"
] ]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["lnbits"]

View file

@ -1,6 +1,5 @@
import random import random
import string import string
from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
@ -14,13 +13,13 @@ class FakeError(Exception):
class DbTestModel(BaseModel): class DbTestModel(BaseModel):
id: int id: int
name: str name: str
value: Optional[str] = None value: str | None = None
class DbTestModel2(BaseModel): class DbTestModel2(BaseModel):
id: int id: int
label: str label: str
description: Optional[str] = None description: str | None = None
child: DbTestModel child: DbTestModel
child_list: list[DbTestModel] child_list: list[DbTestModel]

View file

@ -1,30 +1,28 @@
from typing import Optional, Union
from pydantic import BaseModel from pydantic import BaseModel
class FundingSourceConfig(BaseModel): class FundingSourceConfig(BaseModel):
name: str name: str
skip: Optional[bool] skip: bool | None
wallet_class: str wallet_class: str
settings: dict settings: dict
mock_settings: Optional[dict] mock_settings: dict | None
class FunctionMock(BaseModel): class FunctionMock(BaseModel):
uri: Optional[str] uri: str | None
query_params: Optional[dict] query_params: dict | None
headers: Optional[dict] headers: dict | None
method: Optional[str] method: str | None
class TestMock(BaseModel): class TestMock(BaseModel):
skip: Optional[bool] skip: bool | None
description: Optional[str] description: str | None
request_type: Optional[str] request_type: str | None
request_body: Optional[dict] request_body: dict | None
response_type: str response_type: str
response: Union[str, dict, list] response: str | dict | list
class Mock(FunctionMock, TestMock): class Mock(FunctionMock, TestMock):
@ -62,13 +60,13 @@ class FunctionData(BaseModel):
class WalletTest(BaseModel): class WalletTest(BaseModel):
skip: Optional[bool] skip: bool | None
function: str function: str
description: str description: str
funding_source: FundingSourceConfig funding_source: FundingSourceConfig
call_params: Optional[dict] = {} call_params: dict | None = {}
expect: Optional[dict] expect: dict | None
expect_error: Optional[dict] expect_error: dict | None
mocks: list[Mock] = [] mocks: list[Mock] = []
@staticmethod @staticmethod

View file

@ -1,5 +1,4 @@
import json import json
from typing import Union
from urllib.parse import urlencode from urllib.parse import urlencode
import pytest import pytest
@ -59,7 +58,7 @@ async def test_rest_wallet(httpserver: HTTPServer, test_data: WalletTest):
def _apply_mock(httpserver: HTTPServer, mock: Mock): def _apply_mock(httpserver: HTTPServer, mock: Mock):
request_data: dict[str, Union[str, dict, list]] = {} request_data: dict[str, str | dict | list] = {}
request_type = getattr(mock.dict(), "request_type", None) request_type = getattr(mock.dict(), "request_type", None)
# request_type = mock.request_type <--- this des not work for whatever reason!!! # request_type = mock.request_type <--- this des not work for whatever reason!!!
@ -81,7 +80,7 @@ def _apply_mock(httpserver: HTTPServer, mock: Mock):
**request_data, # type: ignore **request_data, # type: ignore
) )
server_response: Union[str, dict, list, Response] = mock.response server_response: str | dict | list | Response = mock.response
response_type = mock.response_type response_type = mock.response_type
if response_type == "response": if response_type == "response":
assert isinstance(server_response, dict), "server response must be JSON" assert isinstance(server_response, dict), "server response must be JSON"

View file

@ -1,5 +1,4 @@
import importlib import importlib
from typing import Optional
from unittest.mock import AsyncMock, Mock from unittest.mock import AsyncMock, Mock
import pytest import pytest
@ -167,7 +166,7 @@ def _mock_field(field):
return response return response
def _eval_dict(data: Optional[dict]) -> Optional[dict]: def _eval_dict(data: dict | None) -> dict | None:
fn_prefix = "__eval__:" fn_prefix = "__eval__:"
if not data: if not data:
return data return data
@ -190,7 +189,7 @@ def _eval_dict(data: Optional[dict]) -> Optional[dict]:
return d return d
def _dict_to_object(data: Optional[dict]) -> Optional[DataObject]: def _dict_to_object(data: dict | None) -> DataObject | None:
if not data: if not data:
return None return None
# if isinstance(data, list): # if isinstance(data, list):
@ -213,7 +212,7 @@ def _data_mock(data: dict) -> Mock:
return Mock(return_value=_dict_to_object(data)) return Mock(return_value=_dict_to_object(data))
def _raise(error: Optional[dict]): def _raise(error: dict | None):
if not error: if not error:
return Exception() return Exception()
data = error["data"] if "data" in error else None data = error["data"] if "data" in error else None

View file

@ -7,7 +7,6 @@ import argparse
import os import os
import sqlite3 import sqlite3
import sys import sys
from typing import Optional
from lnbits.settings import settings from lnbits.settings import settings
@ -108,7 +107,7 @@ def insert_to_pg(query, data):
connection.close() connection.close()
def migrate_core(file: str, exclude_tables: Optional[list[str]] = None): def migrate_core(file: str, exclude_tables: list[str] | None = None):
if exclude_tables is None: if exclude_tables is None:
exclude_tables = [] exclude_tables = []
print(f"Migrating core: {file}") print(f"Migrating core: {file}")
@ -124,7 +123,7 @@ def migrate_ext(file: str):
print(f"✅ Migrated ext: {schema}") print(f"✅ Migrated ext: {schema}")
def migrate_db(file: str, schema: str, exclude_tables: Optional[list[str]] = None): def migrate_db(file: str, schema: str, exclude_tables: list[str] | None = None):
# first we check if this file exists: # first we check if this file exists:
if exclude_tables is None: if exclude_tables is None:
exclude_tables = [] exclude_tables = []

2916
uv.lock generated Normal file

File diff suppressed because it is too large Load diff