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
with:
python-version: ${{ inputs.python-version }}
# cache poetry install via pip
cache: "pip"
- name: Set up Poetry
uses: abatilo/actions-poetry@v2
- 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
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
path: ./.venv
key: venv-${{ hashFiles('poetry.lock') }}
enable-cache: true
python-version: ${{ inputs.python-version }}
- name: Install the project dependencies
shell: bash
run: |
poetry env use python${{ inputs.python-version }}
poetry install --all-extras
run: uv sync --locked --all-extras --dev
- name: Use Node.js ${{ inputs.node-version }}
if: ${{ (inputs.npm == 'true') }}

View file

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

View file

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

View file

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

View file

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

View file

@ -2,26 +2,20 @@ FROM python:3.12-slim-bookworm AS builder
RUN apt-get clean
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"
WORKDIR /app
# Only copy the files required to install the dependencies
COPY pyproject.toml poetry.lock ./
COPY pyproject.toml uv.lock ./
RUN touch README.md
RUN mkdir data
ENV POETRY_NO_INTERACTION=1 \
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}
RUN uv sync --all-extras
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 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 POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_IN_PROJECT=1 \
POETRY_VIRTUALENVS_CREATE=1 \
VIRTUAL_ENV=/app/.venv \
PATH="/app/.venv/bin:$PATH"
WORKDIR /app
COPY . .
COPY --from=builder /app/.venv .venv
ARG POETRY_INSTALL_ARGS="--only main"
RUN poetry install ${POETRY_INSTALL_ARGS}
RUN uv sync --all-extras
ENV LNBITS_PORT="5000"
ENV LNBITS_HOST="0.0.0.0"
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
# Reinstall dependencies just in case (needed for CMD usage)
ARG POETRY_INSTALL_ARGS="--only main"
RUN poetry install ${POETRY_INSTALL_ARGS}
RUN uv sync --all-extras
# LNbits + boltzd configuration
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
prettier:
poetry run ./node_modules/.bin/prettier --write .
uv run ./node_modules/.bin/prettier --write .
pyright:
poetry run ./node_modules/.bin/pyright
uv run ./node_modules/.bin/pyright
mypy:
poetry run mypy
uv run mypy
black:
poetry run black .
uv run black .
ruff:
poetry run ruff check . --fix
uv run ruff check . --fix
checkruff:
poetry run ruff check .
uv run ruff check .
checkprettier:
poetry run ./node_modules/.bin/prettier --check .
uv run ./node_modules/.bin/prettier --check .
checkblack:
poetry run black --check .
uv run black --check .
checkeditorconfig:
editorconfig-checker
dev:
poetry run lnbits --reload
uv run lnbits --reload
docker:
docker build -t lnbits/lnbits .
@ -46,27 +46,27 @@ test-wallets:
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
PYTHONUNBUFFERED=1 \
DEBUG=true \
poetry run pytest tests/wallets
uv run pytest tests/wallets
test-unit:
LNBITS_DATA_FOLDER="./tests/data" \
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
PYTHONUNBUFFERED=1 \
DEBUG=true \
poetry run pytest tests/unit
uv run pytest tests/unit
test-api:
LNBITS_DATA_FOLDER="./tests/data" \
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
PYTHONUNBUFFERED=1 \
DEBUG=true \
poetry run pytest tests/api
uv run pytest tests/api
test-regtest:
LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \
DEBUG=true \
poetry run pytest tests/regtest
uv run pytest tests/regtest
test-migration:
LNBITS_ADMIN_UI=True \
@ -74,18 +74,18 @@ test-migration:
HOST=0.0.0.0 \
PORT=5002 \
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 \
PORT=5002 \
LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \
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_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \
poetry run python tools/conv.py
uv run python tools/conv.py
migration:
poetry run python tools/conv.py
uv run python tools/conv.py
openapi:
LNBITS_ADMIN_UI=False \
@ -94,9 +94,9 @@ openapi:
PYTHONUNBUFFERED=1 \
HOST=0.0.0.0 \
PORT=5003 \
poetry run lnbits &
uv run lnbits &
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
bak:
@ -109,7 +109,7 @@ sass:
bundle:
npm install
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:
cp lnbits/static/bundle.min.js lnbits/static/bundle.min.js.old
@ -126,8 +126,8 @@ checkbundle:
install-pre-commit-hook:
@echo "Installing pre-commit hook to git"
@echo "Uninstall the hook with poetry run pre-commit uninstall"
poetry run pre-commit install
@echo "Uninstall the hook with uv run pre-commit uninstall"
uv run pre-commit install
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
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
Follow the [Basic installation: Option 1 (recommended): poetry](https://docs.lnbits.org/guide/installation.html#option-1-recommended-poetry)
guide to install poetry and other dependencies.
Follow the [Option 2 (recommended): UV](https://docs.lnbits.org/guide/installation.html)
guide to install uv and other dependencies.
Then you can start LNbits uvicorn server with:
```bash
poetry run lnbits
uv run lnbits
```
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
make dev
# or
poetry run lnbits --reload
uv run lnbits --reload
```
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:
```bash
poetry install
uv sync --all-extras --dev
npm i
```
@ -69,7 +69,7 @@ make format
Run mypy checks:
```bash
poetry run mypy
make mypy
```
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.
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`:
```sh
$ poetry add <package>
```
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`:
**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

View file

@ -53,7 +53,7 @@ $ sudo nano .env
Now start LNbits once in the terminal window
```
$ poetry run lnbits
$ uv run lnbits
```
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.
## 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
## 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
@ -39,11 +39,19 @@ It is recommended to use the latest version of Poetry. Make sure you have Python
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
# 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
@ -51,9 +59,13 @@ curl -sSL https://install.python-poetry.org | python3 - && export PATH="$HOME/.l
```sh
git clone https://github.com/lnbits/lnbits.git
cd lnbits
poetry env use 3.12
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
# Optional: to set funding source amongst other options via the env `nano .env`
```
@ -61,8 +73,11 @@ cp .env.example .env
#### Running the server
```sh
poetry run lnbits
# To change port/host pass 'poetry run lnbits --port 9000 --host 0.0.0.0'
uv run lnbits
# 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
# Note that you have to add the line DEBUG=true in your .env file, too.
```
@ -71,7 +86,7 @@ poetry run lnbits
```sh
# 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
@ -85,15 +100,18 @@ cd lnbits
git pull --rebase
# Check your poetry version with
poetry env list
# poetry env list
# If version is less 3.12, update it by running
poetry env use python3.12
poetry env remove python3.9
poetry env list
# poetry env use python3.12
# poetry env remove python3.9
# poetry env list
# Run install and start LNbits with
poetry install --only main
poetry run lnbits
# poetry install --only main
# 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
```
@ -108,13 +126,13 @@ chmod +x lnbits.sh &&
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
```sh
# 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:
echo 'experimental-features = nix-command flakes' >> /etc/nix/nix.conf
@ -332,7 +350,7 @@ LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
# START LNbits
# STOP LNbits
poetry run python tools/conv.py
uv run python tools/conv.py
# or
make migration
```
@ -357,8 +375,8 @@ Description=LNbits
[Service]
# replace with the absolute path of your lnbits installation
WorkingDirectory=/home/lnbits/lnbits
# same here. run `which poetry` if you can't find the poetry binary
ExecStart=/home/lnbits/.local/bin/poetry run lnbits
# same here. run `which uv` if you can't find the poetry binary
ExecStart=/home/lnbits/.local/bin/uv run lnbits
# replace with the user that you're running lnbits on
User=lnbits
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
To encrypt your macaroon, run `poetry run lnbits-cli encrypt macaroon`.
To encrypt your macaroon, run `uv run lnbits-cli encrypt macaroon`.
### LNbits

149
flake.lock generated
View file

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

193
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 = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.05";
poetry2nix = {
url = "github:nix-community/poetry2nix";
inputs.nixpkgs.follows = "nixpkgs";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
flake-utils.url = "github:numtide/flake-utils";
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:
outputs = { self, nixpkgs, flake-utils, uv2nix, pyproject-nix, build-system-pkgs, ... }:
flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]
(system:
let
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
forSystems = systems: f:
nixpkgs.lib.genAttrs systems
(system: f system (import nixpkgs { inherit system; overlays = [ poetry2nix.overlays.default self.overlays.default ]; }));
forAllSystems = forSystems supportedSystems;
pkgs = import nixpkgs { inherit system; };
lib = pkgs.lib;
python = pkgs.python312;
# Read uv.lock / pyproject via uv2nix
workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
# Prefer wheels when available
uvLockedOverlay = workspace.mkPyprojectOverlay { sourcePreference = "wheel"; };
# Helper for extending lists safely (works if a is null)
plus = a: b: lib.unique (((if a == null then [] else a)) ++ b);
# Extra build inputs for troublesome sdists
myOverrides = (final: prev: {
# embit needs setuptools at build time
embit = prev.embit.overrideAttrs (old: {
nativeBuildInputs = plus (old.nativeBuildInputs or []) [ prev.setuptools ];
});
# http-ece (pywebpush dep) needs setuptools
"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
pythonSet =
(pkgs.callPackage pyproject-nix.build.packages { inherit python; })
.overrideScope (lib.composeManyExtensions [
build-system-pkgs.overlays.default
uvLockedOverlay
myOverrides
]);
projectName = "lnbits";
# 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
{
overlays = {
default = final: prev: {
${projectName} = self.packages.${prev.stdenv.hostPlatform.system}.${projectName};
};
};
packages = forAllSystems (system: pkgs: {
default = self.packages.${system}.${projectName};
${projectName} = pkgs.poetry2nix.mkPoetryApplication {
projectDir = ./.;
meta.rev = self.dirtyRev or self.rev;
meta.mainProgram = projectName;
overrides = pkgs.poetry2nix.overrides.withDefaults (final: prev: {
coincurve = prev.coincurve.override { preferWheel = true; };
protobuf = prev.protobuf.override { preferWheel = true; };
ruff = prev.ruff.override { preferWheel = true; };
wallycore = prev.wallycore.override { preferWheel = true; };
tlv8 = prev.tlv8.overrideAttrs (old: {
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
prev.setuptools
# 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
];
});
pynostr = prev.pynostr.overrideAttrs (old: {
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
prev.setuptools-scm
];
});
});
};
});
nixosModules = {
default = { pkgs, lib, config, ... }: {
imports = [ "${./nix/modules/${projectName}-service.nix}" ];
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 = forAllSystems (system: pkgs:
let
vmTests = import ./nix/tests {
makeTest = (import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }).makeTest;
inherit inputs pkgs;
};
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...
}
);
};
checks = { };
});
}

View file

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

View file

@ -5,9 +5,9 @@ import os
import shutil
import sys
import time
from collections.abc import Callable
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Callable, Optional
from fastapi import FastAPI
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
include_deactivated: Optional[bool] = True,
include_deactivated: bool | None = True,
) -> list[InstallableExtension]:
"""
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 getpass import getpass
from pathlib import Path
from typing import Optional
from uuid import uuid4
import click
@ -96,7 +95,7 @@ def decrypt():
"""
def get_super_user() -> Optional[str]:
def get_super_user() -> str | None:
"""Get the superuser"""
superuser_file = Path(settings.lnbits_data_folder, ".super_user")
if not superuser_file.exists() or not superuser_file.is_file():
@ -155,7 +154,7 @@ async def db_versions():
@db.command("cleanup-wallets")
@click.argument("days", type=int, required=False)
@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"""
async with core_db.connect() as conn:
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.")
@coro
async def check_invalid_payments(
days: Optional[int] = None,
limit: Optional[int] = None,
wallet: Optional[str] = None,
verbose: Optional[bool] = False,
days: int | None = None,
limit: int | None = None,
wallet: str | None = None,
verbose: bool | None = False,
):
"""Check payments that are settled in the DB but pending on the Funding Source"""
await check_admin_settings()
@ -303,7 +302,7 @@ async def create_user(username: str, password: str):
@users.command("cleanup-accounts")
@click.argument("days", type=int, required=False)
@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"""
async with core_db.connect() as conn:
delta = days or settings.cleanup_wallets_days
@ -353,12 +352,12 @@ async def extensions_list():
)
@coro
async def extensions_update( # noqa: C901
extension: Optional[str] = None,
all_extensions: Optional[bool] = False,
repo_index: Optional[str] = None,
source_repo: Optional[str] = None,
url: Optional[str] = None,
admin_user: Optional[str] = None,
extension: str | None = None,
all_extensions: bool | None = False,
repo_index: str | None = None,
source_repo: str | None = None,
url: str | None = None,
admin_user: str | None = None,
):
"""
Update extension to the latest version.
@ -443,10 +442,10 @@ async def extensions_update( # noqa: C901
@coro
async def extensions_install(
extension: str,
repo_index: Optional[str] = None,
source_repo: Optional[str] = None,
url: Optional[str] = None,
admin_user: Optional[str] = None,
repo_index: str | None = None,
source_repo: str | None = None,
url: str | None = None,
admin_user: str | None = None,
):
"""Install a extension"""
click.echo(f"Installing {extension}... {repo_index}")
@ -473,7 +472,7 @@ async def extensions_install(
)
@coro
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"""
click.echo(f"Uninstalling '{extension}'...")
@ -562,10 +561,10 @@ if __name__ == "__main__":
async def install_extension(
extension: str,
repo_index: Optional[str] = None,
source_repo: Optional[str] = None,
url: Optional[str] = None,
admin_user: Optional[str] = None,
repo_index: str | None = None,
source_repo: str | None = None,
url: str | None = None,
admin_user: str | None = None,
) -> tuple[bool, str]:
try:
release = await _select_release(extension, repo_index, source_repo)
@ -591,10 +590,10 @@ async def install_extension(
async def update_extension(
extension: str,
repo_index: Optional[str] = None,
source_repo: Optional[str] = None,
url: Optional[str] = None,
admin_user: Optional[str] = None,
repo_index: str | None = None,
source_repo: str | None = None,
url: str | None = None,
admin_user: str | None = None,
) -> tuple[bool, str]:
try:
click.echo(f"Updating '{extension}' extension.")
@ -644,9 +643,9 @@ async def update_extension(
async def _select_release(
extension: str,
repo_index: Optional[str] = None,
source_repo: Optional[str] = None,
) -> Optional[ExtensionRelease]:
repo_index: str | None = None,
source_repo: str | None = None,
) -> ExtensionRelease | None:
all_releases = await InstallableExtension.get_extension_releases(extension)
if len(all_releases) == 0:
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(
data: CreateExtension, url: Optional[str], user_id: Optional[str] = None
data: CreateExtension, url: str | None, user_id: str | None = None
):
if url:
user_id = user_id or get_super_user()
@ -720,7 +719,7 @@ async def _call_install_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:
user_id = user_id or get_super_user()
@ -756,7 +755,7 @@ async def _can_run_operation(url) -> bool:
return True
async def _is_lnbits_started(url: Optional[str]):
async def _is_lnbits_started(url: str | None):
try:
url = url or f"http://{settings.host}:{settings.port}/api/v1/health"
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.models import AuditEntry, AuditFilters
from lnbits.core.models.audit import AuditCountStat
@ -8,14 +6,14 @@ from lnbits.db import Connection, Filters, Page
async def create_audit_entry(
entry: AuditEntry,
conn: Optional[Connection] = None,
conn: Connection | None = None,
) -> None:
await (conn or db).insert("audit", entry)
async def get_audit_entries(
filters: Optional[Filters[AuditFilters]] = None,
conn: Optional[Connection] = None,
filters: Filters[AuditFilters] | None = None,
conn: Connection | None = None,
) -> Page[AuditEntry]:
return await (conn or db).fetch_page(
"SELECT * from audit",
@ -27,7 +25,7 @@ async def get_audit_entries(
async def delete_expired_audit_entries(
conn: Optional[Connection] = None,
conn: Connection | None = None,
):
await (conn or db).execute(
# 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(
field: str,
filters: Optional[Filters[AuditFilters]] = None,
conn: Optional[Connection] = None,
filters: Filters[AuditFilters] | None = None,
conn: Connection | None = None,
) -> list[AuditCountStat]:
if field not in ["request_method", "component", "response_code"]:
return []
@ -67,8 +65,8 @@ async def get_count_stats(
async def get_long_duration_stats(
filters: Optional[Filters[AuditFilters]] = None,
conn: Optional[Connection] = None,
filters: Filters[AuditFilters] | None = None,
conn: Connection | None = None,
) -> list[AuditCountStat]:
if not filters:
filters = Filters()

View file

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

View file

@ -1,5 +1,5 @@
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.db import db
@ -23,7 +23,7 @@ def update_payment_extra():
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(
"SELECT * FROM apipayments WHERE 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(
checking_id_or_hash: str,
conn: Optional[Connection] = None,
incoming: Optional[bool] = False,
wallet_id: Optional[str] = None,
) -> Optional[Payment]:
conn: Connection | None = None,
incoming: bool | None = False,
wallet_id: str | None = None,
) -> Payment | None:
clause: str = "checking_id = :checking_id OR payment_hash = :hash"
values = {
"wallet_id": wallet_id,
@ -64,8 +64,8 @@ async def get_standalone_payment(
async def get_wallet_payment(
wallet_id: str, payment_hash: str, conn: Optional[Connection] = None
) -> Optional[Payment]:
wallet_id: str, payment_hash: str, conn: Connection | None = None
) -> Payment | None:
payment = await (conn or db).fetchone(
"""
SELECT *
@ -102,17 +102,17 @@ async def get_latest_payments_by_extension(
async def get_payments_paginated( # noqa: C901
*,
wallet_id: Optional[str] = None,
user_id: Optional[str] = None,
wallet_id: str | None = None,
user_id: str | None = None,
complete: bool = False,
pending: bool = False,
failed: bool = False,
outgoing: bool = False,
incoming: bool = False,
since: Optional[int] = None,
since: int | None = None,
exclude_uncheckable: bool = False,
filters: Optional[Filters[PaymentFilters]] = None,
conn: Optional[Connection] = None,
filters: Filters[PaymentFilters] | None = None,
conn: Connection | None = None,
) -> Page[Payment]:
"""
Filters payments to be returned by:
@ -176,17 +176,17 @@ async def get_payments_paginated( # noqa: C901
async def get_payments(
*,
wallet_id: Optional[str] = None,
wallet_id: str | None = None,
complete: bool = False,
pending: bool = False,
outgoing: bool = False,
incoming: bool = False,
since: Optional[int] = None,
since: int | None = None,
exclude_uncheckable: bool = False,
filters: Optional[Filters[PaymentFilters]] = None,
conn: Optional[Connection] = None,
limit: Optional[int] = None,
offset: Optional[int] = None,
filters: Filters[PaymentFilters] | None = None,
conn: Connection | None = None,
limit: int | None = None,
offset: int | None = None,
) -> list[Payment]:
"""
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(
conn: Optional[Connection] = None,
conn: Connection | None = None,
) -> None:
# first we delete all invoices older than one month
@ -259,7 +259,7 @@ async def create_payment(
checking_id: str,
data: CreatePayment,
status: PaymentState = PaymentState.PENDING,
conn: Optional[Connection] = None,
conn: Connection | None = None,
) -> Payment:
# we don't allow the creation of the same invoice twice
# 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(
checking_id: str, new_checking_id: str, conn: Optional[Connection] = None
checking_id: str, new_checking_id: str, conn: Connection | None = None
) -> None:
await (conn or db).execute(
"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(
payment: Payment,
new_checking_id: Optional[str] = None,
conn: Optional[Connection] = None,
new_checking_id: str | None = None,
conn: Connection | None = None,
) -> None:
await (conn or db).update(
"apipayments", payment, "WHERE checking_id = :checking_id"
@ -311,9 +311,9 @@ async def update_payment(
async def get_payments_history(
wallet_id: Optional[str] = None,
wallet_id: str | None = None,
group: DateTrunc = "day",
filters: Optional[Filters] = None,
filters: Filters | None = None,
) -> list[PaymentHistoryPoint]:
if not filters:
filters = Filters()
@ -376,9 +376,9 @@ async def get_payments_history(
async def get_payment_count_stats(
field: PaymentCountField,
filters: Optional[Filters[PaymentFilters]] = None,
user_id: Optional[str] = None,
conn: Optional[Connection] = None,
filters: Filters[PaymentFilters] | None = None,
user_id: str | None = None,
conn: Connection | None = None,
) -> list[PaymentCountStat]:
if not filters:
@ -409,9 +409,9 @@ async def get_payment_count_stats(
async def get_daily_stats(
filters: Optional[Filters[PaymentFilters]] = None,
user_id: Optional[str] = None,
conn: Optional[Connection] = None,
filters: Filters[PaymentFilters] | None = None,
user_id: str | None = None,
conn: Connection | None = None,
) -> tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
if not filters:
@ -459,9 +459,9 @@ async def get_daily_stats(
async def get_wallets_stats(
filters: Optional[Filters[PaymentFilters]] = None,
user_id: Optional[str] = None,
conn: Optional[Connection] = None,
filters: Filters[PaymentFilters] | None = None,
user_id: str | None = None,
conn: Connection | None = None,
) -> list[PaymentWalletStats]:
if not filters:
@ -508,7 +508,7 @@ async def get_wallets_stats(
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:
await (conn or db).execute(
"DELETE FROM apipayments WHERE checking_id = :checking_id AND wallet = :wallet",
@ -517,8 +517,8 @@ async def delete_wallet_payment(
async def check_internal(
payment_hash: str, conn: Optional[Connection] = None
) -> Optional[Payment]:
payment_hash: str, conn: Connection | None = None
) -> Payment | None:
"""
Returns the checking_id of the internal payment if it exists,
otherwise None
@ -534,7 +534,7 @@ async def check_internal(
async def is_internal_status_success(
payment_hash: str, conn: Optional[Connection] = None
payment_hash: str, conn: Connection | None = None
) -> bool:
"""
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(
user_id: str, conn: Optional[Connection] = None
user_id: str, conn: Connection | None = None
) -> str:
wallet_ids = await get_wallets_ids(user_id=user_id, conn=conn) or [
"no-wallets-for-user"

View file

@ -1,5 +1,5 @@
import json
from typing import Any, Optional
from typing import Any
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")
if data:
super_user = await get_settings_field("super_user")
@ -24,7 +24,7 @@ async def get_super_settings() -> Optional[SuperSettings]:
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()
if not sets:
return None
@ -41,7 +41,7 @@ async def get_admin_settings(is_super_user: bool = False) -> Optional[AdminSetti
async def update_admin_settings(
data: EditableSettings, tag: Optional[str] = "core"
data: EditableSettings, tag: str | None = "core"
) -> None:
editable_settings = await get_settings_by_tag("core") or {}
editable_settings.update(data.dict(exclude_unset=True))
@ -61,7 +61,7 @@ async def update_super_user(super_user: str) -> SuperSettings:
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(
"DELETE FROM system_settings WHERE 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(
id_: str, tag: Optional[str] = "core"
) -> Optional[SettingsField]:
id_: str, tag: str | None = "core"
) -> SettingsField | None:
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"])
async def set_settings_field(
id_: str, value: Optional[Any], tag: Optional[str] = "core"
):
async def set_settings_field(id_: str, value: Any | None, tag: str | None = "core"):
value = json.dumps(value) if value is not None else None
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(
"SELECT * FROM system_settings WHERE tag = :tag", {"tag": tag}
)

View file

@ -1,5 +1,3 @@
from typing import Optional
import shortuuid
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)
async def get_tinyurl(tinyurl_id: str) -> Optional[TinyURL]:
async def get_tinyurl(tinyurl_id: str) -> TinyURL | None:
return await db.fetchone(
"SELECT * FROM tiny_url WHERE id = :tinyurl",
{"tinyurl": tinyurl_id},

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
import asyncio
import importlib
from typing import Optional
from loguru import logger
@ -145,7 +144,7 @@ async def start_extension_background_work(ext_id: str) -> bool:
async def get_valid_extensions(
include_deactivated: Optional[bool] = True,
include_deactivated: bool | None = True,
) -> list[Extension]:
installed_extensions = await get_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(
ext_id: str, include_deactivated: Optional[bool] = True
) -> Optional[Extension]:
ext_id: str, include_deactivated: bool | None = True
) -> Extension | None:
ext = await get_installed_extension(ext_id)
if not ext:
return None

View file

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

View file

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

View file

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

View file

@ -1,5 +1,4 @@
from pathlib import Path
from typing import Optional
from uuid import uuid4
from loguru import logger
@ -37,7 +36,7 @@ from .settings import update_cached_settings
async def create_user_account(
account: Optional[Account] = None, wallet_name: Optional[str] = None
account: Account | None = None, wallet_name: str | None = None
) -> User:
if not settings.new_accounts_allowed:
raise ValueError("Account creation is disabled.")
@ -46,9 +45,9 @@ async def create_user_account(
async def create_user_account_no_ckeck(
account: Optional[Account] = None,
wallet_name: Optional[str] = None,
default_exts: Optional[list[str]] = None,
account: Account | None = None,
wallet_name: str | None = None,
default_exts: list[str] | None = None,
) -> User:
if account:
@ -165,12 +164,12 @@ async def check_admin_settings():
settings.first_install = True
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."
)
async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings:
async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
account = None
if super_user:
account = await get_account(super_user)

View file

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

View file

@ -5,7 +5,7 @@ from pathlib import Path
from shutil import make_archive, move
from subprocess import Popen
from tempfile import NamedTemporaryFile
from typing import IO, Optional
from typing import IO
from urllib.parse import urlparse
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(
user: User = Depends(check_admin),
) -> Optional[AdminSettings]:
) -> AdminSettings | None:
admin_settings = await get_admin_settings(user.super_user)
return admin_settings

View file

@ -1,9 +1,9 @@
import base64
import importlib
import json
from collections.abc import Callable
from http import HTTPStatus
from time import time
from typing import Callable, Optional
from uuid import uuid4
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")
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)
if not provider_sso:
@ -319,7 +319,7 @@ async def update_pubkey(
data: UpdateUserPubkey,
user: User = Depends(check_user_exists),
payload: AccessTokenPayload = Depends(access_token_payload),
) -> Optional[User]:
) -> User | None:
if data.user_id != user.id:
raise ValueError("Invalid user ID.")
@ -345,7 +345,7 @@ async def update_password(
data: UpdateUserPassword,
user: User = Depends(check_user_exists),
payload: AccessTokenPayload = Depends(access_token_payload),
) -> Optional[User]:
) -> User | None:
_validate_auth_timeout(payload.auth_time)
if data.user_id != user.id:
raise ValueError("Invalid user ID.")
@ -419,7 +419,7 @@ async def reset_password(data: ResetUserPassword) -> JSONResponse:
@auth_router.put("/update")
async def update(
data: UpdateUser, user: User = Depends(check_user_exists)
) -> Optional[User]:
) -> User | None:
if data.user_id != user.id:
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
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)
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
if not email or not is_valid_email_address(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(
username: Optional[str] = None,
user_id: Optional[str] = None,
email: Optional[str] = None,
username: str | None = None,
user_id: str | None = None,
email: str | None = None,
) -> JSONResponse:
payload = AccessTokenPayload(
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
def _new_sso(provider: str) -> Optional[SSOBase]:
def _new_sso(provider: str) -> SSOBase | None:
try:
if not settings.is_auth_method_allowed(AuthMethods(f"{provider}-auth")):
return None
@ -610,7 +610,7 @@ def _nostr_nip98_event(request: Request) -> 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:
raise ValueError("Tag 'method' is missing.")
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}'.")
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:
raise HTTPException(
HTTPStatus.BAD_REQUEST,

View file

@ -1,5 +1,5 @@
from http import HTTPStatus
from typing import Annotated, Optional, Union
from typing import Annotated
from urllib.parse import urlencode, urlparse
import httpx
@ -161,9 +161,9 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
)
async def wallet(
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),
wal: Optional[UUID4] = Query(None),
wal: UUID4 | None = Query(None),
):
if wal:
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:
res = await _handle(code)
if isinstance(res, (LnurlPayResponse, LnurlWithdrawResponse, LnurlAuthResponse)):
if isinstance(res, LnurlPayResponse | LnurlWithdrawResponse | LnurlAuthResponse):
check_callback_url(res.callback)
return res
@ -169,7 +169,7 @@ async def api_payment_pay_with_nfc(
except (LnurlResponseException, Exception) as exc:
logger.warning(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 res2

View file

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

View file

@ -1,6 +1,5 @@
from hashlib import sha256
from http import HTTPStatus
from typing import Optional
from fastapi import (
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
@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
# 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

View file

@ -2,7 +2,6 @@ import base64
import json
import time
from http import HTTPStatus
from typing import Optional
from uuid import uuid4
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")
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():
raise ValueError(f"Currency '{currency}' not allowed.")

View file

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

View file

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

View file

@ -1,7 +1,6 @@
import sys
import traceback
from http import HTTPStatus
from typing import Optional
from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
@ -30,7 +29,7 @@ class UnsupportedError(Exception):
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
# not fail proof, but everything else get's a JSON response

View file

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

View file

@ -3,7 +3,7 @@ import json
import re
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Optional
from typing import Any
from urllib import request
from urllib.parse import urlparse
@ -39,7 +39,7 @@ def urlsafe_short_hash() -> str:
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 ""
url_params = "?"
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}"
def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templates:
def template_renderer(additional_folders: list | None = None) -> Jinja2Templates:
folders = ["lnbits/templates", "lnbits/core/templates"]
if additional_folders:
additional_folders += [
@ -211,7 +211,7 @@ def is_valid_pubkey(pubkey: str) -> bool:
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
expire = datetime.now(timezone.utc) + timedelta(minutes=minutes)
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")
def encrypt_internal_message(
m: Optional[str] = None, urlsafe: bool = False
) -> Optional[str]:
def encrypt_internal_message(m: str | None = None, urlsafe: bool = False) -> str | None:
"""
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)
def decrypt_internal_message(
m: Optional[str] = None, urlsafe: bool = False
) -> Optional[str]:
def decrypt_internal_message(m: str | None = None, urlsafe: bool = False) -> str | None:
"""
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)
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:
# return shallow clone of the dict even if there are no filters
return {**data}
@ -270,7 +266,7 @@ def version_parse(v: str):
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:
if min_lnbits_version and (
version_parse(min_lnbits_version) > version_parse(settings.version)
@ -347,7 +343,7 @@ def path_segments(path: str) -> list[str]:
return segments[0:]
def normalize_path(path: Optional[str]) -> str:
def normalize_path(path: str | None) -> str:
path = path or ""
return "/" + "/".join(path_segments(path))

View file

@ -2,7 +2,7 @@ import asyncio
import json
from datetime import datetime, timezone
from http import HTTPStatus
from typing import Any, Optional, Union
from typing import Any
from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
@ -62,7 +62,7 @@ class InstalledExtensionMiddleware:
def _response_by_accepted_type(
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`
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:
start_time = datetime.now(timezone.utc)
request_details = await self._request_details(request)
response: Optional[Response] = None
response: Response | None = None
try:
response = await call_next(request)
@ -140,13 +140,13 @@ class AuditMiddleware(BaseHTTPMiddleware):
async def _log_audit(
self,
request: Request,
response: Optional[Response],
response: Response | None,
duration: float,
request_details: Optional[str],
request_details: str | None,
):
try:
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
if not settings.audit_http_request(http_method, path, response_code):
return None
@ -178,7 +178,7 @@ class AuditMiddleware(BaseHTTPMiddleware):
except Exception as 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():
return None

View file

@ -35,7 +35,7 @@ def main(
ssl_certfile: str,
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
Path(settings.lnbits_data_folder).mkdir(parents=True, exist_ok=True)

View file

@ -1,11 +1,7 @@
import asyncio
import traceback
import uuid
from collections.abc import Coroutine
from typing import (
Callable,
Optional,
)
from collections.abc import Callable, Coroutine
from loguru import logger
@ -84,7 +80,7 @@ invoice_listeners: dict[str, asyncio.Queue] = {}
# TODO: name should not be optional
# 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
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 hashlib import md5, pbkdf2_hmac, sha256
from typing import Union
from Cryptodome import Random
from Cryptodome.Cipher import AES
@ -40,7 +39,7 @@ class AESCipher:
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
if isinstance(key, bytes):
self.key = key

View file

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

View file

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

View file

@ -2,7 +2,6 @@ import base64
import hashlib
import json
import re
from typing import Union
from urllib.parse import urlparse
import secp256k1
@ -149,7 +148,7 @@ def sign_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.

View file

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

View file

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

View file

@ -1,6 +1,5 @@
import asyncio
from collections.abc import AsyncGenerator
from typing import Optional
from bolt11.decode import decode
from grpc.aio import AioRpcError
@ -107,9 +106,9 @@ class BoltzWallet(Wallet):
async def create_invoice(
self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
unhashed_description: Optional[bytes] = None,
memo: str | None = None,
description_hash: bytes | None = None,
unhashed_description: bytes | None = None,
**_,
) -> InvoiceResponse:
pair = boltzrpc_pb2.Pair(to=boltzrpc_pb2.LBTC)
@ -254,7 +253,7 @@ class BoltzWallet(Wallet):
)
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:
request = boltzrpc_pb2.GetWalletRequest(name=wallet_name)
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
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):
raise RuntimeError(
"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:
import asyncio
from collections.abc import AsyncGenerator
from pathlib import Path
from typing import Optional
from bolt11 import Bolt11Exception
from bolt11 import decode as bolt11_decode
@ -66,7 +65,7 @@ else:
):
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
if source.split(".")[-1] == extension:
with open(source, "rb") as f:
@ -85,7 +84,7 @@ else:
logger.debug(exc)
return None
def load_greenlight_credentials() -> Optional[GreenlightCredentials]:
def load_greenlight_credentials() -> GreenlightCredentials | None:
if (
settings.breez_greenlight_device_key
and settings.breez_greenlight_device_cert
@ -168,9 +167,9 @@ else:
async def create_invoice(
self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
unhashed_description: Optional[bytes] = None,
memo: str | None = None,
description_hash: bytes | None = None,
unhashed_description: bytes | None = None,
**kwargs,
) -> InvoiceResponse:
# if description_hash or unhashed_description:

View file

@ -8,7 +8,7 @@ if not find_spec("breez_sdk_liquid"):
def __init__(self):
raise RuntimeError(
"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:
@ -16,7 +16,6 @@ else:
from asyncio import Queue
from collections.abc import AsyncGenerator
from pathlib import Path
from typing import Optional
from bolt11 import decode as bolt11_decode
from breez_sdk_liquid import (
@ -128,9 +127,9 @@ else:
async def create_invoice(
self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
unhashed_description: Optional[bytes] = None,
memo: str | None = None,
description_hash: bytes | None = None,
unhashed_description: bytes | None = None,
**_,
) -> InvoiceResponse:
try:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import json
import random
import time
from collections.abc import AsyncGenerator
from typing import Optional, Union, cast
from typing import cast
from urllib.parse import parse_qs, unquote, urlparse
import secp256k1
@ -130,9 +130,9 @@ class NWCWallet(Wallet):
async def create_invoice(
self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
unhashed_description: Optional[bytes] = None,
memo: str | None = None,
description_hash: bytes | None = None,
unhashed_description: bytes | None = None,
**_,
) -> InvoiceResponse:
desc = ""
@ -360,7 +360,7 @@ class NWCConnection:
"""
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.
@ -396,7 +396,7 @@ class NWCConnection:
async def _close_subscription_by_subid(
self, sub_id: str, send_event: bool = True
) -> Optional[dict]:
) -> dict | None:
"""
Closes a subscription by its sub_id.
@ -427,7 +427,7 @@ class NWCConnection:
async def _close_subscription_by_eventid(
self, event_id, send_event=True
) -> Optional[dict]:
) -> dict | None:
"""
Closes a subscription associated to an event_id.
@ -514,7 +514,7 @@ class NWCConnection:
if subscription: # Check if the subscription exists first
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.
"""

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
import asyncio
import hashlib
from collections.abc import AsyncGenerator
from typing import Optional
import httpx
from bolt11 import decode as bolt11_decode
@ -60,9 +59,9 @@ class ZBDWallet(Wallet):
async def create_invoice(
self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
unhashed_description: Optional[bytes] = None,
memo: str | None = None,
description_hash: bytes | None = None,
unhashed_description: bytes | None = None,
**_,
) -> InvoiceResponse:
# 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"
version = "1.3.0-rc4"
requires-python = ">=3.10,<3.13"
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"
repository = "https://github.com/lnbits/lnbits"
homepage = "https://lnbits.com"
dependencies = [
"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 = [
{include = "lnbits"},
{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]
black = "^25.1.0"
mypy = "^1.17.1"
@ -94,14 +114,6 @@ types-mock = "^5.1.0.20240425"
mock = "^5.1.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]
include = [
"lnbits",
@ -255,3 +267,10 @@ extend-immutable-calls = [
"fastapi.Body",
"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 string
from typing import Optional
from pydantic import BaseModel
@ -14,13 +13,13 @@ class FakeError(Exception):
class DbTestModel(BaseModel):
id: int
name: str
value: Optional[str] = None
value: str | None = None
class DbTestModel2(BaseModel):
id: int
label: str
description: Optional[str] = None
description: str | None = None
child: DbTestModel
child_list: list[DbTestModel]

View file

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

View file

@ -1,5 +1,4 @@
import json
from typing import Union
from urllib.parse import urlencode
import pytest
@ -59,7 +58,7 @@ async def test_rest_wallet(httpserver: HTTPServer, test_data: WalletTest):
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 = 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
)
server_response: Union[str, dict, list, Response] = mock.response
server_response: str | dict | list | Response = mock.response
response_type = mock.response_type
if response_type == "response":
assert isinstance(server_response, dict), "server response must be JSON"

View file

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

View file

@ -7,7 +7,6 @@ import argparse
import os
import sqlite3
import sys
from typing import Optional
from lnbits.settings import settings
@ -108,7 +107,7 @@ def insert_to_pg(query, data):
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:
exclude_tables = []
print(f"Migrating core: {file}")
@ -124,7 +123,7 @@ def migrate_ext(file: str):
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:
if exclude_tables is None:
exclude_tables = []

2916
uv.lock generated Normal file

File diff suppressed because it is too large Load diff