diff --git a/.env.example b/.env.example index 0bdb78e23a..496bb905bd 100644 --- a/.env.example +++ b/.env.example @@ -1,82 +1,17 @@ #For more information on .env files, their content and format: https://pypi.org/project/python-dotenv/ -HOST=127.0.0.1 -PORT=5000 - -# uvicorn variable, uncomment to allow https behind a proxy -# FORWARDED_ALLOW_IPS="*" - -DEBUG=false - -# logging into LNBITS_DATA_FOLDER/logs/ -ENABLE_LOG_TO_FILE=true - -# https://loguru.readthedocs.io/en/stable/api/logger.html#file -LOG_ROTATION="100 MB" -LOG_RETENTION="3 months" - -# Server security, rate limiting ips, blocked ips, allowed ips -LNBITS_RATE_LIMIT_NO="200" -LNBITS_RATE_LIMIT_UNIT="minute" -LNBITS_ALLOWED_IPS="" -LNBITS_BLOCKED_IPS="" - -# Allow users and admins by user IDs (comma separated list) -# if set new users will not be able to create accounts -LNBITS_ALLOWED_USERS="" -LNBITS_ADMIN_USERS="" -# Extensions only admin can access -LNBITS_ADMIN_EXTENSIONS="ngrok, admin" - -# Disable account creation for new users -# LNBITS_ALLOW_NEW_ACCOUNTS=false - -# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available -# Warning: Enabling this will make LNbits ignore this configuration file. Your settings will -# be stored in your database and you will be able to change them only through the Admin UI. +###################################### +########### Admin Settings ########### +###################################### + +# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available. +# Warning: Enabling this will make LNbits ignore most configurations in file. Only the +# configurations defined in `ReadOnlySettings` will still be read from the environment variables. +# The rest of the settings will be stored in your database and you will be able to change them +# only through the Admin UI. # Disable this to make LNbits use this config file again. LNBITS_ADMIN_UI=false -LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" - -# Ad space description -# LNBITS_AD_SPACE_TITLE="Supported by" -# csv ad space, format ";;, ;;", extensions can choose to honor -# LNBITS_AD_SPACE="https://shop.lnbits.com/;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-light.png;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-dark.png" - -# Hides wallet api, extensions can choose to honor -LNBITS_HIDE_API=false - -# LNBITS_EXTENSIONS_MANIFESTS="https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json,https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions-trial.json" -# GitHub has rate-limits for its APIs. The limit can be increased specifying a GITHUB_TOKEN -# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx - -# Path where extensions will be installed (defaults to `./lnbits/`). -# Inside this directory the `extensions` and `upgrades` sub-directories will be created. -# LNBITS_EXTENSIONS_PATH="/path/to/some/dir" - - -# Extensions to be installed by default. If an extension from this list is uninstalled then it will be re-installed on the next restart. -# The extension must be removed from this list in order to not be re-installed. -LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos" - -# Database: to use SQLite, specify LNBITS_DATA_FOLDER -# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://... -# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://... -# for both PostgreSQL and CockroachDB, you'll need to install -# psycopg2 as an additional dependency -LNBITS_DATA_FOLDER="./data" -# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename" - -LNBITS_SERVICE_FEE="0.0" -# value in millisats -LNBITS_RESERVE_FEE_MIN=2000 -# value in percent -LNBITS_RESERVE_FEE_PERCENT=1.0 - -# Limit fiat currencies allowed to see in UI -# LNBITS_ALLOWED_CURRENCIES="EUR, USD" - # Change theme LNBITS_SITE_TITLE="LNbits" LNBITS_SITE_TAGLINE="free and open-source lightning wallet" @@ -85,8 +20,15 @@ LNBITS_SITE_DESCRIPTION="Some description about your service, will display if ti LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber" # LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg" +HOST=127.0.0.1 +PORT=5000 + +###################################### +########## Funding Source ############ +###################################### + # which fundingsources are allowed in the admin ui -LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, OpenNodeWallet" +LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, AlbyWallet, ZBDWallet, OpenNodeWallet" LNBITS_BACKEND_WALLET_CLASS=VoidWallet # VoidWallet is just a fallback that works without any actual Lightning capabilities, @@ -109,8 +51,7 @@ CORELIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc" # CoreLightningRestWallet CORELIGHTNING_REST_URL=http://127.0.0.1:8185/ -# Path or BASE64/HEX STRING -CORELIGHTNING_REST_MACAROON="/path/to/clnrest/access.macaroon" +CORELIGHTNING_REST_MACAROON="/path/to/clnrest/access.macaroon" # or BASE64/HEXSTRING CORELIGHTNING_REST_CERT="/path/to/clnrest/tls.cert" # LnbitsWallet @@ -120,15 +61,15 @@ LNBITS_KEY=LNBITS_ADMIN_KEY # LndWallet LND_GRPC_ENDPOINT=127.0.0.1 LND_GRPC_PORT=10009 -LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert" -LND_GRPC_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING" +LND_GRPC_CERT="/home/bob/.lnd/tls.cert" +LND_GRPC_MACAROON="/home/bob/.lnd/data/chain/bitcoin/mainnet/admin.macaroon" # or HEXSTRING # To use an AES-encrypted macaroon, set # LND_GRPC_MACAROON="eNcRyPtEdMaCaRoOn" # LndRestWallet LND_REST_ENDPOINT=https://127.0.0.1:8080/ -LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert" -LND_REST_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING" +LND_REST_CERT="/home/bob/.lnd/tls.cert" +LND_REST_MACAROON="/home/bob/.lnd/data/chain/bitcoin/mainnet/admin.macaroon" # or HEXSTRING # To use an AES-encrypted macaroon, set # LND_REST_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn" @@ -139,6 +80,14 @@ LNPAY_API_KEY=LNPAY_API_KEY # Wallet Admin in Wallet Access Keys LNPAY_WALLET_KEY=LNPAY_ADMIN_KEY +# AlbyWallet +ALBY_API_ENDPOINT=https://api.getalby.com/ +ALBY_ACCESS_TOKEN=ALBY_ACCESS_TOKEN + +# ZBDWallet +ZBD_API_ENDPOINT=https://api.zebedee.io/v0/ +ZBD_API_KEY=ZBD_ACCESS_TOKEN + # OpenNodeWallet OPENNODE_API_ENDPOINT=https://api.opennode.com/ OPENNODE_KEY=OPENNODE_ADMIN_KEY @@ -155,3 +104,139 @@ ECLAIR_PASS=eclairpw # Enter /api in LightningTipBot to get your key LNTIPS_API_KEY=LNTIPS_ADMIN_KEY LNTIPS_API_ENDPOINT=https://ln.tips + +###################################### +####### Auth Configurations ########## +###################################### +# Secret Key: will default to the hash of the super user. It is strongly recommended that you set your own value. +AUTH_SECRET_KEY="" +AUTH_TOKEN_EXPIRE_MINUTES=525600 +# Possible authorization methods: user-id-only, username-password, google-auth, github-auth, keycloak-auth +AUTH_ALLOWED_METHODS="user-id-only, username-password" +# Set this flag if HTTP is used for OAuth +# OAUTHLIB_INSECURE_TRANSPORT="1" + +# Google OAuth Config +# Make sure that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token +GOOGLE_CLIENT_ID="" +GOOGLE_CLIENT_SECRET="" + +# GitHub OAuth Config +# Make sure that the authorization callback URL is set to https://{domain}/api/v1/auth/github/token +GITHUB_CLIENT_ID="" +GITHUB_CLIENT_SECRET="" + +# Keycloak OAuth Config +# Make sure that the valid redirect URIs contain https://{domain}/api/v1/auth/keycloak/token +KEYCLOAK_CLIENT_ID="" +KEYCLOAK_CLIENT_SECRET="" +KEYCLOAK_DISCOVERY_URL="" + + +###################################### + +# uvicorn variable, uncomment to allow https behind a proxy +# IMPORTANT: this also needs the webserver to be configured to forward the headers +# http://docs.lnbits.org/guide/installation.html#running-behind-an-apache2-reverse-proxy-over-https +# FORWARDED_ALLOW_IPS="*" + +# Server security, rate limiting ips, blocked ips, allowed ips +LNBITS_RATE_LIMIT_NO="200" +LNBITS_RATE_LIMIT_UNIT="minute" +LNBITS_ALLOWED_IPS="" +LNBITS_BLOCKED_IPS="" + +# Allow users and admins by user IDs (comma separated list) +# if set new users will not be able to create accounts +LNBITS_ALLOWED_USERS="" +LNBITS_ADMIN_USERS="" +# ID of the super user. The user ID must exist. +# SUPER_USER="" + +# Extensions only admin can access +LNBITS_ADMIN_EXTENSIONS="ngrok, admin" + +# Start LNbits core only. The extensions are not loaded. +# LNBITS_EXTENSIONS_DEACTIVATE_ALL=true + +# Disable account creation for new users +# LNBITS_ALLOW_NEW_ACCOUNTS=false + +# Enable Node Management without activating the LNBITS Admin GUI +# by setting the following variables to true. +LNBITS_NODE_UI=false +LNBITS_PUBLIC_NODE_UI=false +# Enabling the transactions tab can cause crashes on large Core Lightning nodes. +LNBITS_NODE_UI_TRANSACTIONS=false + +LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" + +# Ad space description +# LNBITS_AD_SPACE_TITLE="Supported by" +# csv ad space, format ";;, ;;", extensions can choose to honor +# LNBITS_AD_SPACE="https://shop.lnbits.com/;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-light.png;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-dark.png" + +# Hides wallet api, extensions can choose to honor +LNBITS_HIDE_API=false + +# LNBITS_EXTENSIONS_MANIFESTS="https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json,https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions-trial.json" +# GitHub has rate-limits for its APIs. The limit can be increased specifying a GITHUB_TOKEN +# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx + +# Path where extensions will be installed (defaults to `./lnbits/`). +# Inside this directory the `extensions` and `upgrades` sub-directories will be created. +# LNBITS_EXTENSIONS_PATH="/path/to/some/dir" + +# Extensions to be installed by default. If an extension from this list is uninstalled then it will be re-installed on the next restart. +# The extension must be removed from this list in order to not be re-installed. +LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos" + +# Database: to use SQLite, specify LNBITS_DATA_FOLDER +# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://... +# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://... +# for both PostgreSQL and CockroachDB, you'll need to install +# psycopg2 as an additional dependency +LNBITS_DATA_FOLDER="./data" +# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename" + +# the service fee (in percent) +LNBITS_SERVICE_FEE=0.0 +# the wallet where fees go to +# LNBITS_SERVICE_FEE_WALLET= +# the maximum fee per transaction (in satoshis) +# LNBITS_SERVICE_FEE_MAX=1000 +# disable fees for internal transactions +# LNBITS_SERVICE_FEE_IGNORE_INTERNAL=true + +# value in millisats +LNBITS_RESERVE_FEE_MIN=2000 +# value in percent +LNBITS_RESERVE_FEE_PERCENT=1.0 + +# limit the maximum balance for each wallet +# throw an error if the wallet attempts to create a new invoice + +# LNBITS_WALLET_LIMIT_MAX_BALANCE=1000000 +# LNBITS_WALLET_LIMIT_DAILY_MAX_WITHDRAW=1000000 +# LNBITS_WALLET_LIMIT_SECS_BETWEEN_TRANS=60 + +# Limit fiat currencies allowed to see in UI +# LNBITS_ALLOWED_CURRENCIES="EUR, USD" + +###################################### +###### Logging and Development ####### +###################################### + +DEBUG=false +DEBUG_DATABASE=false +BUNDLE_ASSETS=true + +# logging into LNBITS_DATA_FOLDER/logs/ +ENABLE_LOG_TO_FILE=true + +# https://loguru.readthedocs.io/en/stable/api/logger.html#file +LOG_ROTATION="100 MB" +LOG_RETENTION="3 months" + +# for database cleanup commands +# CLEANUP_WALLETS_DAYS=90 diff --git a/.github/actions/prepare/action.yml b/.github/actions/prepare/action.yml index 9ec480ff21..7ebee9b04b 100644 --- a/.github/actions/prepare/action.yml +++ b/.github/actions/prepare/action.yml @@ -7,10 +7,10 @@ inputs: default: "3.9" poetry-version: description: "Poetry Version" - default: "1.5.1" + default: "1.7.0" node-version: description: "Node Version" - default: "18.x" + default: "20.x" npm: description: "use npm" default: false @@ -21,7 +21,7 @@ runs: using: "composite" steps: - name: Set up Python ${{ inputs.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ inputs.python-version }} # cache poetry install via pip @@ -38,7 +38,7 @@ runs: poetry config virtualenvs.create true --local poetry config virtualenvs.in-project true --local - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Define a cache for the virtual environment based on the dependencies lock file with: path: ./.venv @@ -50,11 +50,11 @@ runs: - name: Use Node.js ${{ inputs.node-version }} if: ${{ (inputs.npm == 'true') }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ inputs.node-version }} - - uses: actions/cache@v3 + - uses: actions/cache@v4 if: ${{ (inputs.npm == 'true') }} name: Define a cache for the npm based on the dependencies lock file with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54418c51d8..569f58de52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,16 +21,18 @@ jobs: uses: ./.github/workflows/tests.yml with: python-version: ${{ matrix.python-version }} + db-url: ${{ matrix.db-url }} + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - migrations: + migration: needs: [ lint ] strategy: matrix: - python-version: ["3.9"] - uses: ./.github/workflows/tests.yml + python-version: ["3.9", "3.10"] + uses: ./.github/workflows/migration.yml with: - make: test-migration - db-name: migration + python-version: ${{ matrix.python-version }} openapi: needs: [ lint ] @@ -38,9 +40,6 @@ jobs: with: make: openapi - # docker: - # uses: ./.github/workflows/docker.yml - regtest: needs: [ lint ] uses: ./.github/workflows/regtest.yml @@ -51,3 +50,16 @@ jobs: with: python-version: ${{ matrix.python-version }} backend-wallet-class: ${{ matrix.backend-wallet-class }} + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + jmeter: + needs: [ lint ] + strategy: + matrix: + python-version: ["3.9"] + poetry-version: ["1.5.1"] + uses: ./.github/workflows/jmeter.yml + with: + python-version: ${{ matrix.python-version }} + poetry-version: ${{ matrix.poetry-version }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5cdd4139b2..ee2212560f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 2 - run: git checkout HEAD^2 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index dd29b9e11e..f2fc503130 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,20 +1,53 @@ name: docker on: + workflow_dispatch: + inputs: + tag: + default: latest + type: string workflow_call: + inputs: + tag: + default: latest + type: string + secrets: + DOCKER_USERNAME: + required: true + DOCKER_PASSWORD: + required: true jobs: - docker: + push_to_dockerhub: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + uses: actions/cache@v4 + id: cache + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + - name: Build and push - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . - push: false - tags: lnbitsdocker/lnbits-legend:latest - cache-from: type=registry,ref=lnbitsdocker/lnbits-legend:latest - cache-to: type=inline + push: true + tags: ${{ secrets.DOCKER_USERNAME }}/lnbits:${{ inputs.tag }} + platforms: linux/amd64,linux/arm64 + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache diff --git a/.github/workflows/jmeter.yml b/.github/workflows/jmeter.yml new file mode 100644 index 0000000000..d46b483e07 --- /dev/null +++ b/.github/workflows/jmeter.yml @@ -0,0 +1,53 @@ +name: JMeter Extension Tests + +on: + workflow_call: + inputs: + python-version: + description: "Python Version" + required: true + default: "3.9" + type: string + poetry-version: + description: "Poetry Version" + required: true + default: "1.5.1" + type: string + +jobs: + jmeter: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/prepare + with: + python-version: ${{ inputs.python-version }} + + - name: run LNbits + env: + LNBITS_ADMIN_UI: true + LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly, satspay, tipjar, tpos, lnurlp, withdraw" + LNBITS_BACKEND_WALLET_CLASS: FakeWallet + run: | + poetry run lnbits & + sleep 5 + + - name: clone lnbits-extensions, install jmeter and run tests + run: | + git clone https://github.com/lnbits/lnbits-extensions + cd lnbits-extensions + mkdir logs + mkdir reports + make install-jmeter + make start-mirror-server + make test + + - name: upload jmeter test results + uses: actions/upload-artifact@v4 + if: ${{ always() }} + with: + name: jmeter-extension-test-results + path: | + lnbits-extensions/reports/ + lnbits-extensions/logs/ diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 53cbad1c4a..722222113c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,25 +3,53 @@ on: workflow_call: jobs: + black: uses: ./.github/workflows/make.yml + strategy: + matrix: + python-version: ["3.9", "3.10"] with: make: checkblack + python-version: ${{ matrix.python-version }} + ruff: uses: ./.github/workflows/make.yml + strategy: + matrix: + python-version: ["3.9", "3.10"] with: make: checkruff + python-version: ${{ matrix.python-version }} + mypy: uses: ./.github/workflows/make.yml + strategy: + matrix: + python-version: ["3.9", "3.10"] with: make: mypy + python-version: ${{ matrix.python-version }} + pyright: uses: ./.github/workflows/make.yml + strategy: + matrix: + python-version: ["3.9", "3.10"] with: make: pyright + python-version: ${{ matrix.python-version }} npm: true + + prettier: uses: ./.github/workflows/make.yml with: make: checkprettier npm: true + + bundle: + uses: ./.github/workflows/make.yml + with: + make: checkbundle + npm: true diff --git a/.github/workflows/make.yml b/.github/workflows/make.yml index 21dec850b6..5f8694fbe0 100644 --- a/.github/workflows/make.yml +++ b/.github/workflows/make.yml @@ -11,21 +11,24 @@ on: description: "use npm install" default: false type: boolean + python-version: + description: "python version" + type: string + default: "3.10" jobs: make: - name: ${{ inputs.make }} (${{ matrix.python-version }}) + name: ${{ inputs.make }} (${{ inputs.python-version }}) strategy: matrix: os-version: ["ubuntu-latest"] - python-version: ["3.9", "3.10"] node-version: ["18.x"] runs-on: ${{ matrix.os-version }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/prepare with: - python-version: ${{ matrix.python-version }} + python-version: ${{ inputs.python-version }} node-version: ${{ matrix.node-version }} npm: ${{ inputs.npm }} - run: make ${{ inputs.make }} diff --git a/.github/workflows/migration.yml b/.github/workflows/migration.yml new file mode 100644 index 0000000000..9edb62e3cd --- /dev/null +++ b/.github/workflows/migration.yml @@ -0,0 +1,37 @@ +name: migration + +on: + workflow_call: + inputs: + python-version: + description: "python version" + type: string + default: "3.10" + +jobs: + make: + name: migration (${{ inputs.python-version }}) + strategy: + matrix: + os-version: ["ubuntu-latest"] + runs-on: ${{ matrix.os-version }} + services: + postgres: + image: postgres:latest + env: + POSTGRES_USER: lnbits + POSTGRES_PASSWORD: lnbits + POSTGRES_DB: migration + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + with: + python-version: ${{ inputs.python-version }} + - run: make test-migration diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 027910cb41..30dd54fbe3 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -1,15 +1,21 @@ name: LNbits CI / nix +# - run : on main, dev, nix and cachix branches when relevant files change +# - cache : on main, dev and cachix branches when relevant files change + on: push: branches: - main - dev + - nix + - cachix paths: - 'flake.nix' - 'flake.lock' - 'pyproject.toml' - 'poetry.lock' + - '.github/workflows/nix.yml' pull_request: paths: - 'flake.nix' @@ -21,8 +27,15 @@ jobs: nix: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v23 + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@v24 + with: + nix_path: nixpkgs=channel:nixos-23.11 + - uses: cachix/cachix-action@v13 + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/cachix' with: - nix_path: nixpkgs=channel:nixos-unstable - - run: nix build + name: lnbits + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + - run: nix build -L + - run: cachix push lnbits ./result + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/cachix' diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml deleted file mode 100644 index 2ff8bff0cd..0000000000 --- a/.github/workflows/on-tag.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Build and push Docker image on tag - -env: - DOCKER_CLI_EXPERIMENTAL: enabled - -on: - push: - tags: - - "[0-9]+.[0-9]+.[0-9]+" - - "[0-9]+.[0-9]+.[0-9]+.[0-9]+" - - "[0-9]+.[0-9]+.[0-9]+-*" - -jobs: - build: - runs-on: ubuntu-20.04 - name: Build and push lnbits image - steps: - - name: Login to Docker Hub - run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin - - - name: Checkout project - uses: actions/checkout@v3 - - - name: Import environment variables - id: import-env - shell: bash - run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV - - - name: Show set environment variables - run: | - printf " TAG: %s\n" "$TAG" - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - id: qemu - - - name: Setup Docker buildx action - uses: docker/setup-buildx-action@v1 - id: buildx - - - name: Show available Docker buildx platforms - run: echo ${{ steps.buildx.outputs.platforms }} - - - name: Cache Docker layers - uses: actions/cache@v2 - id: cache - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx- - - - name: Run Docker buildx against tag - run: | - docker buildx build \ - --cache-from "type=local,src=/tmp/.buildx-cache" \ - --cache-to "type=local,dest=/tmp/.buildx-cache" \ - --platform linux/amd64,linux/arm64 \ - --tag ${{ secrets.DOCKER_USERNAME }}/lnbits-legend:${TAG} \ - --output "type=registry" ./ - - - name: Run Docker buildx against latest - run: | - docker buildx build \ - --cache-from "type=local,src=/tmp/.buildx-cache" \ - --cache-to "type=local,dest=/tmp/.buildx-cache" \ - --platform linux/amd64,linux/arm64 \ - --tag ${{ secrets.DOCKER_USERNAME }}/lnbits-legend:latest \ - --output "type=registry" ./ diff --git a/.github/workflows/regtest.yml b/.github/workflows/regtest.yml index 48643c90a3..87deff7daf 100644 --- a/.github/workflows/regtest.yml +++ b/.github/workflows/regtest.yml @@ -15,26 +15,29 @@ on: backend-wallet-class: required: true type: string + secrets: + CODECOV_TOKEN: + required: true jobs: regtest: runs-on: ${{ inputs.os-version }} timeout-minutes: 10 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Docker Buildx if: ${{ inputs.backend-wallet-class == 'LNbitsWallet' }} - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build and push if: ${{ inputs.backend-wallet-class == 'LNbitsWallet' }} - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . push: false - tags: lnbitsdocker/lnbits-legend:latest - cache-from: type=registry,ref=lnbitsdocker/lnbits-legend:latest + tags: lnbits/lnbits:latest + cache-from: type=registry,ref=lnbits/lnbits:latest cache-to: type=inline - name: Setup Regtest @@ -51,9 +54,10 @@ jobs: - name: Create fake admin if: ${{ inputs.backend-wallet-class == 'LNbitsWallet' }} - run: docker exec lnbits-legend-lnbits-1 poetry run python tools/create_fake_admin.py + run: docker exec lnbits-lnbits-1 poetry run python tools/create_fake_admin.py - - name: Run Tests + - name: Run pytest + uses: pavelzw/pytest-action@v2 env: LNBITS_DATABASE_URL: ${{ inputs.db-url }} LNBITS_BACKEND_WALLET_CLASS: ${{ inputs.backend-wallet-class }} @@ -72,10 +76,20 @@ jobs: LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee" ECLAIR_URL: http://127.0.0.1:8082 ECLAIR_PASS: lnbits - run: make test-real-wallet + LNBITS_DATA_FOLDER: "./tests/data" + PYTHONUNBUFFERED: 1 + DEBUG: true + with: + verbose: false + job-summary: true + emoji: false + click-to-expand: false + custom-pytest: poetry run pytest + report-title: "regtest (${{ inputs.python-version }}, ${{ inputs.backend-wallet-class }}" - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: - token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..4b79df8ca3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +name: release + +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+" + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Create github release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ github.ref_name }} + run: | + gh release create "$tag" --generate-notes --draft + + docker: + needs: [ release ] + uses: ./.github/workflows/docker.yml + with: + tag: ${{ github.ref_name }} + secrets: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 751ce7c617..00c50ce78a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,9 +3,6 @@ name: tests on: workflow_call: inputs: - make: - default: test - type: string python-version: default: "3.9" type: string @@ -18,6 +15,9 @@ on: db-name: default: "lnbits" type: string + secrets: + CODECOV_TOKEN: + required: true jobs: tests: @@ -39,20 +39,32 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/prepare with: python-version: ${{ inputs.python-version }} - - name: Run Tests + - name: Run pytest + uses: pavelzw/pytest-action@v2 env: LNBITS_DATABASE_URL: ${{ inputs.db-url }} LNBITS_BACKEND_WALLET_CLASS: FakeWallet - run: make ${{ inputs.make }} + FAKE_WALLET_SECRET: "ToTheMoon1" + LNBITS_DATA_FOLDER: "./tests/data" + PYTHONUNBUFFERED: 1 + DEBUG: true + with: + verbose: false + job-summary: true + emoji: false + click-to-expand: false + custom-pytest: poetry run pytest + report-title: "test (${{ inputs.python-version }}, ${{ inputs.db-url }})" - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: - token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} + verbose: false diff --git a/.gitignore b/.gitignore index d6c0a6e34e..8e7c3f3416 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ __pycache__ *.egg *.egg-info .coverage +.coverage.* .pytest_cache .webassets-cache htmlcov @@ -35,6 +36,8 @@ coverage.xml node_modules lnbits/static/bundle.js lnbits/static/bundle.css +lnbits/static/bundle.min.js.old +lnbits/static/bundle.min.css.old docker # Nix diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1a64c9c57f..d9950932e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,11 +14,11 @@ repos: - id: mixed-line-ending - id: check-case-conflict - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 24.2.0 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.283 + rev: v0.3.2 hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix ] diff --git a/Dockerfile b/Dockerfile index 53e832a2e8..9f8d36e157 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM python:3.10-slim-bullseye RUN apt-get clean RUN apt-get update -RUN apt-get install -y curl pkg-config build-essential +RUN apt-get install -y curl pkg-config build-essential libnss-myhostname RUN curl -sSL https://install.python-poetry.org | python3 - ENV PATH="/root/.local/bin:$PATH" diff --git a/Makefile b/Makefile index df5834d248..bacee95c58 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ all: format check format: prettier black ruff -check: mypy pyright checkblack checkruff checkprettier +check: mypy pyright checkblack checkruff checkprettier checkbundle prettier: poetry run ./node_modules/.bin/prettier --write lnbits @@ -96,9 +96,16 @@ bundle: npm run vendor_minify_css npm run vendor_bundle_js npm run vendor_minify_js - # increment serviceworker version - sed -i -e "s/CACHE_VERSION =.*/CACHE_VERSION = $$(awk '/CACHE_VERSION =/ { print 1+$$4 }' lnbits/core/static/js/service-worker.js)/" \ - lnbits/core/static/js/service-worker.js + +checkbundle: + cp lnbits/static/bundle.min.js lnbits/static/bundle.min.js.old + cp lnbits/static/bundle.min.css lnbits/static/bundle.min.css.old + make bundle + diff -q lnbits/static/bundle.min.js lnbits/static/bundle.min.js.old || exit 1 + diff -q lnbits/static/bundle.min.css lnbits/static/bundle.min.css.old || exit 1 + @echo "Bundle is OK" + rm lnbits/static/bundle.min.js.old + rm lnbits/static/bundle.min.css.old install-pre-commit-hook: @echo "Installing pre-commit hook to git" diff --git a/docs/devs/api.md b/docs/devs/api.md index 8e15088990..946e9f1ba8 100644 --- a/docs/devs/api.md +++ b/docs/devs/api.md @@ -9,4 +9,4 @@ nav_order: 3 API reference ============= -[Swagger Docs](https://docs.lnbits.org/devs/swagger.html) +[Swagger Docs](https://legend.lnbits.com/docs) diff --git a/docs/devs/extensions.md b/docs/devs/extensions.md index a70f085ff5..2476bfe16d 100644 --- a/docs/devs/extensions.md +++ b/docs/devs/extensions.md @@ -21,7 +21,7 @@ mv templates/example templates/mysuperplugin # Rename templates folder. - if you are on macOS and having difficulty with 'sed', consider `brew install gnu-sed` and use 'gsed', without -0 option after xargs. 1. Edit `manifest.json` and change the organisation name to your GitHub username. -1. Push your changes to GitHub. +1. Push your changes to GitHub. 1. In GitHub create a new release for your extension repo. Tag the release with `0.0.1` 1. Copy the URL of the extension's raw `manifest.json` URL `https://raw.githubusercontent.com/[my-user-name]/mysuperplugin/master/manifest.json` 1. If you are using the LMNbits Admin UI, go to the Admin UI > Server > Extension Sources, click "Add", paste the URL, then click "Save" @@ -42,7 +42,7 @@ Extension structure explained Adding new dependencies ----------------------- -DO NOT ADD NEW DEPENDENCIES. Try to use the dependencies that are availabe in `pyproject.toml`. Getting the LNbits project to accept a new dependency is time consuming and uncertain, and may result in your extension NOT being made available to others. +DO NOT ADD NEW DEPENDENCIES. Try to use the dependencies that are available in `pyproject.toml`. Getting the LNbits project to accept a new dependency is time consuming and uncertain, and may result in your extension NOT being made available to others. If for some reason your extensions must have a new python package to work, and its nees are not met in `pyproject.toml`, you can add a new package using `poerty`: @@ -51,7 +51,7 @@ $ poetry add ``` **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` compatability can be tested with `nix build .#checks.x86_64-linux.vmTest`. +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`. SQLite to PostgreSQL migration diff --git a/docs/guide/admin_ui.md b/docs/guide/admin_ui.md index b705cc9c76..d3ad458bcb 100644 --- a/docs/guide/admin_ui.md +++ b/docs/guide/admin_ui.md @@ -33,7 +33,7 @@ There is also the possibility of posting the super user via webhook to another s Admin Users =========== environment variable: `LNBITS_ADMIN_USERS`, comma-separated list of user ids -Admin Users can change settings in the admin ui as well, with the exception of funding source settings, because they require e server restart and could potentially make the server inaccessable. Also they have access to all the extension defined in `LNBITS_ADMIN_EXTENSIONS`. +Admin Users can change settings in the admin ui as well, with the exception of funding source settings, because they require e server restart and could potentially make the server inaccessible. Also they have access to all the extension defined in `LNBITS_ADMIN_EXTENSIONS`. Allowed Users @@ -60,7 +60,7 @@ $ poetry run lnbits ``` You can now `cat` the Super User ID: ``` -$ cat .super_user +$ cat data/.super_user 123de4bfdddddbbeb48c8bc8382fe123 ``` You can access your super user account at `/wallet?usr=super_user_id`. You just have to append it to your normal LNbits web domain. diff --git a/docs/guide/faq.md b/docs/guide/faq.md index 3d72b5f78c..ced5a57661 100644 --- a/docs/guide/faq.md +++ b/docs/guide/faq.md @@ -34,7 +34,7 @@ allow-self-payment=1
Which funding sources can I use for LNbits?

There are several ways to run a LNbits instance funded from different sources. It is important to choose a source that has a good liquidity and good peers connected. If you use LNbits for public services your users´ payments can then flow happily in both directions. If you would like to fund your LNbits wallet via btc please see section Troubleshooting.

-

The LNbits manual shows you which sources can be used and how to configure each: CLN, LND, LNPay, Cliche, OpenNode as well as bots.

+

The LNbits manual shows you which sources can be used and how to configure each.

{release.version}") + click.echo("") + return None + + if not repo_index.isnumeric() or not 0 <= int(repo_index) < len(repos): + click.echo(f"--repo-index must be between '0' and '{len(repos) - 1}'") + return None + + return latest_repo_releases[repos[int(repo_index)]] + + +def _get_latest_release_per_repo(all_releases): + latest_repo_releases = {} + for release in all_releases: + try: + if not release.is_version_compatible: + continue + # do not remove, parsing also validates + release_version = version.parse(release.version) + if release.source_repo not in latest_repo_releases: + latest_repo_releases[release.source_repo] = release + continue + if release_version > version.parse( + latest_repo_releases[release.source_repo].version + ): + latest_repo_releases[release.source_repo] = release + except version.InvalidVersion as ex: + logger.warning(f"Invalid version {release.name}: {ex}") + return latest_repo_releases + + +async def _call_install_extension( + data: CreateExtension, url: Optional[str], user_id: Optional[str] = None +): + if url: + user_id = user_id or get_super_user() + async with httpx.AsyncClient() as client: + resp = await client.post( + f"{url}/api/v1/extension?usr={user_id}", json=data.dict(), timeout=40 + ) + resp.raise_for_status() + else: + await api_install_extension(data, User(id="mock_id")) + + +async def _call_uninstall_extension( + extension: str, url: Optional[str], user_id: Optional[str] = None +): + if url: + user_id = user_id or get_super_user() + async with httpx.AsyncClient() as client: + resp = await client.delete( + f"{url}/api/v1/extension/{extension}?usr={user_id}", timeout=40 + ) + resp.raise_for_status() + else: + await api_uninstall_extension(extension, User(id="mock_id")) + + +async def _can_run_operation(url) -> bool: + await check_admin_settings() + if await _is_lnbits_started(url): + if not url: + click.echo("LNbits server is started. Please either:") + click.echo( + f" - use the '--url' option. Eg: --url=http://{settings.host}:{settings.port}" + ) + click.echo( + f" - stop the server running at 'http://{settings.host}:{settings.port}'" + ) + + return False + elif url: + click.echo( + "The option '--url' has been provided," + + f" but no server found runnint at '{url}'" + ) + return False + + return True + + +async def _is_lnbits_started(url: Optional[str]): + try: + url = url or f"http://{settings.host}:{settings.port}/api/v1/health" + async with httpx.AsyncClient() as client: + await client.get(url) + return True + except Exception: + return False + + +def _is_url(url): + try: + result = urlparse(url) + return all([result.scheme, result.netloc]) + except ValueError: + return False diff --git a/lnbits/core/__init__.py b/lnbits/core/__init__.py index 467ca67d75..1ce6a0f6c4 100644 --- a/lnbits/core/__init__.py +++ b/lnbits/core/__init__.py @@ -1,24 +1,38 @@ -from fastapi import APIRouter +from fastapi import APIRouter, FastAPI from .db import core_app_extra, db from .views.admin_api import admin_router from .views.api import api_router +from .views.auth_api import auth_router +from .views.extension_api import extension_router # this compat is needed for usermanager extension from .views.generic import generic_router, update_user_extension from .views.node_api import node_router, public_node_router, super_node_router +from .views.payment_api import payment_router from .views.public_api import public_router +from .views.tinyurl_api import tinyurl_router +from .views.wallet_api import wallet_router +from .views.webpush_api import webpush_router +from .views.websocket_api import websocket_router # backwards compatibility for extensions core_app = APIRouter(tags=["Core"]) -def init_core_routers(app): +def init_core_routers(app: FastAPI): app.include_router(core_app) app.include_router(generic_router) - app.include_router(public_router) - app.include_router(api_router) + app.include_router(auth_router) + app.include_router(admin_router) app.include_router(node_router) + app.include_router(extension_router) app.include_router(super_node_router) app.include_router(public_node_router) - app.include_router(admin_router) + app.include_router(public_router) + app.include_router(payment_router) + app.include_router(wallet_router) + app.include_router(api_router) + app.include_router(websocket_router) + app.include_router(tinyurl_router) + app.include_router(webpush_router) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 3788732070..023228c351 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -1,10 +1,12 @@ import datetime import json +from time import time from typing import Any, Dict, List, Literal, Optional from urllib.parse import urlparse from uuid import UUID, uuid4 import shortuuid +from passlib.context import CryptContext from lnbits.core.db import db from lnbits.core.models import WalletType @@ -20,11 +22,14 @@ from .models import ( BalanceCheck, + CreateUser, Payment, PaymentFilters, PaymentHistoryPoint, TinyURL, + UpdateUserPassword, User, + UserConfig, Wallet, WebPushSubscription, ) @@ -33,8 +38,48 @@ # -------- +async def create_user( + data: CreateUser, user_config: Optional[UserConfig] = None +) -> User: + if not settings.new_accounts_allowed: + raise ValueError("Account creation is disabled.") + if await get_account_by_username(data.username): + raise ValueError("Username already exists.") + + if data.email and await get_account_by_email(data.email): + raise ValueError("Email already exists.") + + pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + user_id = uuid4().hex + tsph = db.timestamp_placeholder + now = int(time()) + await db.execute( + f""" + INSERT INTO accounts + (id, email, username, pass, extra, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, {tsph}, {tsph}) + """, + ( + user_id, + data.email, + data.username, + pwd_context.hash(data.password), + json.dumps(dict(user_config)) if user_config else "{}", + now, + now, + ), + ) + new_account = await get_account(user_id=user_id) + assert new_account, "Newly created account couldn't be retrieved" + return new_account + + async def create_account( - conn: Optional[Connection] = None, user_id: Optional[str] = None + conn: Optional[Connection] = None, + user_id: Optional[str] = None, + email: Optional[str] = None, + user_config: Optional[UserConfig] = None, ) -> User: if user_id: user_uuid4 = UUID(hex=user_id, version=4) @@ -42,7 +87,15 @@ async def create_account( else: user_id = uuid4().hex - await (conn or db).execute("INSERT INTO accounts (id) VALUES (?)", (user_id,)) + extra = json.dumps(dict(user_config)) if user_config else "{}" + now = int(time()) + await (conn or db).execute( + f""" + INSERT INTO accounts (id, email, extra, created_at, updated_at) + VALUES (?, ?, ?, {db.timestamp_placeholder}, {db.timestamp_placeholder}) + """, + (user_id, email, extra, now, now), + ) new_account = await get_account(user_id=user_id, conn=conn) assert new_account, "Newly created account couldn't be retrieved" @@ -50,19 +103,176 @@ async def create_account( return new_account +async def update_account( + user_id: str, + username: Optional[str] = None, + email: Optional[str] = None, + user_config: Optional[UserConfig] = None, +) -> Optional[User]: + user = await get_account(user_id) + assert user, "User not found" + + if email: + assert not user.email or email == user.email, "Cannot change email." + account = await get_account_by_email(email) + assert not account or account.id == user_id, "Email already in use." + + if username: + assert not user.username or username == user.username, "Cannot change username." + account = await get_account_by_username(username) + assert not account or account.id == user_id, "Username already in exists." + + username = user.username or username + email = user.email or email + extra = user_config or user.config + + now = int(time()) + await db.execute( + f""" + UPDATE accounts SET (username, email, extra, updated_at) = + (?, ?, ?, {db.timestamp_placeholder}) + WHERE id = ? + """, + ( + username, + email, + json.dumps(dict(extra)) if extra else "{}", + now, + user_id, + ), + ) + + user = await get_user(user_id) + assert user, "Updated account couldn't be retrieved" + return user + + async def get_account( user_id: str, conn: Optional[Connection] = None ) -> Optional[User]: row = await (conn or db).fetchone( - "SELECT id, email, pass as password FROM accounts WHERE id = ?", (user_id,) + """ + SELECT id, email, username, created_at, updated_at, extra + FROM accounts WHERE id = ? + """, + (user_id,), + ) + + user = User(**row) if row else None + if user and row["extra"]: + user.config = UserConfig(**json.loads(row["extra"])) + return user + + +async def delete_accounts_no_wallets( + time_delta: int, + conn: Optional[Connection] = None, +) -> None: + await (conn or db).execute( + f""" + DELETE FROM accounts + WHERE NOT EXISTS ( + SELECT wallets.id FROM wallets WHERE wallets.user = accounts.id + ) AND updated_at < {db.timestamp_placeholder} + """, + (int(time()) - time_delta,), + ) + + +async def get_user_password(user_id: str) -> Optional[str]: + row = await db.fetchone( + "SELECT pass FROM accounts WHERE id = ?", + (user_id,), + ) + if not row: + return None + + return row[0] + + +async def verify_user_password(user_id: str, password: str) -> bool: + existing_password = await get_user_password(user_id) + if not existing_password: + return False + + pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + return pwd_context.verify(password, existing_password) + + +# todo: , conn: Optional[Connection] = None ?? +async def update_user_password(data: UpdateUserPassword) -> Optional[User]: + assert data.password == data.password_repeat, "Passwords do not match." + + # old accounts do not have a pasword + if await get_user_password(data.user_id): + assert data.password_old, "Missing old password" + old_pwd_ok = await verify_user_password(data.user_id, data.password_old) + assert old_pwd_ok, "Invalid credentials." + + pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + now = int(time()) + await db.execute( + f""" + UPDATE accounts SET pass = ?, updated_at = {db.timestamp_placeholder} + WHERE id = ? + """, + ( + pwd_context.hash(data.password), + now, + data.user_id, + ), + ) + + user = await get_user(data.user_id) + assert user, "Updated account couldn't be retrieved" + return user + + +async def get_account_by_username( + username: str, conn: Optional[Connection] = None +) -> Optional[User]: + row = await (conn or db).fetchone( + """ + SELECT id, username, email, created_at, updated_at + FROM accounts WHERE username = ? + """, + (username,), ) return User(**row) if row else None +async def get_account_by_email( + email: str, conn: Optional[Connection] = None +) -> Optional[User]: + row = await (conn or db).fetchone( + """ + SELECT id, username, email, created_at, updated_at + FROM accounts WHERE email = ? + """, + (email,), + ) + + return User(**row) if row else None + + +async def get_account_by_username_or_email( + username_or_email: str, conn: Optional[Connection] = None +) -> Optional[User]: + user = await get_account_by_username(username_or_email, conn) + if not user: + user = await get_account_by_email(username_or_email, conn) + return user + + async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[User]: user = await (conn or db).fetchone( - "SELECT id, email FROM accounts WHERE id = ?", (user_id,) + """ + SELECT id, email, username, pass, extra, created_at, updated_at + FROM accounts WHERE id = ? + """, + (user_id,), ) if user: @@ -86,6 +296,7 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[ return User( id=user["id"], email=user["email"], + username=user["username"], extensions=[ e[0] for e in extensions if User.is_extension_for_user(e[0], user["id"]) ], @@ -93,6 +304,8 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[ admin=user["id"] == settings.super_user or user["id"] in settings.lnbits_admin_users, super_user=user["id"] == settings.super_user, + has_password=True if user["pass"] else False, + config=UserConfig(**json.loads(user["extra"])) if user["extra"] else None, ) @@ -109,6 +322,7 @@ async def add_installed_extension( dict(ext.installed_release) if ext.installed_release else None ), "dependencies": ext.dependencies, + "payments": [dict(p) for p in ext.payments] if ext.payments else None, } version = ext.installed_release.version if ext.installed_release else "" @@ -181,13 +395,15 @@ async def drop_extension_db(*, ext_id: str, conn: Optional[Connection] = None) - ) -async def get_installed_extension(ext_id: str, conn: Optional[Connection] = None): +async def get_installed_extension( + ext_id: str, conn: Optional[Connection] = None +) -> Optional[InstallableExtension]: row = await (conn or db).fetchone( "SELECT * FROM installed_extensions WHERE id = ?", (ext_id,), ) - return dict(row) if row else None + return InstallableExtension.from_row(row) if row else None async def get_installed_extensions( @@ -231,10 +447,11 @@ async def create_wallet( conn: Optional[Connection] = None, ) -> Wallet: wallet_id = uuid4().hex + now = int(time()) await (conn or db).execute( - """ - INSERT INTO wallets (id, name, "user", adminkey, inkey) - VALUES (?, ?, ?, ?, ?) + f""" + INSERT INTO wallets (id, name, "user", adminkey, inkey, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, {db.timestamp_placeholder}, {db.timestamp_placeholder}) """, ( wallet_id, @@ -242,6 +459,8 @@ async def create_wallet( user_id, uuid4().hex, uuid4().hex, + now, + now, ), ) @@ -258,7 +477,10 @@ async def update_wallet( conn: Optional[Connection] = None, ) -> Optional[Wallet]: set_clause = [] - values = [] + values: list = [] + set_clause.append(f"updated_at = {db.timestamp_placeholder}") + now = int(time()) + values.append(now) if name: set_clause.append("name = ?") values.append(name) @@ -280,13 +502,48 @@ async def update_wallet( async def delete_wallet( *, user_id: str, wallet_id: str, conn: Optional[Connection] = None ) -> None: + now = int(time()) await (conn or db).execute( - """ + f""" UPDATE wallets - SET deleted = true + SET deleted = true, updated_at = {db.timestamp_placeholder} WHERE id = ? AND "user" = ? """, - (wallet_id, user_id), + (now, wallet_id, user_id), + ) + + +async def delete_wallet_by_id( + *, wallet_id: str, conn: Optional[Connection] = None +) -> Optional[int]: + now = int(time()) + result = await (conn or db).execute( + f""" + UPDATE wallets + SET deleted = true, updated_at = {db.timestamp_placeholder} + WHERE id = ? + """, + (now, wallet_id), + ) + return result.rowcount + + +async def remove_deleted_wallets(conn: Optional[Connection] = 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, +) -> None: + await (conn or db).execute( + f""" + DELETE FROM wallets + WHERE ( + SELECT COUNT(*) FROM apipayments WHERE wallet = wallets.id + ) = 0 AND updated_at < {db.timestamp_placeholder} + """, + (int(time()) - time_delta,), ) @@ -312,7 +569,8 @@ async def get_wallet_for_key( row = await (conn or db).fetchone( """ SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) - AS balance_msat FROM wallets WHERE adminkey = ? OR inkey = ? + AS balance_msat FROM wallets + WHERE (adminkey = ? OR inkey = ?) AND deleted = false """, (key, key), ) @@ -360,6 +618,7 @@ async def get_standalone_payment( SELECT * FROM apipayments WHERE {clause} + ORDER BY amount LIMIT 1 """, tuple(values), @@ -771,6 +1030,16 @@ async def check_internal_pending( return row["pending"] +async def mark_webhook_sent(payment_hash: str, status: int) -> None: + await db.execute( + """ + UPDATE apipayments SET webhook_status = ? + WHERE hash = ? + """, + (status, payment_hash), + ) + + # balance_check # ------------- @@ -856,6 +1125,8 @@ async def get_admin_settings(is_super_user: bool = False) -> Optional[AdminSetti return None row_dict = dict(sets) row_dict.pop("super_user") + row_dict.pop("auth_all_methods") + admin_settings = AdminSettings( is_super_user=is_super_user, lnbits_allowed_funding_sources=settings.lnbits_allowed_funding_sources, diff --git a/lnbits/core/helpers.py b/lnbits/core/helpers.py index 68ad4f34ac..839d58b71f 100644 --- a/lnbits/core/helpers.py +++ b/lnbits/core/helpers.py @@ -1,6 +1,6 @@ import importlib import re -from typing import Any +from typing import Any, Optional from uuid import UUID import httpx @@ -48,24 +48,64 @@ async def run_migration( await update_migration_version(conn, db_name, version) -async def stop_extension_background_work(ext_id: str, user: str): +async def stop_extension_background_work( + ext_id: str, user: str, access_token: Optional[str] = None +): """ Stop background work for extension (like asyncio.Tasks, WebSockets, etc). - Extensions SHOULD expose a DELETE enpoint at the root level of their API. - This function tries first to call the endpoint using `http` - and if it fails it tries using `https`. + Extensions SHOULD expose a `api_stop()` function and/or a DELETE enpoint + at the root level of their API. """ + stopped = await _stop_extension_background_work(ext_id) + + if not stopped: + # fallback to REST API call + await _stop_extension_background_work_via_api(ext_id, user, access_token) + + +async def _stop_extension_background_work(ext_id) -> bool: + upgrade_hash = settings.extension_upgrade_hash(ext_id) or "" + ext = Extension(ext_id, True, False, upgrade_hash=upgrade_hash) + + try: + logger.info(f"Stopping background work for extension '{ext.module_name}'.") + old_module = importlib.import_module(ext.module_name) + + # Extensions must expose an `{ext_id}_stop()` function at the module level + # The `api_stop()` function is for backwards compatibility (will be deprecated) + stop_fns = [f"{ext_id}_stop", "api_stop"] + stop_fn_name = next((fn for fn in stop_fns if hasattr(old_module, fn)), None) + assert stop_fn_name, "No stop function found for '{ext.module_name}'" + + await getattr(old_module, stop_fn_name)() + + logger.info(f"Stopped background work for extension '{ext.module_name}'.") + except Exception as ex: + logger.warning(f"Failed to stop background work for '{ext.module_name}'.") + logger.warning(ex) + return False + + return True + + +async def _stop_extension_background_work_via_api(ext_id, user, access_token): + logger.info( + f"Stopping background work for extension '{ext_id}' using the REST API." + ) async with httpx.AsyncClient() as client: try: url = f"http://{settings.host}:{settings.port}/{ext_id}/api/v1?usr={user}" - await client.delete(url) + headers = ( + {"Authorization": "Bearer " + access_token} if access_token else None + ) + resp = await client.delete(url=url, headers=headers) + resp.raise_for_status() + logger.info(f"Stopped background work for extension '{ext_id}'.") except Exception as ex: + logger.warning( + f"Failed to stop background work for '{ext_id}' using the REST API." + ) logger.warning(ex) - try: - # try https - url = f"https://{settings.host}:{settings.port}/{ext_id}/api/v1?usr={user}" - except Exception as ex: - logger.warning(ex) def to_valid_user_id(user_id: str) -> UUID: diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index 9cc8891335..3f511c369c 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -1,4 +1,5 @@ import datetime +from time import time from loguru import logger from sqlalchemy.exc import OperationalError @@ -393,3 +394,80 @@ async def m015_create_push_notification_subscriptions_table(db): ); """ ) + + +async def m016_add_username_column_to_accounts(db): + """ + Adds username column to accounts. + """ + try: + await db.execute("ALTER TABLE accounts ADD COLUMN username TEXT") + await db.execute("ALTER TABLE accounts ADD COLUMN extra TEXT") + except OperationalError: + pass + + +async def m017_add_timestamp_columns_to_accounts_and_wallets(db): + """ + Adds created_at and updated_at column to accounts and wallets. + """ + try: + await db.execute( + "ALTER TABLE accounts " + f"ADD COLUMN created_at TIMESTAMP DEFAULT {db.timestamp_column_default}" + ) + await db.execute( + "ALTER TABLE accounts " + f"ADD COLUMN updated_at TIMESTAMP DEFAULT {db.timestamp_column_default}" + ) + await db.execute( + "ALTER TABLE wallets " + f"ADD COLUMN created_at TIMESTAMP DEFAULT {db.timestamp_column_default}" + ) + await db.execute( + "ALTER TABLE wallets " + f"ADD COLUMN updated_at TIMESTAMP DEFAULT {db.timestamp_column_default}" + ) + + # # set their wallets created_at with the first payment + # await db.execute( + # """ + # UPDATE wallets SET created_at = ( + # SELECT time FROM apipayments + # WHERE apipayments.wallet = wallets.id + # ORDER BY time ASC LIMIT 1 + # ) + # """ + # ) + + # # then set their accounts created_at with the wallet + # await db.execute( + # """ + # UPDATE accounts SET created_at = ( + # SELECT created_at FROM wallets + # WHERE wallets.user = accounts.id + # ORDER BY created_at ASC LIMIT 1 + # ) + # """ + # ) + + # set all to now where they are null + now = int(time()) + await db.execute( + f""" + UPDATE wallets SET created_at = {db.timestamp_placeholder} + WHERE created_at IS NULL + """, + (now,), + ) + await db.execute( + f""" + UPDATE accounts SET created_at = {db.timestamp_placeholder} + WHERE created_at IS NULL + """, + (now,), + ) + + except OperationalError as exc: + logger.error(f"Migration 17 failed: {exc}") + pass diff --git a/lnbits/core/models.py b/lnbits/core/models.py index d768b92020..68c6c3e420 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -10,26 +10,31 @@ from ecdsa import SECP256k1, SigningKey from fastapi import Query -from lnurl import encode as lnurl_encode from loguru import logger from pydantic import BaseModel from lnbits.db import Connection, FilterModel, FromRowModel from lnbits.helpers import url_for +from lnbits.lnurl import encode as lnurl_encode from lnbits.settings import settings from lnbits.wallets import get_wallet_class -from lnbits.wallets.base import PaymentStatus +from lnbits.wallets.base import PaymentPendingStatus, PaymentStatus -class Wallet(BaseModel): +class BaseWallet(BaseModel): id: str name: str - user: str adminkey: str inkey: str - currency: Optional[str] balance_msat: int + + +class Wallet(BaseWallet): + user: str + currency: Optional[str] deleted: bool + created_at: Optional[int] = None + updated_at: Optional[int] = None @property def balance(self) -> int: @@ -79,14 +84,31 @@ class WalletTypeInfo: wallet: Wallet +class UserConfig(BaseModel): + email_verified: Optional[bool] = False + first_name: Optional[str] = None + last_name: Optional[str] = None + display_name: Optional[str] = None + picture: Optional[str] = None + # Auth provider, possible values: + # - "env": the user was created automatically by the system + # - "lnbits": the user was created via register form (username/pass or user_id only) + # - "google | github | ...": the user was created using an SSO provider + provider: Optional[str] = "lnbits" # auth provider + + class User(BaseModel): id: str email: Optional[str] = None + username: Optional[str] = None extensions: List[str] = [] wallets: List[Wallet] = [] - password: Optional[str] = None admin: bool = False super_user: bool = False + has_password: bool = False + config: Optional[UserConfig] = None + created_at: Optional[int] = None + updated_at: Optional[int] = None @property def wallet_ids(self) -> List[str]: @@ -107,6 +129,43 @@ def is_extension_for_user(cls, ext: str, user: str) -> bool: return False +class CreateUser(BaseModel): + email: Optional[str] = Query(default=None) + username: str = Query(default=..., min_length=2, max_length=20) + password: str = Query(default=..., min_length=8, max_length=50) + password_repeat: str = Query(default=..., min_length=8, max_length=50) + + +class UpdateUser(BaseModel): + user_id: str + email: Optional[str] = Query(default=None) + username: Optional[str] = Query(default=..., min_length=2, max_length=20) + config: Optional[UserConfig] = None + + +class UpdateUserPassword(BaseModel): + user_id: str + password: str = Query(default=..., min_length=8, max_length=50) + password_repeat: str = Query(default=..., min_length=8, max_length=50) + password_old: Optional[str] = Query(default=None, min_length=8, max_length=50) + username: Optional[str] = Query(default=..., min_length=2, max_length=20) + + +class UpdateSuperuserPassword(BaseModel): + username: str = Query(default=..., min_length=2, max_length=20) + password: str = Query(default=..., min_length=8, max_length=50) + password_repeat: str = Query(default=..., min_length=8, max_length=50) + + +class LoginUsr(BaseModel): + usr: str + + +class LoginUsernamePassword(BaseModel): + username: str + password: str + + class Payment(FromRowModel): checking_id: str pending: bool @@ -199,7 +258,7 @@ async def check_status( conn: Optional[Connection] = None, ) -> PaymentStatus: if self.is_uncheckable: - return PaymentStatus(None) + return PaymentPendingStatus() logger.debug( f"Checking {'outgoing' if self.is_out else 'incoming'} " @@ -221,11 +280,18 @@ async def check_status( f"expired {expiration_date}" ) await self.delete(conn) + # wait at least 15 minutes before deleting failed outgoing payments elif self.is_out and status.failed: - logger.warning( - f"Deleting outgoing failed payment {self.checking_id}: {status}" - ) - await self.delete(conn) + if self.time + 900 < int(time.time()): + logger.warning( + f"Deleting outgoing failed payment {self.checking_id}: {status}" + ) + await self.delete(conn) + else: + logger.warning( + f"Tried to delete outgoing payment {self.checking_id}: " + "skipping because it's not old enough" + ) elif not status.pending: logger.info( f"Marking '{'in' if self.is_in else 'out'}' " @@ -275,8 +341,12 @@ def from_row(cls, row: Row): return cls(wallet=row["wallet"], service=row["service"], url=row["url"]) +def _do_nothing(*_): + pass + + class CoreAppExtra: - register_new_ext_routes: Callable + register_new_ext_routes: Callable = _do_nothing register_new_ratelimiter: Callable @@ -312,6 +382,7 @@ class CreateLnurl(BaseModel): amount: int comment: Optional[str] = None description: Optional[str] = None + unit: Optional[str] = None class CreateInvoice(BaseModel): diff --git a/lnbits/core/services.py b/lnbits/core/services.py index a944a42c76..9edeb0f477 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -1,6 +1,7 @@ import asyncio import datetime import json +import time from io import BytesIO from pathlib import Path from typing import Dict, List, Optional, Tuple, TypedDict @@ -11,8 +12,6 @@ from bolt11 import decode as bolt11_decode from cryptography.hazmat.primitives import serialization from fastapi import Depends, WebSocket -from lnurl import LnurlErrorResponse -from lnurl import decode as decode_lnurl from loguru import logger from py_vapid import Vapid from py_vapid.utils import b64urlencode @@ -21,6 +20,8 @@ from lnbits.db import Connection from lnbits.decorators import WalletTypeInfo, require_admin_key from lnbits.helpers import url_for +from lnbits.lnurl import LnurlErrorResponse +from lnbits.lnurl import decode as decode_lnurl from lnbits.settings import ( EditableSettings, SuperSettings, @@ -30,7 +31,12 @@ ) from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat from lnbits.wallets import FAKE_WALLET, get_wallet_class, set_wallet_class -from lnbits.wallets.base import PaymentResponse, PaymentStatus +from lnbits.wallets.base import ( + PaymentPendingStatus, + PaymentResponse, + PaymentStatus, + PaymentSuccessStatus, +) from .crud import ( check_internal, @@ -41,6 +47,7 @@ create_wallet, delete_wallet_payment, get_account, + get_payments, get_standalone_payment, get_super_settings, get_total_balance, @@ -52,7 +59,7 @@ update_super_user, ) from .helpers import to_valid_user_id -from .models import Payment, Wallet +from .models import Payment, UserConfig, Wallet class PaymentFailure(Exception): @@ -118,8 +125,9 @@ async def create_invoice( if not amount > 0: raise InvoiceFailure("Amountless invoices not supported.") - if await get_wallet(wallet_id, conn=conn) is None: - raise InvoiceFailure("Wallet does not exist.") + user_wallet = await get_wallet(wallet_id, conn=conn) + if not user_wallet: + raise InvoiceFailure(f"Could not fetch wallet '{wallet_id}'.") invoice_memo = None if description_hash else memo @@ -130,6 +138,14 @@ async def create_invoice( amount, wallet_id, currency=currency, extra=extra, conn=conn ) + if settings.is_wallet_max_balance_exceeded( + user_wallet.balance_msat / 1000 + amount_sat + ): + raise InvoiceFailure( + f"Wallet balance cannot exceed " + f"{settings.lnbits_wallet_limit_max_balance} sats." + ) + ok, checking_id, payment_request, error_message = await wallet.create_invoice( amount=amount_sat, memo=invoice_memo, @@ -178,23 +194,22 @@ async def pay_invoice( If the payment is still in flight, we hope that some other process will regularly check for the payment. """ - invoice = bolt11_decode(payment_request) + try: + invoice = bolt11_decode(payment_request) + except Exception: + raise InvoiceFailure("Bolt11 decoding failed.") if not invoice.amount_msat or not invoice.amount_msat > 0: - raise ValueError("Amountless invoices not supported.") + raise InvoiceFailure("Amountless invoices not supported.") if max_sat and invoice.amount_msat > max_sat * 1000: - raise ValueError("Amount in invoice is too high.") + raise InvoiceFailure("Amount in invoice is too high.") + + await check_wallet_limits(wallet_id, conn, invoice.amount_msat) - fee_reserve_msat = fee_reserve(invoice.amount_msat) async with db.reuse_conn(conn) if conn else db.connect() as conn: temp_id = invoice.payment_hash internal_id = f"internal_{invoice.payment_hash}" - if invoice.amount_msat == 0: - raise ValueError("Amountless invoices not supported.") - if max_sat and invoice.amount_msat > max_sat * 1000: - raise ValueError("Amount in invoice is too high.") - _, extra = await calculate_fiat_amounts( invoice.amount_msat / 1000, wallet_id, extra=extra, conn=conn ) @@ -228,6 +243,9 @@ class PaymentKwargs(TypedDict): # (pending only) internal_checking_id = await check_internal(invoice.payment_hash, conn=conn) if internal_checking_id: + fee_reserve_total_msat = fee_reserve_total( + invoice.amount_msat, internal=True + ) # perform additional checks on the internal payment # the payment hash is not enough to make sure that this is the same invoice internal_invoice = await get_standalone_payment( @@ -244,19 +262,22 @@ class PaymentKwargs(TypedDict): # create a new payment from this wallet new_payment = await create_payment( checking_id=internal_id, - fee=0, + fee=0 + abs(fee_reserve_total_msat), pending=False, conn=conn, **payment_kwargs, ) else: + fee_reserve_total_msat = fee_reserve_total( + invoice.amount_msat, internal=False + ) logger.debug(f"creating temporary payment with id {temp_id}") # create a temporary payment here so we can check if # the balance is enough in the next step try: new_payment = await create_payment( checking_id=temp_id, - fee=-fee_reserve_msat, + fee=-abs(fee_reserve_total_msat), conn=conn, **payment_kwargs, ) @@ -270,14 +291,18 @@ class PaymentKwargs(TypedDict): assert wallet, "Wallet for balancecheck could not be fetched" if wallet.balance_msat < 0: logger.debug("balance is too low, deleting temporary payment") - if not internal_checking_id and wallet.balance_msat > -fee_reserve_msat: + if ( + not internal_checking_id + and wallet.balance_msat > -fee_reserve_total_msat + ): raise PaymentFailure( - f"You must reserve at least ({round(fee_reserve_msat/1000)} sat) to" - " cover potential routing fees." + f"You must reserve at least ({round(fee_reserve_total_msat/1000)}" + " sat) to cover potential routing fees." ) raise PermissionError("Insufficient balance.") if internal_checking_id: + service_fee_msat = service_fee(invoice.amount_msat, internal=True) logger.debug(f"marking temporary payment as not pending {internal_checking_id}") # mark the invoice from the other side as not pending anymore # so the other side only has access to his new money when we are sure @@ -294,6 +319,8 @@ class PaymentKwargs(TypedDict): logger.debug(f"enqueuing internal invoice {internal_checking_id}") await internal_invoice_queue.put(internal_checking_id) else: + fee_reserve_msat = fee_reserve(invoice.amount_msat, internal=False) + service_fee_msat = service_fee(invoice.amount_msat, internal=False) logger.debug(f"backend: sending payment {temp_id}") # actually pay the external invoice WALLET = get_wallet_class() @@ -315,7 +342,10 @@ class PaymentKwargs(TypedDict): await update_payment_details( checking_id=temp_id, pending=payment.ok is not True, - fee=payment.fee_msat, + fee=-( + abs(payment.fee_msat if payment.fee_msat else 0) + + abs(service_fee_msat) + ), preimage=payment.preimage, new_checking_id=payment.checking_id, conn=conn, @@ -343,9 +373,73 @@ class PaymentKwargs(TypedDict): f" database: {temp_id}" ) + # credit service fee wallet + if settings.lnbits_service_fee_wallet and service_fee_msat: + new_payment = await create_payment( + wallet_id=settings.lnbits_service_fee_wallet, + fee=0, + amount=abs(service_fee_msat), + memo="Service fee", + checking_id="service_fee" + temp_id, + payment_request=payment_request, + payment_hash=invoice.payment_hash, + pending=False, + ) return invoice.payment_hash +async def check_wallet_limits(wallet_id, conn, amount_msat): + await check_time_limit_between_transactions(conn, wallet_id) + await check_wallet_daily_withdraw_limit(conn, wallet_id, amount_msat) + + +async def check_time_limit_between_transactions(conn, wallet_id): + limit = settings.lnbits_wallet_limit_secs_between_trans + if not limit or limit <= 0: + return + + payments = await get_payments( + since=int(time.time()) - limit, + wallet_id=wallet_id, + limit=1, + conn=conn, + ) + + if len(payments) == 0: + return + + raise ValueError( + f"The time limit of {limit} seconds between payments has been reached." + ) + + +async def check_wallet_daily_withdraw_limit(conn, wallet_id, amount_msat): + limit = settings.lnbits_wallet_limit_daily_max_withdraw + if not limit or limit <= 0: + return + + payments = await get_payments( + since=int(time.time()) - 60 * 60 * 24, + outgoing=True, + wallet_id=wallet_id, + limit=1, + conn=conn, + ) + if len(payments) == 0: + return + + total = 0 + for pay in payments: + total += pay.amount + total = total - amount_msat + if limit * 1000 + total < 0: + raise ValueError( + "Daily withdrawal limit of " + + str(settings.lnbits_wallet_limit_daily_max_withdraw) + + " sats reached." + ) + + async def redeem_lnurl_withdraw( wallet_id: str, lnurl_request: str, @@ -359,7 +453,8 @@ async def redeem_lnurl_withdraw( res = {} - async with httpx.AsyncClient() as client: + headers = {"User-Agent": settings.user_agent} + async with httpx.AsyncClient(headers=headers) as client: lnurl = decode_lnurl(lnurl_request) r = await client.get(str(lnurl)) res = r.json() @@ -393,7 +488,8 @@ async def redeem_lnurl_withdraw( except Exception: pass - async with httpx.AsyncClient() as client: + headers = {"User-Agent": settings.user_agent} + async with httpx.AsyncClient(headers=headers) as client: try: await client.get(res["callback"], params=params) except Exception: @@ -456,7 +552,8 @@ def encode_strict_der(r: int, s: int, order: int): sig = key.sign_digest_deterministic(k1, sigencode=encode_strict_der) - async with httpx.AsyncClient() as client: + headers = {"User-Agent": settings.user_agent} + async with httpx.AsyncClient(headers=headers) as client: assert key.verifying_key, "LNURLauth verifying_key does not exist" r = await client.get( callback, @@ -485,10 +582,10 @@ async def check_transaction_status( wallet_id, payment_hash, conn=conn ) if not payment: - return PaymentStatus(None) + return PaymentPendingStatus() if not payment.pending: # note: before, we still checked the status of the payment again - return PaymentStatus(True, fee_msat=payment.fee) + return PaymentSuccessStatus(fee_msat=payment.fee) status: PaymentStatus = await payment.check_status() return status @@ -496,12 +593,33 @@ async def check_transaction_status( # WARN: this same value must be used for balance check and passed to # WALLET.pay_invoice(), it may cause a vulnerability if the values differ -def fee_reserve(amount_msat: int) -> int: +def fee_reserve(amount_msat: int, internal: bool = False) -> int: + if internal: + return 0 reserve_min = settings.lnbits_reserve_fee_min reserve_percent = settings.lnbits_reserve_fee_percent return max(int(reserve_min), int(amount_msat * reserve_percent / 100.0)) +def service_fee(amount_msat: int, internal: bool = False) -> int: + service_fee_percent = settings.lnbits_service_fee + fee_max = settings.lnbits_service_fee_max * 1000 + if settings.lnbits_service_fee_wallet: + if internal and settings.lnbits_service_fee_ignore_internal: + return 0 + fee_percentage = int(amount_msat / 100 * service_fee_percent) + if fee_max > 0 and fee_percentage > fee_max: + return fee_max + else: + return fee_percentage + else: + return 0 + + +def fee_reserve_total(amount_msat: int, internal: bool = False) -> int: + return fee_reserve(amount_msat, internal) + service_fee(amount_msat, internal) + + async def send_payment_notification(wallet: Wallet, payment: Payment): await websocketUpdater( wallet.id, @@ -561,6 +679,10 @@ async def check_admin_settings(): ): send_admin_user_to_saas() + account = await get_account(settings.super_user) + if account and account.config and account.config.provider == "env": + settings.first_install = True + logger.success( "✔️ Admin UI is enabled. run `poetry run lnbits-cli superuser` " "to get the superuser." @@ -592,11 +714,14 @@ async def check_webpush_settings(): def update_cached_settings(sets_dict: dict): for key, value in sets_dict.items(): - if key not in readonly_variables: - try: - setattr(settings, key, value) - except Exception: - logger.warning(f"Failed overriding setting: {key}, value: {value}") + if key in readonly_variables: + continue + if key not in settings.dict().keys(): + continue + try: + setattr(settings, key, value) + except Exception: + logger.warning(f"Failed overriding setting: {key}, value: {value}") if "super_user" in sets_dict: setattr(settings, "super_user", sets_dict["super_user"]) @@ -606,7 +731,9 @@ async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings if super_user: account = await get_account(super_user) if not account: - account = await create_account(user_id=super_user) + account = await create_account( + user_id=super_user, user_config=UserConfig(provider="env") + ) if not account.wallets or len(account.wallets) == 0: await create_wallet(user_id=account.id) diff --git a/lnbits/core/sso/keycloak.py b/lnbits/core/sso/keycloak.py new file mode 100644 index 0000000000..afac9a491c --- /dev/null +++ b/lnbits/core/sso/keycloak.py @@ -0,0 +1,37 @@ +"""Keycloak SSO Login Helper +""" + +from typing import Optional + +import httpx +from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase + + +class KeycloakSSO(SSOBase): + """Class providing login via Keycloak OAuth""" + + provider = "keycloak" + scope = ["openid", "email", "profile"] + discovery_url = "" + + async def openid_from_response( + self, response: dict, session: Optional["httpx.AsyncClient"] = None + ) -> OpenID: + """Return OpenID from user information provided by Keycloak""" + return OpenID( + email=response.get("email", ""), + provider=self.provider, + id=response.get("sub"), + first_name=response.get("given_name"), + last_name=response.get("family_name"), + display_name=response.get("name"), + picture=response.get("picture"), + ) + + async def get_discovery_document(self) -> DiscoveryDocument: + """Get document containing handy urls""" + async with httpx.AsyncClient() as session: + response = await session.get(self.discovery_url) + content = response.json() + + return content diff --git a/lnbits/core/static/extension.png b/lnbits/core/static/extension.png deleted file mode 100644 index 78378fd767..0000000000 Binary files a/lnbits/core/static/extension.png and /dev/null differ diff --git a/lnbits/core/static/js/index.js b/lnbits/core/static/js/index.js deleted file mode 100644 index 314a1f7e7a..0000000000 --- a/lnbits/core/static/js/index.js +++ /dev/null @@ -1,36 +0,0 @@ -new Vue({ - el: '#vue', - mixins: [windowMixin], - data: function () { - return { - disclaimerDialog: { - show: false, - data: {}, - description: '' - }, - walletName: '' - } - }, - computed: { - formatDescription() { - return LNbits.utils.convertMarkdown(this.description) - } - }, - methods: { - createWallet: function () { - LNbits.api.createAccount(this.walletName).then(res => { - window.location = '/wallet?usr=' + res.data.user + '&wal=' + res.data.id - }) - }, - processing: function () { - this.$q.notify({ - timeout: 0, - message: 'Processing...', - icon: null - }) - } - }, - created() { - this.description = SITE_DESCRIPTION - } -}) diff --git a/lnbits/core/tasks.py b/lnbits/core/tasks.py index 08f7a1e96b..c35a5ad3ff 100644 --- a/lnbits/core/tasks.py +++ b/lnbits/core/tasks.py @@ -8,8 +8,8 @@ get_balance_notify, get_wallet, get_webpush_subscriptions_for_user, + mark_webhook_sent, ) -from lnbits.core.db import db from lnbits.core.models import Payment from lnbits.core.services import ( get_balance_delta, @@ -17,29 +17,16 @@ switch_to_voidwallet, ) from lnbits.settings import get_wallet_class, settings -from lnbits.tasks import ( - SseListenersDict, - create_permanent_task, - create_task, - register_invoice_listener, - send_push_notification, -) +from lnbits.tasks import send_push_notification -api_invoice_listeners: Dict[str, asyncio.Queue] = SseListenersDict( - "api_invoice_listeners" -) +api_invoice_listeners: Dict[str, asyncio.Queue] = {} -def register_killswitch(): +async def killswitch_task(): """ - Registers a killswitch which will check lnbits-status repository for a signal from + killswitch will check lnbits-status repository for a signal from LNbits and will switch to VoidWallet if the killswitch is triggered. """ - logger.debug("Starting killswitch task") - create_permanent_task(killswitch_task) - - -async def killswitch_task(): while True: WALLET = get_wallet_class() if settings.lnbits_killswitch and WALLET.__class__.__name__ != "VoidWallet": @@ -54,7 +41,7 @@ async def killswitch_task(): "Switching to VoidWallet. Killswitch triggered." ) await switch_to_voidwallet() - except (httpx.ConnectError, httpx.RequestError): + except (httpx.RequestError, httpx.HTTPStatusError): logger.error( "Cannot fetch lnbits status manifest." f" {settings.lnbits_status_manifest}" @@ -62,17 +49,11 @@ async def killswitch_task(): await asyncio.sleep(settings.lnbits_killswitch_interval * 60) -async def register_watchdog(): +async def watchdog_task(): """ Registers a watchdog which will check lnbits balance and nodebalance and will switch to VoidWallet if the watchdog delta is reached. """ - # TODO: implement watchdog properly - # logger.debug("Starting watchdog task") - # create_permanent_task(watchdog_task) - - -async def watchdog_task(): while True: WALLET = get_wallet_class() if settings.lnbits_watchdog and WALLET.__class__.__name__ != "VoidWallet": @@ -87,46 +68,46 @@ async def watchdog_task(): await asyncio.sleep(settings.lnbits_watchdog_interval * 60) -def register_task_listeners(): - """ - Registers an invoice listener queue for the core tasks. Incoming payments in this - queue will eventually trigger the signals sent to all other extensions - and fulfill other core tasks such as dispatching webhooks. - """ - invoice_paid_queue = asyncio.Queue(5) - # we register invoice_paid_queue to receive all incoming invoices - register_invoice_listener(invoice_paid_queue, "core/tasks.py") - # register a worker that will react to invoices - create_task(wait_for_paid_invoices(invoice_paid_queue)) - - async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue): """ - This worker dispatches events to all extensions, - dispatches webhooks and balance notifys. + This task dispatches events to all api_invoice_listeners, + webhooks, push notifications and balance notifications. """ while True: payment = await invoice_paid_queue.get() logger.trace("received invoice paid event") - # send information to sse channel + # dispatch api_invoice_listeners await dispatch_api_invoice_listeners(payment) + # payment notification wallet = await get_wallet(payment.wallet_id) if wallet: await send_payment_notification(wallet, payment) # dispatch webhook if payment.webhook and not payment.webhook_status: await dispatch_webhook(payment) - # dispatch balance_notify url = await get_balance_notify(payment.wallet_id) if url: - async with httpx.AsyncClient() as client: + headers = {"User-Agent": settings.user_agent} + async with httpx.AsyncClient(headers=headers) as client: try: r = await client.post(url, timeout=4) - await mark_webhook_sent(payment, r.status_code) - except (httpx.ConnectError, httpx.RequestError): - pass + r.raise_for_status() + await mark_webhook_sent(payment.payment_hash, r.status_code) + except httpx.HTTPStatusError as exc: + status_code = exc.response.status_code + await mark_webhook_sent(payment.payment_hash, status_code) + logger.warning( + f"balance_notify returned a bad status_code: {status_code} " + f"while requesting {exc.request.url!r}." + ) + logger.warning(exc) + except httpx.RequestError as exc: + await mark_webhook_sent(payment.payment_hash, -1) + logger.warning(f"Could not send balance_notify to {url}") + logger.warning(exc) + # dispatch push notification await send_payment_push_notification(payment) @@ -136,10 +117,12 @@ async def dispatch_api_invoice_listeners(payment: Payment): """ for chan_name, send_channel in api_invoice_listeners.items(): try: - logger.debug(f"sending invoice paid event to {chan_name}") + logger.debug(f"api invoice listener: sending paid event to {chan_name}") send_channel.put_nowait(payment) except asyncio.QueueFull: - logger.error(f"removing sse listener {send_channel}:{chan_name}") + logger.error( + f"api invoice listener: QueueFull, removing {send_channel}:{chan_name}" + ) api_invoice_listeners.pop(chan_name) @@ -150,25 +133,24 @@ async def dispatch_webhook(payment: Payment): logger.debug("sending webhook", payment.webhook) if not payment.webhook: - return await mark_webhook_sent(payment, -1) + return await mark_webhook_sent(payment.payment_hash, -1) - async with httpx.AsyncClient() as client: + headers = {"User-Agent": settings.user_agent} + async with httpx.AsyncClient(headers=headers) as client: data = payment.dict() try: r = await client.post(payment.webhook, json=data, timeout=40) - await mark_webhook_sent(payment, r.status_code) - except (httpx.ConnectError, httpx.RequestError): - await mark_webhook_sent(payment, -1) - - -async def mark_webhook_sent(payment: Payment, status: int) -> None: - await db.execute( - """ - UPDATE apipayments SET webhook_status = ? - WHERE hash = ? - """, - (status, payment.payment_hash), - ) + r.raise_for_status() + await mark_webhook_sent(payment.payment_hash, r.status_code) + except httpx.HTTPStatusError as exc: + await mark_webhook_sent(payment.payment_hash, exc.response.status_code) + logger.warning( + f"webhook returned a bad status_code: {exc.response.status_code} " + f"while requesting {exc.request.url!r}." + ) + except httpx.RequestError: + await mark_webhook_sent(payment.payment_hash, -1) + logger.warning(f"Could not send webhook to {payment.webhook}") async def send_payment_push_notification(payment: Payment): @@ -186,6 +168,8 @@ async def send_payment_push_notification(payment: Payment): body += f"\r\n{payment.memo}" for subscription in subscriptions: + # todo: review permissions when user-id-only not allowed + # todo: replace all this logic with websockets? url = ( f"https://{subscription.host}/wallet?usr={wallet.user}&wal={wallet.id}" ) diff --git a/lnbits/core/templates/admin/_tab_funding.html b/lnbits/core/templates/admin/_tab_funding.html index 3aa011d14a..bd909073ab 100644 --- a/lnbits/core/templates/admin/_tab_funding.html +++ b/lnbits/core/templates/admin/_tab_funding.html @@ -7,21 +7,21 @@
Wallets Management

Funding Source Info

    - {%raw%} -
  • Funding Source: {{settings.lnbits_backend_wallet_class}}
  • -
  • - Node Balance: {{(auditData.node_balance_msats / - 1000).toLocaleString()}} sats -
  • -
  • - LNbits Balance: {{(auditData.lnbits_balance_msats / - 1000).toLocaleString()}} sats -
  • -
  • - Reserve Percent: {{(auditData.node_balance_msats / - auditData.lnbits_balance_msats * 100).toFixed(2)}} % -
  • - {%endraw%} +
  • +
  • +
  • +

diff --git a/lnbits/core/templates/admin/_tab_security.html b/lnbits/core/templates/admin/_tab_security.html index 0b62f1ae02..0f77cf528f 100644 --- a/lnbits/core/templates/admin/_tab_security.html +++ b/lnbits/core/templates/admin/_tab_security.html @@ -1,7 +1,122 @@ -
+
Authentication
+
+
+ + +
+
+ +
+
+
+ + Google Auth + +
+
+ + +
+
+ + +
+
+
+ + GitHub Auth + +
+
+ + +
+
+ + +
+
+
+ + Keycloak Auth + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
@@ -16,7 +131,7 @@
style="padding: 10px; color: #fafafa; height: 320px" > {% raw %}{{ log }}{% endraw %}

@@ -51,7 +166,6 @@
>
- {%raw%} @remove="removeBlockedIPs(blocked_ip)" color="primary" text-color="white" - > - {{ blocked_ip }} - - {%endraw%} + v-text="blocked_ip" + >

@@ -83,7 +195,6 @@
>
- {%raw%} @remove="removeAllowedIPs(allowed_ip)" color="primary" text-color="white" - > - {{ allowed_ip }} - - {%endraw%} + v-text="allowed_ip" + >

+

@@ -121,6 +231,36 @@
+ +
+

+
+
+ +
+
+ +
+
+ +
+
+

diff --git a/lnbits/core/templates/admin/_tab_security_notifications.html b/lnbits/core/templates/admin/_tab_security_notifications.html index 03e5bad05f..1683eda85a 100644 --- a/lnbits/core/templates/admin/_tab_security_notifications.html +++ b/lnbits/core/templates/admin/_tab_security_notifications.html @@ -1,4 +1,3 @@ -{% raw %} @@ -8,6 +7,7 @@ class="q-btn" color="white" target="_blank" + rel="noopener noreferrer" href="https://github.com/lnbits/lnbits/releases" v-text="$t('releases')" > @@ -29,9 +29,12 @@ - - {% endraw %}
- {% raw %}
@@ -254,19 +250,21 @@
-
- {{ props.row.alias }} -
+
- #{{ props.row.color }} -
- {{ shortenNodeId(props.row.id) }} -
+
- - {% endraw %}
diff --git a/lnbits/core/templates/node/_tab_dashboard.html b/lnbits/core/templates/node/_tab_dashboard.html index 3fa8ba0226..e045fc2678 100644 --- a/lnbits/core/templates/node/_tab_dashboard.html +++ b/lnbits/core/templates/node/_tab_dashboard.html @@ -1,13 +1,12 @@ - {% raw %}
@@ -65,6 +64,5 @@ >
- {% endraw %} diff --git a/lnbits/core/templates/node/_tab_transactions.html b/lnbits/core/templates/node/_tab_transactions.html index 2743ff2787..40b3fb4d23 100644 --- a/lnbits/core/templates/node/_tab_transactions.html +++ b/lnbits/core/templates/node/_tab_transactions.html @@ -3,7 +3,6 @@ - {% raw %}
:
- {{ transactionDetailsDialog.data.payment_hash }} +
:
- {{ transactionDetailsDialog.data.preimage }} +
- {% endraw %} @@ -102,7 +102,6 @@ :filter="paymentsTable.filter" @request="getPayments" > - {% raw %} - {% endraw %} @@ -266,7 +263,6 @@ :filter="invoiceTable.filter" @request="getInvoices" > - {% raw %} - - {% endraw %} diff --git a/lnbits/core/templates/node/index.html b/lnbits/core/templates/node/index.html index be6c71f04d..5ae501cfac 100644 --- a/lnbits/core/templates/node/index.html +++ b/lnbits/core/templates/node/index.html @@ -42,7 +42,7 @@
{% endblock %} {% block scripts %} {{ window_vars(user) }} - + + + {% endfor %} diff --git a/lnbits/templates/macros.jinja b/lnbits/templates/macros.jinja index 5f3c96f06c..5daaed3d26 100644 --- a/lnbits/templates/macros.jinja +++ b/lnbits/templates/macros.jinja @@ -1,6 +1,9 @@ {% macro window_vars(user, wallet) -%} + {% endfor %} {% block scripts %}{% endblock %} diff --git a/lnbits/utils/crypto.py b/lnbits/utils/crypto.py new file mode 100644 index 0000000000..f60c478326 --- /dev/null +++ b/lnbits/utils/crypto.py @@ -0,0 +1,75 @@ +import base64 +import getpass +from hashlib import md5 + +from Cryptodome import Random +from Cryptodome.Cipher import AES + +BLOCK_SIZE = 16 + + +class AESCipher: + """This class is compatible with crypto-js/aes.js + + Encrypt and decrypt in Javascript using: + import AES from "crypto-js/aes.js"; + import Utf8 from "crypto-js/enc-utf8.js"; + AES.encrypt(decrypted, password).toString() + AES.decrypt(encrypted, password).toString(Utf8); + + """ + + def __init__(self, key=None, description=""): + self.key = key + self.description = description + " " + + def pad(self, data): + length = BLOCK_SIZE - (len(data) % BLOCK_SIZE) + return data + (chr(length) * length).encode() + + def unpad(self, data): + return data[: -(data[-1] if isinstance(data[-1], int) else ord(data[-1]))] + + @property + def passphrase(self): + passphrase = self.key if self.key is not None else None + if passphrase is None: + passphrase = getpass.getpass(f"Enter {self.description}password:") + return passphrase + + def bytes_to_key(self, data, salt, output=48): + # extended from https://gist.github.com/gsakkis/4546068 + assert len(salt) == 8, len(salt) + data += salt + key = md5(data).digest() + final_key = key + while len(final_key) < output: + key = md5(key + data).digest() + final_key += key + return final_key[:output] + + def decrypt(self, encrypted: str) -> str: # type: ignore + """Decrypts a string using AES-256-CBC.""" + passphrase = self.passphrase + encrypted = base64.b64decode(encrypted) # type: ignore + assert encrypted[0:8] == b"Salted__" + salt = encrypted[8:16] + key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16) + key = key_iv[:32] + iv = key_iv[32:] + aes = AES.new(key, AES.MODE_CBC, iv) + try: + return self.unpad(aes.decrypt(encrypted[16:])).decode() # type: ignore + except UnicodeDecodeError: + raise ValueError("Wrong passphrase") + + def encrypt(self, message: bytes) -> str: + passphrase = self.passphrase + salt = Random.new().read(8) + key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16) + key = key_iv[:32] + iv = key_iv[32:] + aes = AES.new(key, AES.MODE_CBC, iv) + return base64.b64encode( + b"Salted__" + salt + aes.encrypt(self.pad(message)) + ).decode() diff --git a/lnbits/utils/exchange_rates.py b/lnbits/utils/exchange_rates.py index 89567535d2..7ea0d09dac 100644 --- a/lnbits/utils/exchange_rates.py +++ b/lnbits/utils/exchange_rates.py @@ -4,6 +4,7 @@ import httpx from loguru import logger +from lnbits.settings import settings from lnbits.utils.cache import cache currencies = { @@ -175,6 +176,16 @@ } +def allowed_currencies(): + if len(settings.lnbits_allowed_currencies) > 0: + return [ + item + for item in currencies.keys() + if item.upper() in settings.lnbits_allowed_currencies + ] + return list(currencies.keys()) + + class Provider(NamedTuple): name: str domain: str @@ -246,7 +257,8 @@ async def btc_price(currency: str) -> float: async def fetch_price(provider: Provider): url = provider.api_url.format(**replacements) try: - async with httpx.AsyncClient() as client: + headers = {"User-Agent": settings.user_agent} + async with httpx.AsyncClient(headers=headers) as client: r = await client.get(url, timeout=0.5) r.raise_for_status() data = r.json() @@ -262,7 +274,7 @@ async def fetch_price(provider: Provider): *[fetch_price(provider) for provider in exchange_rate_providers.values()], return_exceptions=True, ) - rates = [r for r in results if not isinstance(r, Exception)] + rates = [r for r in results if not isinstance(r, BaseException)] if not rates: return 9999999999 diff --git a/lnbits/wallets/__init__.py b/lnbits/wallets/__init__.py index 6b3759f767..6a4bbe1023 100644 --- a/lnbits/wallets/__init__.py +++ b/lnbits/wallets/__init__.py @@ -7,8 +7,12 @@ from lnbits.settings import settings from lnbits.wallets.base import Wallet +from .alby import AlbyWallet from .cliche import ClicheWallet from .corelightning import CoreLightningWallet + +# The following import is intentional to keep backwards compatibility +# for old configs that called it CLightningWallet. Do not remove. from .corelightning import CoreLightningWallet as CLightningWallet from .corelightningrest import CoreLightningRestWallet from .eclair import EclairWallet @@ -21,6 +25,7 @@ from .opennode import OpenNodeWallet from .spark import SparkWallet from .void import VoidWallet +from .zbd import ZBDWallet def set_wallet_class(class_name: Optional[str] = None): diff --git a/lnbits/wallets/alby.py b/lnbits/wallets/alby.py new file mode 100644 index 0000000000..69a9509b09 --- /dev/null +++ b/lnbits/wallets/alby.py @@ -0,0 +1,189 @@ +import asyncio +import hashlib +import json +from typing import AsyncGenerator, Dict, Optional + +import httpx +from loguru import logger + +from lnbits.settings import settings + +from .base import ( + InvoiceResponse, + PaymentPendingStatus, + PaymentResponse, + PaymentStatus, + StatusResponse, + Wallet, +) + + +class AlbyWallet(Wallet): + """https://guides.getalby.com/alby-wallet-api/reference/api-reference""" + + def __init__(self): + if not settings.alby_api_endpoint: + raise ValueError("cannot initialize AlbyWallet: missing alby_api_endpoint") + if not settings.alby_access_token: + raise ValueError("cannot initialize AlbyWallet: missing alby_access_token") + + self.endpoint = self.normalize_endpoint(settings.alby_api_endpoint) + self.auth = { + "Authorization": "Bearer " + settings.alby_access_token, + "User-Agent": settings.user_agent, + } + self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.auth) + + async def cleanup(self): + try: + await self.client.aclose() + except RuntimeError as e: + logger.warning(f"Error closing wallet connection: {e}") + + async def status(self) -> StatusResponse: + try: + r = await self.client.get("/balance", timeout=10) + r.raise_for_status() + + data = r.json() + + if len(data) == 0: + return StatusResponse("no data", 0) + + if r.is_error or data["unit"] != "sat": + error_message = data["message"] if "message" in data else r.text + return StatusResponse(f"Server error: '{error_message}'", 0) + + # multiply balance by 1000 to get msats balance + return StatusResponse(None, data["balance"] * 1000) + except KeyError as exc: + logger.warning(exc) + return StatusResponse("Server error: 'missing required fields'", 0) + except json.JSONDecodeError as exc: + logger.warning(exc) + return StatusResponse("Server error: 'invalid json response'", 0) + except Exception as exc: + logger.warning(exc) + return StatusResponse(f"Unable to connect to {self.endpoint}.", 0) + + async def create_invoice( + self, + amount: int, + memo: Optional[str] = None, + description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, + **kwargs, + ) -> InvoiceResponse: + # https://api.getalby.com/invoices + data: Dict = {"amount": f"{amount}"} + if description_hash: + data["description_hash"] = description_hash.hex() + elif unhashed_description: + data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest() + else: + data["memo"] = memo or "" + + try: + r = await self.client.post( + "/invoices", + json=data, + timeout=40, + ) + r.raise_for_status() + + data = r.json() + + if r.is_error: + error_message = data["message"] if "message" in data else r.text + return InvoiceResponse(False, None, None, error_message) + + checking_id = data["payment_hash"] + payment_request = data["payment_request"] + return InvoiceResponse(True, checking_id, payment_request, None) + except KeyError as exc: + logger.warning(exc) + return InvoiceResponse( + False, None, None, "Server error: 'missing required fields'" + ) + except json.JSONDecodeError as exc: + logger.warning(exc) + return InvoiceResponse( + False, None, None, "Server error: 'invalid json response'" + ) + except Exception as exc: + logger.warning(exc) + return InvoiceResponse( + False, None, None, f"Unable to connect to {self.endpoint}." + ) + + async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: + try: + # https://api.getalby.com/payments/bolt11 + r = await self.client.post( + "/payments/bolt11", + json={"invoice": bolt11}, # assume never need amount in body + timeout=None, + ) + r.raise_for_status() + data = r.json() + + if r.is_error: + error_message = data["message"] if "message" in data else r.text + return PaymentResponse(False, None, None, None, error_message) + + checking_id = data["payment_hash"] + # todo: confirm with bitkarrot that having the minus is fine + # other funding sources return a positive fee value + fee_msat = -data["fee"] + preimage = data["payment_preimage"] + + return PaymentResponse(True, checking_id, fee_msat, preimage, None) + except KeyError as exc: + logger.warning(exc) + return PaymentResponse( + False, None, None, None, "Server error: 'missing required fields'" + ) + except json.JSONDecodeError as exc: + logger.warning(exc) + return PaymentResponse( + False, None, None, None, "Server error: 'invalid json response'" + ) + except Exception as exc: + logger.info(f"Failed to pay invoice {bolt11}") + logger.warning(exc) + return PaymentResponse( + False, None, None, None, f"Unable to connect to {self.endpoint}." + ) + + async def get_invoice_status(self, checking_id: str) -> PaymentStatus: + return await self.get_payment_status(checking_id) + + async def get_payment_status(self, checking_id: str) -> PaymentStatus: + try: + r = await self.client.get(f"/invoices/{checking_id}") + + if r.is_error: + return PaymentPendingStatus() + + data = r.json() + + statuses = { + "CREATED": None, + "SETTLED": True, + } + # todo: extract fee and preimage + # maybe use the more specific endpoints: + # - https://api.getalby.com/invoices/incoming + # - https://api.getalby.com/invoices/outgoing + return PaymentStatus( + statuses[data.get("state")], fee_msat=None, preimage=None + ) + except Exception as e: + logger.error(f"Error getting invoice status: {e}") + return PaymentPendingStatus() + + async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: + self.queue: asyncio.Queue = asyncio.Queue(0) + while True: + value = await self.queue.get() + yield value diff --git a/lnbits/wallets/base.py b/lnbits/wallets/base.py index 46786265c4..3ca38e820e 100644 --- a/lnbits/wallets/base.py +++ b/lnbits/wallets/base.py @@ -18,6 +18,18 @@ class InvoiceResponse(NamedTuple): payment_request: Optional[str] = None error_message: Optional[str] = None + @property + def success(self) -> bool: + return self.ok is True + + @property + def pending(self) -> bool: + return self.ok is None + + @property + def failed(self) -> bool: + return self.ok is False + class PaymentResponse(NamedTuple): # when ok is None it means we don't know if this succeeded @@ -27,12 +39,28 @@ class PaymentResponse(NamedTuple): preimage: Optional[str] = None error_message: Optional[str] = None + @property + def success(self) -> bool: + return self.ok is True + + @property + def pending(self) -> bool: + return self.ok is None + + @property + def failed(self) -> bool: + return self.ok is False + class PaymentStatus(NamedTuple): paid: Optional[bool] = None fee_msat: Optional[int] = None preimage: Optional[str] = None + @property + def success(self) -> bool: + return self.paid is True + @property def pending(self) -> bool: return self.paid is not True @@ -52,6 +80,18 @@ def __str__(self) -> str: return "unknown (should never happen)" +class PaymentSuccessStatus(PaymentStatus): + paid = True + + +class PaymentFailedStatus(PaymentStatus): + paid = False + + +class PaymentPendingStatus(PaymentStatus): + paid = None + + class Wallet(ABC): async def cleanup(self): pass @@ -95,6 +135,14 @@ def get_payment_status( def paid_invoices_stream(self) -> AsyncGenerator[str, None]: pass + def normalize_endpoint(self, endpoint: str, add_proto=True) -> str: + endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint + if add_proto: + endpoint = ( + f"https://{endpoint}" if not endpoint.startswith("http") else endpoint + ) + return endpoint + class Unsupported(Exception): pass diff --git a/lnbits/wallets/cliche.py b/lnbits/wallets/cliche.py index 36894d0eee..b9c56a644f 100644 --- a/lnbits/wallets/cliche.py +++ b/lnbits/wallets/cliche.py @@ -10,6 +10,7 @@ from .base import ( InvoiceResponse, + PaymentPendingStatus, PaymentResponse, PaymentStatus, StatusResponse, @@ -21,9 +22,10 @@ class ClicheWallet(Wallet): """https://github.com/fiatjaf/cliche""" def __init__(self): - self.endpoint = settings.cliche_endpoint - if not self.endpoint: - raise Exception("cannot initialize cliche") + if not settings.cliche_endpoint: + raise ValueError("cannot initialize ClicheWallet: missing cliche_endpoint") + + self.endpoint = self.normalize_endpoint(settings.cliche_endpoint) async def status(self) -> StatusResponse: try: @@ -138,7 +140,7 @@ async def get_invoice_status(self, checking_id: str) -> PaymentStatus: if data.get("error") is not None and data["error"].get("message"): logger.error(data["error"]["message"]) - return PaymentStatus(None) + return PaymentPendingStatus() statuses = {"pending": None, "complete": True, "failed": False} return PaymentStatus(statuses[data["result"]["status"]]) @@ -151,7 +153,7 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus: if data.get("error") is not None and data["error"].get("message"): logger.error(data["error"]["message"]) - return PaymentStatus(None) + return PaymentPendingStatus() payment = data["result"] statuses = {"pending": None, "complete": True, "failed": False} return PaymentStatus( diff --git a/lnbits/wallets/corelightning.py b/lnbits/wallets/corelightning.py index 7080d23092..8a755d3bca 100644 --- a/lnbits/wallets/corelightning.py +++ b/lnbits/wallets/corelightning.py @@ -12,8 +12,11 @@ from .base import ( InvoiceResponse, + PaymentFailedStatus, + PaymentPendingStatus, PaymentResponse, PaymentStatus, + PaymentSuccessStatus, StatusResponse, Unsupported, Wallet, @@ -29,9 +32,13 @@ class CoreLightningWallet(Wallet): __node_cls__ = CoreLightningNode def __init__(self): - self.rpc = settings.corelightning_rpc or settings.clightning_rpc - self.ln = LightningRpc(self.rpc) + rpc = settings.corelightning_rpc or settings.clightning_rpc + if not rpc: + raise ValueError( + "cannot initialize CoreLightningWallet: missing corelightning_rpc" + ) + self.ln = LightningRpc(rpc) # check if description_hash is supported (from corelightning>=v0.11.0) command = self.ln.help("invoice")["help"][0]["command"] # type: ignore self.supports_description_hash = "deschashonly" in command @@ -135,8 +142,6 @@ async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse f" '{exc.error.get('message') or exc.error}'." # type: ignore ) return PaymentResponse(False, None, None, None, error_message) - except Exception as exc: - return PaymentResponse(False, None, None, None, str(exc)) fee_msat = -int(r["amount_sent_msat"] - r["amount_msat"]) return PaymentResponse( @@ -147,30 +152,34 @@ async def get_invoice_status(self, checking_id: str) -> PaymentStatus: try: r: dict = self.ln.listinvoices(payment_hash=checking_id) # type: ignore except RpcError: - return PaymentStatus(None) + return PaymentPendingStatus() if not r["invoices"]: - return PaymentStatus(None) + return PaymentPendingStatus() invoice_resp = r["invoices"][-1] if invoice_resp["payment_hash"] == checking_id: if invoice_resp["status"] == "paid": - return PaymentStatus(True) + return PaymentSuccessStatus() elif invoice_resp["status"] == "unpaid": - return PaymentStatus(None) + return PaymentPendingStatus() elif invoice_resp["status"] == "expired": - return PaymentStatus(False) + return PaymentFailedStatus() else: logger.warning(f"supplied an invalid checking_id: {checking_id}") - return PaymentStatus(None) + return PaymentPendingStatus() async def get_payment_status(self, checking_id: str) -> PaymentStatus: try: r: dict = self.ln.listpays(payment_hash=checking_id) # type: ignore except Exception: - return PaymentStatus(None) - if "pays" not in r or not r["pays"]: - return PaymentStatus(None) + return PaymentPendingStatus() + if "pays" not in r: + return PaymentPendingStatus() + if not r["pays"]: + # no payment with this payment_hash is found + return PaymentFailedStatus() + payment_resp = r["pays"][-1] if payment_resp["payment_hash"] == checking_id: @@ -180,14 +189,16 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus: payment_resp["amount_sent_msat"] - payment_resp["amount_msat"] ) - return PaymentStatus(True, fee_msat, payment_resp["preimage"]) + return PaymentSuccessStatus( + fee_msat=fee_msat, preimage=payment_resp["preimage"] + ) elif status == "failed": - return PaymentStatus(False) + return PaymentFailedStatus() else: - return PaymentStatus(None) + return PaymentPendingStatus() else: logger.warning(f"supplied an invalid checking_id: {checking_id}") - return PaymentStatus(None) + return PaymentPendingStatus() async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: while True: diff --git a/lnbits/wallets/corelightningrest.py b/lnbits/wallets/corelightningrest.py index f60ab22431..870e51e6c3 100644 --- a/lnbits/wallets/corelightningrest.py +++ b/lnbits/wallets/corelightningrest.py @@ -12,6 +12,7 @@ from .base import ( InvoiceResponse, + PaymentPendingStatus, PaymentResponse, PaymentStatus, StatusResponse, @@ -23,29 +24,33 @@ class CoreLightningRestWallet(Wallet): def __init__(self): - macaroon = settings.corelightning_rest_macaroon - assert macaroon, "missing cln-rest macaroon" - - self.macaroon = load_macaroon(macaroon) - - url = settings.corelightning_rest_url - if not url: - raise Exception("missing url for corelightning-rest") + if not settings.corelightning_rest_url: + raise ValueError( + "cannot initialize CoreLightningRestWallet: " + "missing corelightning_rest_url" + ) + if not settings.corelightning_rest_macaroon: + raise ValueError( + "cannot initialize CoreLightningRestWallet: " + "missing corelightning_rest_macaroon" + ) + macaroon = load_macaroon(settings.corelightning_rest_macaroon) if not macaroon: - raise Exception("missing macaroon for corelightning-rest") + raise ValueError( + "cannot initialize CoreLightningRestWallet: " + "invalid corelightning_rest_macaroon provided" + ) - self.url = url[:-1] if url.endswith("/") else url - self.url = ( - f"https://{self.url}" if not self.url.startswith("http") else self.url - ) - self.auth = { - "macaroon": self.macaroon, + self.url = self.normalize_endpoint(settings.corelightning_rest_url) + headers = { + "macaroon": macaroon, "encodingtype": "hex", "accept": "application/json", + "User-Agent": settings.user_agent, } self.cert = settings.corelightning_rest_cert or False - self.client = httpx.AsyncClient(verify=self.cert, headers=self.auth) + self.client = httpx.AsyncClient(verify=self.cert, headers=headers) self.last_pay_index = 0 self.statuses = { "paid": True, @@ -61,23 +66,28 @@ async def cleanup(self): logger.warning(f"Error closing wallet connection: {e}") async def status(self) -> StatusResponse: - r = await self.client.get(f"{self.url}/v1/channel/localremotebal", timeout=5) - r.raise_for_status() - if r.is_error or "error" in r.json(): - try: - data = r.json() - error_message = data["error"] - except Exception: - error_message = r.text - return StatusResponse( - f"Failed to connect to {self.url}, got: '{error_message}...'", 0 + try: + r = await self.client.get( + f"{self.url}/v1/channel/localremotebal", timeout=5 ) + r.raise_for_status() + data = r.json() - data = r.json() - if len(data) == 0: - return StatusResponse("no data", 0) + if len(data) == 0: + return StatusResponse("no data", 0) - return StatusResponse(None, int(data.get("localBalance") * 1000)) + if "error" in data: + return StatusResponse(f"""Server error: '{data["error"]}'""", 0) + + if r.is_error or "localBalance" not in data: + return StatusResponse(f"Server error: '{r.text}'", 0) + + return StatusResponse(None, int(data.get("localBalance") * 1000)) + except json.JSONDecodeError: + return StatusResponse("Server error: 'invalid json response'", 0) + except Exception as exc: + logger.warning(exc) + return StatusResponse(f"Unable to connect to {self.url}.", 0) async def create_invoice( self, @@ -87,7 +97,7 @@ async def create_invoice( unhashed_description: Optional[bytes] = None, **kwargs, ) -> InvoiceResponse: - label = f"lbl{random.random()}" + label = kwargs.get("label", f"lbl{random.random()}") data: Dict = { "amount": amount * 1000, "description": memo, @@ -108,26 +118,41 @@ async def create_invoice( if kwargs.get("preimage"): data["preimage"] = kwargs["preimage"] - r = await self.client.post( - f"{self.url}/v1/invoice/genInvoice", - data=data, - ) + try: + r = await self.client.post( + f"{self.url}/v1/invoice/genInvoice", + data=data, + ) + r.raise_for_status() - if r.is_error or "error" in r.json(): - try: - data = r.json() - error_message = data["error"] - except Exception: - error_message = r.text + data = r.json() + + if len(data) == 0: + return InvoiceResponse(False, None, None, "no data") + + if "error" in data: + return InvoiceResponse( + False, None, None, f"""Server error: '{data["error"]}'""" + ) + + if r.is_error: + return InvoiceResponse(False, None, None, f"Server error: '{r.text}'") - return InvoiceResponse(False, None, None, error_message) + if "payment_hash" not in data or "bolt11" not in data: + return InvoiceResponse( + False, None, None, "Server error: 'missing required fields'" + ) - data = r.json() - assert "payment_hash" in data - assert "bolt11" in data - # NOTE: use payment_hash when corelightning-rest updates and supports it - # return InvoiceResponse(True, data["payment_hash"], data["bolt11"], None) - return InvoiceResponse(True, label, data["bolt11"], None) + return InvoiceResponse(True, data["payment_hash"], data["bolt11"], None) + except json.JSONDecodeError: + return InvoiceResponse( + False, None, None, "Server error: 'invalid json response'" + ) + except Exception as exc: + logger.warning(exc) + return InvoiceResponse( + False, None, None, f"Unable to connect to {self.url}." + ) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: try: @@ -139,46 +164,58 @@ async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse error_message = "0 amount invoices are not allowed" return PaymentResponse(False, None, None, None, error_message) fee_limit_percent = fee_limit_msat / invoice.amount_msat * 100 - r = await self.client.post( - f"{self.url}/v1/pay", - data={ - "invoice": bolt11, - "maxfeepercent": f"{fee_limit_percent:.11}", - "exemptfee": 0, # so fee_limit_percent is applied even on payments - # with fee < 5000 millisatoshi (which is default value of exemptfee) - }, - timeout=None, - ) - - if r.is_error or "error" in r.json(): - try: - data = r.json() - error_message = data["error"] - except Exception: - error_message = r.text - return PaymentResponse(False, None, None, None, error_message) + try: + r = await self.client.post( + f"{self.url}/v1/pay", + data={ + "invoice": bolt11, + "maxfeepercent": f"{fee_limit_percent:.11}", + "exemptfee": 0, # so fee_limit_percent is applied even on payments + # with fee < 5000 millisatoshi (which is default value of exemptfee) + }, + timeout=None, + ) - data = r.json() + r.raise_for_status() + data = r.json() - if data["status"] != "complete": - return PaymentResponse(False, None, None, None, "payment failed") + if "error" in data: + return PaymentResponse(False, None, None, None, data["error"]) + if r.is_error: + return PaymentResponse(False, None, None, None, r.text) + if ( + "payment_hash" not in data + or "payment_preimage" not in data + or "msatoshi_sent" not in data + or "msatoshi" not in data + or "status" not in data + ): + return PaymentResponse( + False, None, None, None, "Server error: 'missing required fields'" + ) - checking_id = data["payment_hash"] - preimage = data["payment_preimage"] - fee_msat = data["msatoshi_sent"] - data["msatoshi"] + checking_id = data["payment_hash"] + preimage = data["payment_preimage"] + fee_msat = data["msatoshi_sent"] - data["msatoshi"] - return PaymentResponse( - self.statuses.get(data["status"]), checking_id, fee_msat, preimage, None - ) + return PaymentResponse( + self.statuses.get(data["status"]), checking_id, fee_msat, preimage, None + ) + except json.JSONDecodeError: + return PaymentResponse( + False, None, None, None, "Server error: 'invalid json response'" + ) + except Exception as exc: + logger.info(f"Failed to pay invoice {bolt11}") + logger.warning(exc) + return PaymentResponse( + False, None, None, None, f"Unable to connect to {self.url}." + ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: - # get invoice bolt11 from checking_id - # corelightning-rest wants the "label" here.... - # NOTE: We can get rid of all labels and use payment_hash when - # corelightning-rest updates and supports it r = await self.client.get( f"{self.url}/v1/invoice/listInvoices", - params={"label": checking_id}, + params={"payment_hash": checking_id}, ) try: r.raise_for_status() @@ -189,17 +226,12 @@ async def get_invoice_status(self, checking_id: str) -> PaymentStatus: return PaymentStatus(self.statuses.get(data["invoices"][0]["status"])) except Exception as e: logger.error(f"Error getting invoice status: {e}") - return PaymentStatus(None) + return PaymentPendingStatus() async def get_payment_status(self, checking_id: str) -> PaymentStatus: - from lnbits.core.services import get_standalone_payment - - payment = await get_standalone_payment(checking_id) - if not payment: - raise ValueError(f"Payment with checking_id {checking_id} not found") r = await self.client.get( f"{self.url}/v1/pay/listPays", - params={"invoice": payment.bolt11}, + params={"payment_hash": checking_id}, ) try: r.raise_for_status() @@ -221,7 +253,7 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus: return PaymentStatus(self.statuses.get(pay["status"]), fee_msat, preimage) except Exception as e: logger.error(f"Error getting payment status: {e}") - return PaymentStatus(None) + return PaymentPendingStatus() async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: while True: @@ -241,19 +273,24 @@ async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: except Exception: continue logger.trace(f"paid invoice: {inv}") - yield inv["label"] - # NOTE: use payment_hash when corelightning-rest updates - # and supports it + + # NOTE: use payment_hash when corelightning-rest returns it + # when using waitAnyInvoice # payment_hash = inv["payment_hash"] # yield payment_hash # hack to return payment_hash if the above shouldn't work - # r = await self.client.get( - # f"{self.url}/v1/invoice/listInvoices", - # params={"label": inv["label"]}, - # ) - # paid_invoce = r.json() - # logger.trace(f"paid invoice: {paid_invoce}") - # yield paid_invoce["invoices"][0]["payment_hash"] + r = await self.client.get( + f"{self.url}/v1/invoice/listInvoices", + params={"label": inv["label"]}, + ) + paid_invoice = r.json() + logger.trace(f"paid invoice: {paid_invoice}") + assert self.statuses[ + paid_invoice["invoices"][0]["status"] + ], "streamed invoice not paid" + assert "invoices" in paid_invoice, "no invoices in response" + assert len(paid_invoice["invoices"]), "no invoices in response" + yield paid_invoice["invoices"][0]["payment_hash"] except Exception as exc: logger.debug( diff --git a/lnbits/wallets/eclair.py b/lnbits/wallets/eclair.py index ac6c5e42b9..a1578fe80c 100644 --- a/lnbits/wallets/eclair.py +++ b/lnbits/wallets/eclair.py @@ -13,6 +13,7 @@ from .base import ( InvoiceResponse, + PaymentPendingStatus, PaymentResponse, PaymentStatus, StatusResponse, @@ -30,18 +31,22 @@ class UnknownError(Exception): class EclairWallet(Wallet): def __init__(self): - url = settings.eclair_url - passw = settings.eclair_pass - if not url or not passw: - raise Exception("cannot initialize eclair") + if not settings.eclair_url: + raise ValueError("cannot initialize EclairWallet: missing eclair_url") + if not settings.eclair_pass: + raise ValueError("cannot initialize EclairWallet: missing eclair_pass") - self.url = url[:-1] if url.endswith("/") else url + self.url = self.normalize_endpoint(settings.eclair_url) self.ws_url = f"ws://{urllib.parse.urlsplit(self.url).netloc}/ws" - encodedAuth = base64.b64encode(f":{passw}".encode()) + password = settings.eclair_pass + encodedAuth = base64.b64encode(f":{password}".encode()) auth = str(encodedAuth, "utf-8") - self.auth = {"Authorization": f"Basic {auth}"} - self.client = httpx.AsyncClient(base_url=self.url, headers=self.auth) + self.headers = { + "Authorization": f"Basic {auth}", + "User-Agent": settings.user_agent, + } + self.client = httpx.AsyncClient(base_url=self.url, headers=self.headers) async def cleanup(self): try: @@ -176,7 +181,7 @@ async def get_invoice_status(self, checking_id: str) -> PaymentStatus: } return PaymentStatus(statuses.get(data["status"]["type"])) except Exception: - return PaymentStatus(None) + return PaymentPendingStatus() async def get_payment_status(self, checking_id: str) -> PaymentStatus: try: @@ -207,14 +212,14 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus: statuses.get(data["status"]["type"]), fee_msat, preimage ) except Exception: - return PaymentStatus(None) + return PaymentPendingStatus() async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: while True: try: async with connect( self.ws_url, - extra_headers=[("Authorization", self.auth["Authorization"])], + extra_headers=[("Authorization", self.headers["Authorization"])], ) as ws: while True: message = await ws.recv() diff --git a/lnbits/wallets/fake.py b/lnbits/wallets/fake.py index 9cc498485e..01323ad5af 100644 --- a/lnbits/wallets/fake.py +++ b/lnbits/wallets/fake.py @@ -1,9 +1,8 @@ import asyncio import hashlib -import random from datetime import datetime from os import urandom -from typing import AsyncGenerator, Optional +from typing import AsyncGenerator, Dict, Optional, Set from bolt11 import ( Bolt11, @@ -20,8 +19,11 @@ from .base import ( InvoiceResponse, + PaymentFailedStatus, + PaymentPendingStatus, PaymentResponse, PaymentStatus, + PaymentSuccessStatus, StatusResponse, Wallet, ) @@ -29,6 +31,8 @@ class FakeWallet(Wallet): queue: asyncio.Queue = asyncio.Queue(0) + payment_secrets: Dict[str, str] = {} + paid_invoices: Set[str] = set() secret: str = settings.fake_wallet_secret privkey: str = hashlib.pbkdf2_hmac( "sha256", @@ -70,20 +74,18 @@ async def create_invoice( if expiry: tags.add(TagChar.expire_time, expiry) - # random hash - checking_id = ( - self.privkey[:6] - + hashlib.sha256(str(random.getrandbits(256)).encode()).hexdigest()[6:] - ) - - tags.add(TagChar.payment_hash, checking_id) - if payment_secret: secret = payment_secret.hex() else: secret = urandom(32).hex() tags.add(TagChar.payment_secret, secret) + payment_hash = hashlib.sha256(secret.encode()).hexdigest() + + tags.add(TagChar.payment_hash, payment_hash) + + self.payment_secrets[payment_hash] = secret + bolt11 = Bolt11( currency="bc", amount_msat=MilliSatoshi(amount * 1000), @@ -93,7 +95,9 @@ async def create_invoice( payment_request = encode(bolt11, self.privkey) - return InvoiceResponse(True, checking_id, payment_request) + return InvoiceResponse( + ok=True, checking_id=payment_hash, payment_request=payment_request + ) async def pay_invoice(self, bolt11: str, _: int) -> PaymentResponse: try: @@ -101,19 +105,29 @@ async def pay_invoice(self, bolt11: str, _: int) -> PaymentResponse: except Bolt11Exception as exc: return PaymentResponse(ok=False, error_message=str(exc)) - if invoice.payment_hash[:6] == self.privkey[:6]: + if invoice.payment_hash in self.payment_secrets: await self.queue.put(invoice) - return PaymentResponse(True, invoice.payment_hash, 0) + self.paid_invoices.add(invoice.payment_hash) + return PaymentResponse( + ok=True, + checking_id=invoice.payment_hash, + fee_msat=0, + preimage=self.payment_secrets.get(invoice.payment_hash) or "0" * 64, + ) else: return PaymentResponse( ok=False, error_message="Only internal invoices can be used!" ) - async def get_invoice_status(self, _: str) -> PaymentStatus: - return PaymentStatus(None) + async def get_invoice_status(self, checking_id: str) -> PaymentStatus: + if checking_id in self.paid_invoices: + return PaymentSuccessStatus() + if checking_id in list(self.payment_secrets.keys()): + return PaymentPendingStatus() + return PaymentFailedStatus() async def get_payment_status(self, _: str) -> PaymentStatus: - return PaymentStatus(None) + return PaymentPendingStatus() async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: while True: diff --git a/lnbits/wallets/lnbits.py b/lnbits/wallets/lnbits.py index 0bc47d06c3..e2674c6ef5 100644 --- a/lnbits/wallets/lnbits.py +++ b/lnbits/wallets/lnbits.py @@ -9,8 +9,11 @@ from .base import ( InvoiceResponse, + PaymentFailedStatus, + PaymentPendingStatus, PaymentResponse, PaymentStatus, + PaymentSuccessStatus, StatusResponse, Wallet, ) @@ -20,16 +23,21 @@ class LNbitsWallet(Wallet): """https://github.com/lnbits/lnbits""" def __init__(self): - self.endpoint = settings.lnbits_endpoint + if not settings.lnbits_endpoint: + raise ValueError("cannot initialize LNbitsWallet: missing lnbits_endpoint") key = ( settings.lnbits_key or settings.lnbits_admin_key or settings.lnbits_invoice_key ) - if not self.endpoint or not key: - raise Exception("cannot initialize lnbits wallet") - self.key = {"X-Api-Key": key} - self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.key) + if not key: + raise ValueError( + "cannot initialize LNbitsWallet: " + "missing lnbits_key or lnbits_admin_key or lnbits_invoice_key" + ) + self.endpoint = self.normalize_endpoint(settings.lnbits_endpoint) + self.headers = {"X-Api-Key": key, "User-Agent": settings.user_agent} + self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.headers) async def cleanup(self): try: @@ -114,29 +122,44 @@ async def get_invoice_status(self, checking_id: str) -> PaymentStatus: r = await self.client.get( url=f"/api/v1/payments/{checking_id}", ) - if r.is_error: - return PaymentStatus(None) - return PaymentStatus(r.json()["paid"]) + r.raise_for_status() + + data = r.json() + details = data.get("details", None) + + if details and details.get("pending", False) is True: + return PaymentPendingStatus() + if data.get("paid", False) is True: + return PaymentSuccessStatus() + return PaymentFailedStatus() except Exception: - return PaymentStatus(None) + return PaymentPendingStatus() async def get_payment_status(self, checking_id: str) -> PaymentStatus: r = await self.client.get(url=f"/api/v1/payments/{checking_id}") if r.is_error: - return PaymentStatus(False) + return PaymentPendingStatus() data = r.json() - if "paid" not in data and "details" not in data: - return PaymentStatus(None) - return PaymentStatus(data["paid"], data["details"]["fee"], data["preimage"]) + if "paid" not in data or not data["paid"]: + return PaymentPendingStatus() + + if "details" not in data: + return PaymentPendingStatus() + + return PaymentSuccessStatus( + fee_msat=data["details"]["fee"], preimage=data["preimage"] + ) async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: url = f"{self.endpoint}/api/v1/payments/sse" while True: try: - async with httpx.AsyncClient(timeout=None, headers=self.key) as client: + async with httpx.AsyncClient( + timeout=None, headers=self.headers + ) as client: del client.headers[ "accept-encoding" ] # we have to disable compression for SSEs diff --git a/lnbits/wallets/lndgrpc.py b/lnbits/wallets/lndgrpc.py index abb554ad3c..54776e8e09 100644 --- a/lnbits/wallets/lndgrpc.py +++ b/lnbits/wallets/lndgrpc.py @@ -12,15 +12,18 @@ import lnbits.wallets.lnd_grpc_files.router_pb2 as router import lnbits.wallets.lnd_grpc_files.router_pb2_grpc as routerrpc from lnbits.settings import settings +from lnbits.utils.crypto import AESCipher from .base import ( InvoiceResponse, + PaymentPendingStatus, PaymentResponse, PaymentStatus, + PaymentSuccessStatus, StatusResponse, Wallet, ) -from .macaroon import AESCipher, load_macaroon +from .macaroon import load_macaroon def b64_to_bytes(checking_id: str) -> bytes: @@ -57,7 +60,16 @@ def bytes_to_hex(b: bytes) -> str: class LndWallet(Wallet): def __init__(self): - endpoint = settings.lnd_grpc_endpoint + if not settings.lnd_grpc_endpoint: + raise ValueError("cannot initialize LndWallet: missing lnd_grpc_endpoint") + if not settings.lnd_grpc_port: + raise ValueError("cannot initialize LndWallet: missing lnd_grpc_port") + + cert_path = settings.lnd_grpc_cert or settings.lnd_cert + if not cert_path: + raise ValueError( + "cannot initialize LndWallet: missing lnd_grpc_cert or lnd_cert" + ) macaroon = ( settings.lnd_grpc_macaroon @@ -66,24 +78,25 @@ def __init__(self): or settings.lnd_grpc_invoice_macaroon or settings.lnd_invoice_macaroon ) - encrypted_macaroon = settings.lnd_grpc_macaroon_encrypted if encrypted_macaroon: macaroon = AESCipher(description="macaroon decryption").decrypt( encrypted_macaroon ) + if not macaroon: + raise ValueError( + "cannot initialize LndWallet: " + "missing lnd_grpc_macaroon or lnd_grpc_admin_macaroon or " + "lnd_admin_macaroon or lnd_grpc_invoice_macaroon or " + "lnd_invoice_macaroon or lnd_grpc_macaroon_encrypted" + ) - cert_path = settings.lnd_grpc_cert or settings.lnd_cert - if not endpoint or not macaroon or not cert_path or not settings.lnd_grpc_port: - raise Exception("cannot initialize lndrest") - - self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint + self.endpoint = self.normalize_endpoint( + settings.lnd_grpc_endpoint, add_proto=False + ) self.port = int(settings.lnd_grpc_port) - self.cert_path = settings.lnd_grpc_cert or settings.lnd_cert - self.macaroon = load_macaroon(macaroon) - self.cert_path = cert_path - cert = open(self.cert_path, "rb").read() + cert = open(cert_path, "rb").read() creds = grpc.ssl_channel_credentials(cert) auth_creds = grpc.metadata_call_credentials(self.metadata_callback) composite_creds = grpc.composite_channel_credentials(creds, auth_creds) @@ -192,15 +205,15 @@ async def get_invoice_status(self, checking_id: str) -> PaymentStatus: except ValueError: # this may happen if we switch between backend wallets # that use different checking_id formats - return PaymentStatus(None) + return PaymentPendingStatus() try: resp = await self.rpc.LookupInvoice(ln.PaymentHash(r_hash=r_hash)) except grpc.RpcError: - return PaymentStatus(None) + return PaymentPendingStatus() if resp.settled: - return PaymentStatus(True) + return PaymentSuccessStatus() - return PaymentStatus(None) + return PaymentPendingStatus() async def get_payment_status(self, checking_id: str) -> PaymentStatus: """ @@ -213,7 +226,7 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus: except ValueError: # this may happen if we switch between backend wallets # that use different checking_id formats - return PaymentStatus(None) + return PaymentPendingStatus() resp = self.routerpc.TrackPaymentV2( router.TrackPaymentRequest(payment_hash=r_hash) @@ -236,16 +249,15 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus: try: async for payment in resp: if len(payment.htlcs) and statuses[payment.status]: - return PaymentStatus( - True, - -payment.htlcs[-1].route.total_fees_msat, - bytes_to_hex(payment.htlcs[-1].preimage), + return PaymentSuccessStatus( + fee_msat=-payment.htlcs[-1].route.total_fees_msat, + preimage=bytes_to_hex(payment.htlcs[-1].preimage), ) return PaymentStatus(statuses[payment.status]) except Exception: # most likely the payment wasn't found - return PaymentStatus(None) + return PaymentPendingStatus() - return PaymentStatus(None) + return PaymentPendingStatus() async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: while True: diff --git a/lnbits/wallets/lndrest.py b/lnbits/wallets/lndrest.py index 10be48edeb..32c3a0e1dc 100644 --- a/lnbits/wallets/lndrest.py +++ b/lnbits/wallets/lndrest.py @@ -9,15 +9,19 @@ from lnbits.nodes.lndrest import LndRestNode from lnbits.settings import settings +from lnbits.utils.crypto import AESCipher from .base import ( InvoiceResponse, + PaymentFailedStatus, + PaymentPendingStatus, PaymentResponse, PaymentStatus, + PaymentSuccessStatus, StatusResponse, Wallet, ) -from .macaroon import AESCipher, load_macaroon +from .macaroon import load_macaroon class LndRestWallet(Wallet): @@ -26,8 +30,10 @@ class LndRestWallet(Wallet): __node_cls__ = LndRestNode def __init__(self): - endpoint = settings.lnd_rest_endpoint - cert = settings.lnd_rest_cert + if not settings.lnd_rest_endpoint: + raise ValueError( + "cannot initialize LndRestWallet: missing lnd_rest_endpoint" + ) macaroon = ( settings.lnd_rest_macaroon @@ -36,40 +42,39 @@ def __init__(self): or settings.lnd_invoice_macaroon or settings.lnd_rest_invoice_macaroon ) - encrypted_macaroon = settings.lnd_rest_macaroon_encrypted if encrypted_macaroon: macaroon = AESCipher(description="macaroon decryption").decrypt( encrypted_macaroon ) - - if not endpoint: - raise Exception("cannot initialize lndrest: no endpoint") - if not macaroon: - raise Exception("cannot initialize lndrest: no macaroon") + raise ValueError( + "cannot initialize LndRestWallet: " + "missing lnd_rest_macaroon or lnd_admin_macaroon or " + "lnd_rest_admin_macaroon or lnd_invoice_macaroon or " + "lnd_rest_invoice_macaroon or lnd_rest_macaroon_encrypted" + ) - if not cert: + if not settings.lnd_rest_cert: logger.warning( - "no certificate for lndrest provided, this only works if you have a" - " publicly issued certificate" + "No certificate for LndRestWallet provided! " + "This only works if you have a publicly issued certificate." ) - endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint - endpoint = ( - f"https://{endpoint}" if not endpoint.startswith("http") else endpoint - ) - self.endpoint = endpoint - self.macaroon = load_macaroon(macaroon) + self.endpoint = self.normalize_endpoint(settings.lnd_rest_endpoint) # if no cert provided it should be public so we set verify to True # and it will still check for validity of certificate and fail if its not valid # even on startup - self.cert = cert or True + cert = settings.lnd_rest_cert or True - self.auth = {"Grpc-Metadata-macaroon": self.macaroon} + macaroon = load_macaroon(macaroon) + headers = { + "Grpc-Metadata-macaroon": macaroon, + "User-Agent": settings.user_agent, + } self.client = httpx.AsyncClient( - base_url=self.endpoint, headers=self.auth, verify=self.cert + base_url=self.endpoint, headers=headers, verify=cert ) async def cleanup(self): @@ -82,15 +87,18 @@ async def status(self) -> StatusResponse: try: r = await self.client.get("/v1/balance/channels") r.raise_for_status() - except (httpx.ConnectError, httpx.RequestError) as exc: - return StatusResponse(f"Unable to connect to {self.endpoint}. {exc}", 0) - try: data = r.json() - if r.is_error: - raise Exception - except Exception: - return StatusResponse(r.text[:200], 0) + if len(data) == 0: + return StatusResponse("no data", 0) + if r.is_error or "balance" not in data: + return StatusResponse(f"Server error: '{r.text}'", 0) + + except json.JSONDecodeError: + return StatusResponse("Server error: 'invalid json response'", 0) + except Exception as exc: + logger.warning(exc) + return StatusResponse(f"Unable to connect to {self.endpoint}.", 0) return StatusResponse(None, int(data["balance"]) * 1000) @@ -102,7 +110,11 @@ async def create_invoice( unhashed_description: Optional[bytes] = None, **kwargs, ) -> InvoiceResponse: - data: Dict = {"value": amount, "private": True, "memo": memo or ""} + data: Dict = { + "value": amount, + "private": settings.lnd_rest_route_hints, + "memo": memo or "", + } if kwargs.get("expiry"): data["expiry"] = kwargs["expiry"] if description_hash: @@ -114,53 +126,104 @@ async def create_invoice( hashlib.sha256(unhashed_description).digest() ).decode("ascii") - r = await self.client.post(url="/v1/invoices", json=data) + try: + r = await self.client.post(url="/v1/invoices", json=data) + r.raise_for_status() + data = r.json() - if r.is_error: - error_message = r.text - try: - error_message = r.json()["error"] - except Exception: - pass - return InvoiceResponse(False, None, None, error_message) + if len(data) == 0: + return InvoiceResponse(False, None, None, "no data") - data = r.json() - payment_request = data["payment_request"] - payment_hash = base64.b64decode(data["r_hash"]).hex() - checking_id = payment_hash + if "error" in data: + return InvoiceResponse( + False, None, None, f"""Server error: '{data["error"]}'""" + ) + + if r.is_error: + return InvoiceResponse(False, None, None, f"Server error: '{r.text}'") - return InvoiceResponse(True, checking_id, payment_request, None) + if "payment_request" not in data or "r_hash" not in data: + return InvoiceResponse( + False, None, None, "Server error: 'missing required fields'" + ) + + payment_request = data["payment_request"] + payment_hash = base64.b64decode(data["r_hash"]).hex() + checking_id = payment_hash + + return InvoiceResponse(True, checking_id, payment_request, None) + except json.JSONDecodeError: + return InvoiceResponse( + False, None, None, "Server error: 'invalid json response'" + ) + except Exception as exc: + logger.warning(exc) + return InvoiceResponse( + False, None, None, f"Unable to connect to {self.endpoint}." + ) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: # set the fee limit for the payment - lnrpcFeeLimit = dict() + lnrpcFeeLimit = {} lnrpcFeeLimit["fixed_msat"] = f"{fee_limit_msat}" - r = await self.client.post( - url="/v1/channels/transactions", - json={"payment_request": bolt11, "fee_limit": lnrpcFeeLimit}, - timeout=None, - ) + try: + r = await self.client.post( + url="/v1/channels/transactions", + json={"payment_request": bolt11, "fee_limit": lnrpcFeeLimit}, + timeout=None, + ) + r.raise_for_status() + except Exception as exc: + logger.warning(f"LndRestWallet pay_invoice POST error: {exc}.") + return PaymentResponse( + None, None, None, None, f"Unable to connect to {self.endpoint}." + ) - if r.is_error or r.json().get("payment_error"): - error_message = r.json().get("payment_error") or r.text - return PaymentResponse(False, None, None, None, error_message) + try: + data = r.json() - data = r.json() - checking_id = base64.b64decode(data["payment_hash"]).hex() - fee_msat = int(data["payment_route"]["total_fees_msat"]) - preimage = base64.b64decode(data["payment_preimage"]).hex() - return PaymentResponse(True, checking_id, fee_msat, preimage, None) + if data.get("payment_error"): + error_message = r.json().get("payment_error") or r.text + logger.warning( + f"LndRestWallet pay_invoice payment_error: {error_message}." + ) + return PaymentResponse(False, None, None, None, error_message) + + if ( + "payment_hash" not in data + or "payment_route" not in data + or "total_fees_msat" not in data["payment_route"] + or "payment_preimage" not in data + ): + return PaymentResponse( + False, None, None, None, "Server error: 'missing required fields'" + ) + + checking_id = base64.b64decode(data["payment_hash"]).hex() + fee_msat = int(data["payment_route"]["total_fees_msat"]) + preimage = base64.b64decode(data["payment_preimage"]).hex() + return PaymentResponse(True, checking_id, fee_msat, preimage, None) + except json.JSONDecodeError: + return PaymentResponse( + False, None, None, None, "Server error: 'invalid json response'" + ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: r = await self.client.get(url=f"/v1/invoice/{checking_id}") - if r.is_error or not r.json().get("settled"): - # this must also work when checking_id is not a hex recognizable by lnd - # it will return an error and no "settled" attribute on the object - return PaymentStatus(None) + try: + r.raise_for_status() + data = r.json() - return PaymentStatus(True) + if r.is_error or not data.get("settled"): + # this must also work when checking_id is not a hex recognizable by lnd + # it will return an error and no "settled" attribute on the object + return PaymentPendingStatus() + except Exception as e: + logger.error(f"Error getting invoice status: {e}") + return PaymentPendingStatus() + return PaymentSuccessStatus() async def get_payment_status(self, checking_id: str) -> PaymentStatus: """ @@ -172,7 +235,7 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus: "ascii" ) except ValueError: - return PaymentStatus(None) + return PaymentPendingStatus() url = f"/v2/router/track/{checking_id}" @@ -195,7 +258,13 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus: if "message" in line["error"] else line["error"] ) - return PaymentStatus(None) + if ( + line["error"].get("code") == 5 + and line["error"].get("message") + == "payment isn't initiated" + ): + return PaymentFailedStatus() + return PaymentPendingStatus() payment = line.get("result") if payment is not None and payment.get("status"): return PaymentStatus( @@ -204,11 +273,11 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus: preimage=payment.get("payment_preimage"), ) else: - return PaymentStatus(None) + return PaymentPendingStatus() except Exception: continue - return PaymentStatus(None) + return PaymentPendingStatus() async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: while True: diff --git a/lnbits/wallets/lnpay.py b/lnbits/wallets/lnpay.py index 9c7e3a0e7f..46aaab0032 100644 --- a/lnbits/wallets/lnpay.py +++ b/lnbits/wallets/lnpay.py @@ -9,6 +9,7 @@ from .base import ( InvoiceResponse, + PaymentPendingStatus, PaymentResponse, PaymentStatus, StatusResponse, @@ -20,16 +21,28 @@ class LNPayWallet(Wallet): """https://docs.lnpay.co/""" def __init__(self): - endpoint = settings.lnpay_api_endpoint + if not settings.lnpay_api_endpoint: + raise ValueError( + "cannot initialize LNPayWallet: missing lnpay_api_endpoint" + ) + if not settings.lnpay_api_key: + raise ValueError("cannot initialize LNPayWallet: missing lnpay_api_key") + wallet_key = settings.lnpay_wallet_key or settings.lnpay_admin_key + if not wallet_key: + raise ValueError( + "cannot initialize LNPayWallet: " + "missing lnpay_wallet_key or lnpay_admin_key" + ) + self.wallet_key = wallet_key - if not endpoint or not wallet_key or not settings.lnpay_api_key: - raise Exception("cannot initialize lnpay") + self.endpoint = self.normalize_endpoint(settings.lnpay_api_endpoint) - self.wallet_key = wallet_key - self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint - self.auth = {"X-Api-Key": settings.lnpay_api_key} - self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.auth) + headers = { + "X-Api-Key": settings.lnpay_api_key, + "User-Agent": settings.user_agent, + } + self.client = httpx.AsyncClient(base_url=self.endpoint, headers=headers) async def cleanup(self): try: @@ -122,7 +135,7 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus: ) if r.is_error: - return PaymentStatus(None) + return PaymentPendingStatus() data = r.json() preimage = data["payment_preimage"] @@ -135,28 +148,3 @@ async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: while True: value = await self.queue.get() yield value - - async def webhook_listener(self): - logger.error("LNPay webhook listener disabled.") - return - # TODO: request.get_data is undefined, was it something with Flask or quart? - # probably issue introduced when refactoring? - # text: str = await request.get_data() - # try: - # data = json.loads(text) - # except json.decoder.JSONDecodeError: - # logger.error(f"error on lnpay webhook endpoint: {text[:200]}") - # data = None - # if ( - # type(data) is not dict - # or "event" not in data - # or data["event"].get("name") != "wallet_receive" - # ): - # raise HTTPException(status_code=HTTPStatus.NO_CONTENT) - - # lntx_id = data["data"]["wtx"]["lnTx"]["id"] - # r = await self.client.get(f"/lntx/{lntx_id}?fields=settled") - # data = r.json() - # if data["settled"]: - # await self.queue.put(lntx_id) - # raise HTTPException(status_code=HTTPStatus.NO_CONTENT) diff --git a/lnbits/wallets/lntips.py b/lnbits/wallets/lntips.py index 0487270613..a269adb689 100644 --- a/lnbits/wallets/lntips.py +++ b/lnbits/wallets/lntips.py @@ -11,6 +11,7 @@ from .base import ( InvoiceResponse, + PaymentPendingStatus, PaymentResponse, PaymentStatus, StatusResponse, @@ -20,17 +21,28 @@ class LnTipsWallet(Wallet): def __init__(self): - endpoint = settings.lntips_api_endpoint + if not settings.lntips_api_endpoint: + raise ValueError( + "cannot initialize LnTipsWallet: missing lntips_api_endpoint" + ) key = ( settings.lntips_api_key or settings.lntips_admin_key or settings.lntips_invoice_key ) - if not endpoint or not key: - raise Exception("cannot initialize lntxbod") - self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint - self.auth = {"Authorization": f"Basic {key}"} - self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.auth) + if not key: + raise ValueError( + "cannot initialize LnTipsWallet: " + "missing lntips_api_key or lntips_admin_key or lntips_invoice_key" + ) + + self.endpoint = self.normalize_endpoint(settings.lntips_api_endpoint) + + headers = { + "Authorization": f"Basic {key}", + "User-Agent": settings.user_agent, + } + self.client = httpx.AsyncClient(base_url=self.endpoint, headers=headers) async def cleanup(self): try: @@ -121,7 +133,7 @@ async def get_invoice_status(self, checking_id: str) -> PaymentStatus: data = r.json() return PaymentStatus(data["paid"]) except Exception: - return PaymentStatus(None) + return PaymentPendingStatus() async def get_payment_status(self, checking_id: str) -> PaymentStatus: try: @@ -136,7 +148,7 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus: paid_to_status = {False: None, True: True} return PaymentStatus(paid_to_status[data.get("paid")]) except Exception: - return PaymentStatus(None) + return PaymentPendingStatus() async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: last_connected = None diff --git a/lnbits/wallets/macaroon/__init__.py b/lnbits/wallets/macaroon/__init__.py index 16617aa655..44efcf4fed 100644 --- a/lnbits/wallets/macaroon/__init__.py +++ b/lnbits/wallets/macaroon/__init__.py @@ -1 +1 @@ -from .macaroon import AESCipher, load_macaroon +from .macaroon import load_macaroon diff --git a/lnbits/wallets/macaroon/macaroon.py b/lnbits/wallets/macaroon/macaroon.py index 0bb28d56d8..8985c647c8 100644 --- a/lnbits/wallets/macaroon/macaroon.py +++ b/lnbits/wallets/macaroon/macaroon.py @@ -1,12 +1,8 @@ import base64 -import getpass -from hashlib import md5 -from Cryptodome import Random -from Cryptodome.Cipher import AES from loguru import logger -BLOCK_SIZE = 16 +from lnbits.utils.crypto import AESCipher def load_macaroon(macaroon: str) -> str: @@ -39,73 +35,7 @@ def load_macaroon(macaroon: str) -> str: return macaroon -class AESCipher: - """This class is compatible with crypto-js/aes.js - - Encrypt and decrypt in Javascript using: - import AES from "crypto-js/aes.js"; - import Utf8 from "crypto-js/enc-utf8.js"; - AES.encrypt(decrypted, password).toString() - AES.decrypt(encrypted, password).toString(Utf8); - - """ - - def __init__(self, key=None, description=""): - self.key = key - self.description = description + " " - - def pad(self, data): - length = BLOCK_SIZE - (len(data) % BLOCK_SIZE) - return data + (chr(length) * length).encode() - - def unpad(self, data): - return data[: -(data[-1] if isinstance(data[-1], int) else ord(data[-1]))] - - @property - def passphrase(self): - passphrase = self.key if self.key is not None else None - if passphrase is None: - passphrase = getpass.getpass(f"Enter {self.description}password:") - return passphrase - - def bytes_to_key(self, data, salt, output=48): - # extended from https://gist.github.com/gsakkis/4546068 - assert len(salt) == 8, len(salt) - data += salt - key = md5(data).digest() - final_key = key - while len(final_key) < output: - key = md5(key + data).digest() - final_key += key - return final_key[:output] - - def decrypt(self, encrypted: str) -> str: # type: ignore - """Decrypts a string using AES-256-CBC.""" - passphrase = self.passphrase - encrypted = base64.b64decode(encrypted) # type: ignore - assert encrypted[0:8] == b"Salted__" - salt = encrypted[8:16] - key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16) - key = key_iv[:32] - iv = key_iv[32:] - aes = AES.new(key, AES.MODE_CBC, iv) - try: - return self.unpad(aes.decrypt(encrypted[16:])).decode() # type: ignore - except UnicodeDecodeError: - raise ValueError("Wrong passphrase") - - def encrypt(self, message: bytes) -> str: - passphrase = self.passphrase - salt = Random.new().read(8) - key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16) - key = key_iv[:32] - iv = key_iv[32:] - aes = AES.new(key, AES.MODE_CBC, iv) - return base64.b64encode( - b"Salted__" + salt + aes.encrypt(self.pad(message)) - ).decode() - - +# todo: move to its own (crypto.py) file # if this file is executed directly, ask for a macaroon and encrypt it if __name__ == "__main__": macaroon = input("Enter macaroon: ") diff --git a/lnbits/wallets/opennode.py b/lnbits/wallets/opennode.py index b922a33585..7b71f80ff3 100644 --- a/lnbits/wallets/opennode.py +++ b/lnbits/wallets/opennode.py @@ -8,6 +8,7 @@ from .base import ( InvoiceResponse, + PaymentPendingStatus, PaymentResponse, PaymentStatus, StatusResponse, @@ -20,18 +21,29 @@ class OpenNodeWallet(Wallet): """https://developers.opennode.com/""" def __init__(self): - endpoint = settings.opennode_api_endpoint + if not settings.opennode_api_endpoint: + raise ValueError( + "cannot initialize OpenNodeWallet: missing opennode_api_endpoint" + ) key = ( settings.opennode_key or settings.opennode_admin_key or settings.opennode_invoice_key ) - if not endpoint or not key: - raise Exception("cannot initialize opennode") - - self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint - self.auth = {"Authorization": key} - self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.auth) + if not key: + raise ValueError( + "cannot initialize OpenNodeWallet: " + "missing opennode_key or opennode_admin_key or opennode_invoice_key" + ) + self.key = key + + self.endpoint = self.normalize_endpoint(settings.opennode_api_endpoint) + + headers = { + "Authorization": self.key, + "User-Agent": settings.user_agent, + } + self.client = httpx.AsyncClient(base_url=self.endpoint, headers=headers) async def cleanup(self): try: @@ -45,10 +57,12 @@ async def status(self) -> StatusResponse: except (httpx.ConnectError, httpx.RequestError): return StatusResponse(f"Unable to connect to '{self.endpoint}'", 0) - data = r.json()["data"] if r.is_error: - return StatusResponse(data["message"], 0) + error_message = r.json()["message"] + return StatusResponse(error_message, 0) + data = r.json()["data"] + # multiply balance by 1000 to get msats balance return StatusResponse(None, data["balance"]["BTC"] * 1000) async def create_invoice( @@ -67,7 +81,6 @@ async def create_invoice( json={ "amount": amount, "description": memo or "", - # "callback_url": url_for("/webhook_listener", _external=True), }, timeout=40, ) @@ -104,7 +117,7 @@ async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse async def get_invoice_status(self, checking_id: str) -> PaymentStatus: r = await self.client.get(f"/v1/charge/{checking_id}") if r.is_error: - return PaymentStatus(None) + return PaymentPendingStatus() data = r.json()["data"] statuses = {"processing": None, "paid": True, "unpaid": None} return PaymentStatus(statuses[data.get("status")]) @@ -113,7 +126,7 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus: r = await self.client.get(f"/v1/withdrawal/{checking_id}") if r.is_error: - return PaymentStatus(None) + return PaymentPendingStatus() data = r.json()["data"] statuses = { @@ -131,22 +144,3 @@ async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: while True: value = await self.queue.get() yield value - - async def webhook_listener(self): - logger.error("webhook listener for opennode is disabled.") - return - # TODO: request.form is undefined, was it something with Flask or quart? - # probably issue introduced when refactoring? - # data = await request.form # type: ignore - # if "status" not in data or data["status"] != "paid": - # raise HTTPException(status_code=HTTPStatus.NO_CONTENT) - - # charge_id = data["id"] - # x = hmac.new(self.auth["Authorization"].encode("ascii"), digestmod="sha256") - # x.update(charge_id.encode("ascii")) - # if x.hexdigest() != data["hashed_order"]: - # logger.error("invalid webhook, not from opennode") - # raise HTTPException(status_code=HTTPStatus.NO_CONTENT) - - # await self.queue.put(charge_id) - # raise HTTPException(status_code=HTTPStatus.NO_CONTENT) diff --git a/lnbits/wallets/spark.py b/lnbits/wallets/spark.py index a7c51ce42c..1276744a89 100644 --- a/lnbits/wallets/spark.py +++ b/lnbits/wallets/spark.py @@ -11,8 +11,11 @@ from .base import ( InvoiceResponse, + PaymentFailedStatus, + PaymentPendingStatus, PaymentResponse, PaymentStatus, + PaymentSuccessStatus, StatusResponse, Wallet, ) @@ -28,13 +31,17 @@ class UnknownError(Exception): class SparkWallet(Wallet): def __init__(self): - assert settings.spark_url, "spark url does not exist" - self.url = settings.spark_url.replace("/rpc", "") + if not settings.spark_url: + raise ValueError("cannot initialize SparkWallet: missing spark_url") + if not settings.spark_token: + raise ValueError("cannot initialize SparkWallet: missing spark_token") + + url = self.normalize_endpoint(settings.spark_url) + url = url.replace("/rpc", "") self.token = settings.spark_token - assert self.token, "spark wallet token does not exist" - self.client = httpx.AsyncClient( - base_url=self.url, headers={"X-Access": self.token} - ) + + headers = {"X-Access": self.token, "User-Agent": settings.user_agent} + self.client = httpx.AsyncClient(base_url=url, headers=headers) async def cleanup(self): try: @@ -192,33 +199,33 @@ async def get_invoice_status(self, checking_id: str) -> PaymentStatus: try: r = await self.listinvoices(label=checking_id) except (SparkError, UnknownError): - return PaymentStatus(None) + return PaymentPendingStatus() if not r or not r.get("invoices"): - return PaymentStatus(None) + return PaymentPendingStatus() if r["invoices"][0]["status"] == "paid": - return PaymentStatus(True) + return PaymentSuccessStatus() else: - return PaymentStatus(False) + return PaymentFailedStatus() async def get_payment_status(self, checking_id: str) -> PaymentStatus: # check if it's 32 bytes hex if len(checking_id) != 64: - return PaymentStatus(None) + return PaymentPendingStatus() try: int(checking_id, 16) except ValueError: - return PaymentStatus(None) + return PaymentPendingStatus() # ask sparko try: r = await self.listpays(payment_hash=checking_id) except (SparkError, UnknownError): - return PaymentStatus(None) + return PaymentPendingStatus() if not r["pays"]: - return PaymentStatus(False) + return PaymentFailedStatus() if r["pays"][0]["payment_hash"] == checking_id: status = r["pays"][0]["status"] if status == "complete": @@ -226,10 +233,12 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus: int(r["pays"][0]["amount_sent_msat"][0:-4]) - int(r["pays"][0]["amount_msat"][0:-4]) ) - return PaymentStatus(True, fee_msat, r["pays"][0]["preimage"]) + return PaymentSuccessStatus( + fee_msat=fee_msat, preimage=r["pays"][0]["preimage"] + ) if status == "failed": - return PaymentStatus(False) - return PaymentStatus(None) + return PaymentFailedStatus() + return PaymentPendingStatus() raise KeyError("supplied an invalid checking_id") async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: diff --git a/lnbits/wallets/void.py b/lnbits/wallets/void.py index 22eba7695b..0ecfed770e 100644 --- a/lnbits/wallets/void.py +++ b/lnbits/wallets/void.py @@ -4,6 +4,7 @@ from .base import ( InvoiceResponse, + PaymentPendingStatus, PaymentResponse, PaymentStatus, StatusResponse, @@ -31,10 +32,10 @@ async def pay_invoice(self, *_, **__) -> PaymentResponse: ) async def get_invoice_status(self, *_, **__) -> PaymentStatus: - return PaymentStatus(None) + return PaymentPendingStatus() async def get_payment_status(self, *_, **__) -> PaymentStatus: - return PaymentStatus(None) + return PaymentPendingStatus() async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: yield "" diff --git a/lnbits/wallets/zbd.py b/lnbits/wallets/zbd.py new file mode 100644 index 0000000000..4dee4a86e2 --- /dev/null +++ b/lnbits/wallets/zbd.py @@ -0,0 +1,159 @@ +import asyncio +from typing import AsyncGenerator, Dict, Optional + +import httpx +from loguru import logger + +from lnbits import bolt11 +from lnbits.settings import settings + +from .base import ( + InvoiceResponse, + PaymentPendingStatus, + PaymentResponse, + PaymentStatus, + StatusResponse, + Unsupported, + Wallet, +) + + +class ZBDWallet(Wallet): + """https://zbd.dev/api-reference/""" + + def __init__(self): + if not settings.zbd_api_endpoint: + raise ValueError("cannot initialize ZBDWallet: missing zbd_api_endpoint") + if not settings.zbd_api_key: + raise ValueError("cannot initialize ZBDWallet: missing zbd_api_key") + + self.endpoint = self.normalize_endpoint(settings.zbd_api_endpoint) + headers = { + "apikey": settings.zbd_api_key, + "User-Agent": settings.user_agent, + } + self.client = httpx.AsyncClient(base_url=self.endpoint, headers=headers) + + async def cleanup(self): + try: + await self.client.aclose() + except RuntimeError as e: + logger.warning(f"Error closing wallet connection: {e}") + + async def status(self) -> StatusResponse: + try: + r = await self.client.get("wallet", timeout=10) + except (httpx.ConnectError, httpx.RequestError): + return StatusResponse(f"Unable to connect to '{self.endpoint}'", 0) + + if r.is_error: + error_message = r.json()["message"] + return StatusResponse(error_message, 0) + + data = int(r.json()["data"]["balance"]) + # ZBD returns everything as a str not int + # balance is returned in msats already in ZBD + return StatusResponse(None, data) + + async def create_invoice( + self, + amount: int, + memo: Optional[str] = None, + description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, + **kwargs, + ) -> InvoiceResponse: + # https://api.zebedee.io/v0/charges + if description_hash or unhashed_description: + raise Unsupported("description_hash") + + msats_amount = amount * 1000 + data: Dict = { + "amount": f"{msats_amount}", + "description": memo, + "expiresIn": 3600, + "callbackUrl": "", + "internalId": "", + } + + r = await self.client.post( + "charges", + json=data, + timeout=40, + ) + + if r.is_error: + error_message = r.json()["message"] + return InvoiceResponse(False, None, None, error_message) + + data = r.json()["data"] + checking_id = data["id"] # this is a zbd id + payment_request = data["invoice"]["request"] + return InvoiceResponse(True, checking_id, payment_request, None) + + async def pay_invoice( + self, bolt11_invoice: str, fee_limit_msat: int + ) -> PaymentResponse: + # https://api.zebedee.io/v0/payments + r = await self.client.post( + "payments", + json={ + "invoice": bolt11_invoice, + "description": "", + "amount": "", + "internalId": "", + "callbackUrl": "", + }, + timeout=40, + ) + + if r.is_error: + error_message = r.json()["message"] + return PaymentResponse(False, None, None, None, error_message) + + data = r.json() + + checking_id = bolt11.decode(bolt11_invoice).payment_hash + fee_msat = -int(data["data"]["fee"]) + preimage = data["data"]["preimage"] + + return PaymentResponse(True, checking_id, fee_msat, preimage, None) + + async def get_invoice_status(self, checking_id: str) -> PaymentStatus: + r = await self.client.get(f"charges/{checking_id}") + if r.is_error: + return PaymentPendingStatus() + data = r.json()["data"] + + statuses = { + "pending": None, + "paid": True, + "unpaid": None, + "expired": False, + "completed": True, + } + return PaymentStatus(statuses[data.get("status")]) + + async def get_payment_status(self, checking_id: str) -> PaymentStatus: + r = await self.client.get(f"payments/{checking_id}") + if r.is_error: + return PaymentPendingStatus() + + data = r.json()["data"] + + statuses = { + "initial": None, + "pending": None, + "completed": True, + "error": None, + "expired": False, + "failed": False, + } + + return PaymentStatus(statuses[data.get("status")], fee_msat=None, preimage=None) + + async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: + self.queue: asyncio.Queue = asyncio.Queue(0) + while True: + value = await self.queue.get() + yield value diff --git a/nix/modules/lnbits-service.nix b/nix/modules/lnbits-service.nix index dd9beca27b..e1cedf4a7b 100644 --- a/nix/modules/lnbits-service.nix +++ b/nix/modules/lnbits-service.nix @@ -62,6 +62,17 @@ in default = "lnbits"; description = "group to run lnbits as"; }; + env = mkOption { + type = types.attrsOf types.str; + default = {}; + description = '' + Additional environment variables that are passed to lnbits. + Reference Variables: https://github.com/lnbits/lnbits/blob/dev/.env.example + ''; + example = { + LNBITS_ADMIN_UI = "true"; + }; + }; }; }; @@ -86,11 +97,14 @@ in description = "lnbits"; wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" ]; - environment = { - LNBITS_DATA_FOLDER = "${cfg.stateDir}"; - LNBITS_EXTENSIONS_PATH = "${cfg.stateDir}/extensions"; - LNBITS_PATH = "${cfg.package.src}"; - }; + environment = lib.mkMerge [ + { + LNBITS_DATA_FOLDER = "${cfg.stateDir}"; + LNBITS_EXTENSIONS_PATH = "${cfg.stateDir}/extensions"; + LNBITS_PATH = "${cfg.package.src}"; + } + cfg.env + ]; serviceConfig = { User = cfg.user; Group = cfg.group; diff --git a/package-lock.json b/package-lock.json index dd44439614..33cd31162f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "name": "lnbits", "dependencies": { "@chenfengyuan/vue-qrcode": "1.0.2", - "axios": "^1.3.4", + "axios": "^1.6.0", "chart.js": "2.9", "moment": "^2.29.4", "quasar": "1.13.2", @@ -31,7 +31,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@chenfengyuan/vue-qrcode/-/vue-qrcode-1.0.2.tgz", "integrity": "sha512-hwy1d4YMJAyEh+V7dLPG8eAKACRvugzSB4ylwb6QNqo84KHTF50/5EJcBYdUhTRPfAqrxG0i6jDAXONWOGyQbQ==", - "license": "MIT", "dependencies": { "qrcode": "^1.4.4" }, @@ -40,9 +39,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", @@ -54,9 +53,9 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "dev": true, "engines": { "node": ">=6.0.0" @@ -72,9 +71,9 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", @@ -82,25 +81,25 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -113,7 +112,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", "engines": { "node": ">=8" } @@ -122,7 +120,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -133,18 +130,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/ansi-styles/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -164,9 +149,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz", - "integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -203,8 +188,7 @@ "node_modules/callforth": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/callforth/-/callforth-0.3.1.tgz", - "integrity": "sha512-Q2zPfqnwoKsb1DTVCr4lmhe49wKNBsMmNlbudjleu3/co+Nw1pOqFHYJHrW3VZ253ou9AAr+xauQR0C55NPdzA==", - "license": "MIT" + "integrity": "sha512-Q2zPfqnwoKsb1DTVCr4lmhe49wKNBsMmNlbudjleu3/co+Nw1pOqFHYJHrW3VZ253ou9AAr+xauQR0C55NPdzA==" }, "node_modules/camel-case": { "version": "4.1.2", @@ -220,7 +204,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "license": "MIT", "engines": { "node": ">=6" } @@ -229,7 +212,6 @@ "version": "2.9.4", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz", "integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==", - "license": "MIT", "dependencies": { "chartjs-color": "^2.1.0", "moment": "^2.10.2" @@ -239,7 +221,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz", "integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==", - "license": "MIT", "dependencies": { "chartjs-color-string": "^0.6.0", "color-convert": "^1.9.3" @@ -249,22 +230,28 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz", "integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==", - "license": "MIT", "dependencies": { "color-name": "^1.0.0" } }, + "node_modules/chartjs-color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/chartjs-color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -277,14 +264,17 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "node_modules/clean-css": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", - "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", "dev": true, "dependencies": { "source-map": "~0.6.0" @@ -297,7 +287,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -305,25 +294,20 @@ } }, "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "node_modules/color-convert/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -358,9 +342,9 @@ } }, "node_modules/core-js": { - "version": "3.30.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.2.tgz", - "integrity": "sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.1.tgz", + "integrity": "sha512-IgdsbxNyMskrTFxa9lWHyMwAJU5gXOPP+1yO+K59d50VLVAIDAbs7gIv705KzALModfK3ZrSZTPNpC0PQgIZuw==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -400,7 +384,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -414,10 +397,9 @@ } }, "node_modules/dijkstrajs": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.2.tgz", - "integrity": "sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==", - "license": "MIT" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" }, "node_modules/dot-case": { "version": "3.0.4", @@ -432,19 +414,17 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/encode-utf8": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", - "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", - "license": "MIT" + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" }, "node_modules/entities": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", - "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "engines": { "node": ">=0.12" @@ -469,7 +449,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -479,16 +458,15 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], - "license": "MIT", "engines": { "node": ">=4.0" }, @@ -512,9 +490,9 @@ } }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -529,7 +507,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -547,14 +524,14 @@ } }, "node_modules/html-minifier-terser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.1.0.tgz", - "integrity": "sha512-BvPO2S7Ip0Q5qt+Y8j/27Vclj6uHC6av0TMoDn7/bJPhMWHI2UtR2e/zEgJn3/qYAmxumrGp9q4UHurL6mtW9Q==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", + "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", "dev": true, "dependencies": { "camel-case": "^4.1.2", - "clean-css": "5.2.0", - "commander": "^9.4.1", + "clean-css": "~5.3.2", + "commander": "^10.0.0", "entities": "^4.4.0", "param-case": "^3.0.4", "relateurl": "^0.2.7", @@ -567,31 +544,19 @@ "node": "^14.13.1 || >=16.0.0" } }, - "node_modules/html-minifier-terser/node_modules/clean-css": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.0.tgz", - "integrity": "sha512-2639sWGa43EMmG7fn8mdVuBSs6HuWaSor+ZPoFWzenBc6oN+td8YhTfghWXZ25G1NiiSvz8bOFBS7PdSbTiqEA==", - "dev": true, - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 10.0" - } - }, "node_modules/html-minifier-terser/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true, "engines": { - "node": "^12.20.0 || >=14" + "node": ">=14" } }, "node_modules/immutable": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", - "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", "dev": true }, "node_modules/is-binary-path": { @@ -619,7 +584,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", "engines": { "node": ">=8" } @@ -655,7 +619,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -822,7 +785,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -837,7 +799,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -849,7 +810,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "license": "MIT", "engines": { "node": ">=6" } @@ -878,7 +838,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", "engines": { "node": ">=8" } @@ -899,7 +858,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", - "license": "MIT", "engines": { "node": ">=10.13.0" } @@ -909,7 +867,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz", "integrity": "sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==", "dev": true, - "license": "MIT", "bin": { "prettier": "bin-prettier.js" }, @@ -930,7 +887,6 @@ "resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.289.tgz", "integrity": "sha512-fG3STxnwAt3i7bxbXUPJdYNFrcOWHLwCSEOySH2foUqtYdzWLcxDez0Kgl1X8LMQx0arMJ6HRkKghxfRD1/z6g==", "dev": true, - "license": "MIT", "bin": { "pyright": "index.js", "pyright-langserver": "langserver.index.js" @@ -940,10 +896,9 @@ } }, "node_modules/qrcode": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.1.tgz", - "integrity": "sha512-nS8NJ1Z3md8uTjKtP+SGGhfqmTCs5flU/xR623oI0JX+Wepz9R8UrRVCTBTJm3qGw3rH6jJ6MUHjkDx15cxSSg==", - "license": "MIT", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", + "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", "dependencies": { "dijkstrajs": "^1.0.1", "encode-utf8": "^1.0.3", @@ -1009,7 +964,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1017,8 +971,7 @@ "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "license": "ISC" + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, "node_modules/rtcpeerconnection-shim": { "version": "1.2.15", @@ -1057,8 +1010,7 @@ "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, "node_modules/showdown": { "version": "2.1.0", @@ -1128,7 +1080,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -1142,7 +1093,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -1151,13 +1101,13 @@ } }, "node_modules/terser": { - "version": "5.16.8", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.8.tgz", - "integrity": "sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", + "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", "dev": true, "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -1199,9 +1149,9 @@ } }, "node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, "node_modules/underscore": { @@ -1212,7 +1162,8 @@ "node_modules/vue": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz", - "integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg==" + "integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg==", + "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details." }, "node_modules/vue-i18n": { "version": "8.28.2", @@ -1257,16 +1208,14 @@ } }, "node_modules/which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", - "license": "ISC" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -1279,14 +1228,12 @@ "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC" + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" }, "node_modules/yargs": { "version": "15.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "license": "MIT", "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", @@ -1308,7 +1255,6 @@ "version": "18.1.3", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "license": "ISC", "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" diff --git a/package.json b/package.json index 0b63ce71ce..dec351fcf3 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "sass": "./node_modules/.bin/sass ./lnbits/static/scss/base.scss > ./lnbits/static/css/base.css", "vendor_copy": "node -e \"require('./package.json').vendor.forEach((file) => require('fs').copyFileSync(file, './lnbits/static/vendor/'+file.split('/').pop()))\"", "vendor_json": "node -e \"require('fs').writeFileSync('./lnbits/static/vendor.json', JSON.stringify(require('./package.json').bundle))\"", - "vendor_bundle_css": "node -e \"require('concat')(require('./package.json').bundle.css.map(a => './lnbits/'+a), './lnbits/static/bundle.css')\"", - "vendor_bundle_js": "node -e \"require('concat')(require('./package.json').bundle.js.map(a => './lnbits/'+a), './lnbits/static/bundle.js')\"", + "vendor_bundle_css": "node -e \"require('concat')(require('./package.json').bundle.css.map(a => 'lnbits/static/'+a), './lnbits/static/bundle.css')\"", + "vendor_bundle_js": "node -e \"require('concat')(require('./package.json').bundle.js.map(a => 'lnbits/static/'+a),'./lnbits/static/bundle.js')\"", "vendor_minify_css": "./node_modules/.bin/minify ./lnbits/static/bundle.css > ./lnbits/static/bundle.min.css", "vendor_minify_js": "./node_modules/.bin/minify ./lnbits/static/bundle.js > ./lnbits/static/bundle.min.js" }, @@ -18,7 +18,7 @@ }, "dependencies": { "@chenfengyuan/vue-qrcode": "1.0.2", - "axios": "^1.3.4", + "axios": "^1.6.0", "chart.js": "2.9", "moment": "^2.29.4", "quasar": "1.13.2", @@ -49,44 +49,50 @@ ], "bundle": { "js": [ - "/static/vendor/moment.js", - "/static/vendor/underscore.js", - "/static/vendor/axios.js", - "/static/vendor/vue.js", - "/static/vendor/vue-router.js", - "/static/vendor/VueQrcodeReader.umd.js", - "/static/vendor/vue-qrcode.js", - "/static/vendor/vuex.js", - "/static/vendor/quasar.ie.polyfills.umd.min.js", - "/static/vendor/quasar.umd.js", - "/static/vendor/Chart.bundle.js", - "/static/vendor/vue-i18n.js", - "/static/vendor/showdown.js", - "/static/i18n/i18n.js", - "/static/i18n/de.js", - "/static/i18n/en.js", - "/static/i18n/es.js", - "/static/i18n/fr.js", - "/static/i18n/it.js", - "/static/i18n/jp.js", - "/static/i18n/cn.js", - "/static/i18n/nl.js", - "/static/i18n/pi.js", - "/static/i18n/pl.js", - "/static/i18n/fr.js", - "/static/i18n/nl.js", - "/static/i18n/we.js", - "/static/i18n/pt.js", - "/static/i18n/br.js", - "/static/js/base.js", - "/static/js/components.js", - "/static/js/components/lnbits-funding-sources.js", - "/static/js/bolt11-decoder.js" + "vendor/moment.js", + "vendor/underscore.js", + "vendor/axios.js", + "vendor/vue.js", + "vendor/vue-router.js", + "vendor/VueQrcodeReader.umd.js", + "vendor/vue-qrcode.js", + "vendor/vuex.js", + "vendor/quasar.ie.polyfills.umd.min.js", + "vendor/quasar.umd.js", + "vendor/Chart.bundle.js", + "vendor/vue-i18n.js", + "vendor/showdown.js", + "i18n/i18n.js", + "i18n/de.js", + "i18n/en.js", + "i18n/es.js", + "i18n/fr.js", + "i18n/it.js", + "i18n/jp.js", + "i18n/cn.js", + "i18n/nl.js", + "i18n/pi.js", + "i18n/pl.js", + "i18n/fr.js", + "i18n/nl.js", + "i18n/we.js", + "i18n/pt.js", + "i18n/br.js", + "i18n/cs.js", + "i18n/sk.js", + "i18n/kr.js", + "i18n/fi.js", + "js/base.js", + "js/components.js", + "js/components/lnbits-funding-sources.js", + "js/components/extension-settings.js", + "js/event-reactions.js", + "js/bolt11-decoder.js" ], "css": [ - "/static/vendor/quasar.css", - "/static/vendor/Chart.css", - "/static/css/base.css" + "vendor/quasar.css", + "vendor/Chart.css", + "css/base.css" ] } } diff --git a/poetry.lock b/poetry.lock index bbbee44a53..f06dfebde2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "anyio" @@ -21,6 +21,20 @@ doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd- test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (<0.22)"] +[[package]] +name = "asgi-lifespan" +version = "2.1.0" +description = "Programmatic startup/shutdown of ASGI apps." +optional = false +python-versions = ">=3.7" +files = [ + {file = "asgi-lifespan-2.1.0.tar.gz", hash = "sha256:5e2effaf0bfe39829cf2d64e7ecc47c7d86d676a6599f7afba378c31f5e3a308"}, + {file = "asgi_lifespan-2.1.0-py3-none-any.whl", hash = "sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f"}, +] + +[package.dependencies] +sniffio = "*" + [[package]] name = "asn1crypto" version = "1.5.1" @@ -64,6 +78,36 @@ files = [ [package.extras] tests = ["PyHamcrest (>=2.0.2)", "mypy", "pytest (>=4.6)", "pytest-benchmark", "pytest-cov", "pytest-flake8"] +[[package]] +name = "bcrypt" +version = "4.1.1" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "bcrypt-4.1.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:196008d91201bbb1aa4e666fee5e610face25d532e433a560cabb33bfdff958b"}, + {file = "bcrypt-4.1.1-cp37-abi3-macosx_13_0_universal2.whl", hash = "sha256:2e197534c884336f9020c1f3a8efbaab0aa96fc798068cb2da9c671818b7fbb0"}, + {file = "bcrypt-4.1.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d573885b637815a7f3a3cd5f87724d7d0822da64b0ab0aa7f7c78bae534e86dc"}, + {file = "bcrypt-4.1.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab33473f973e8058d1b2df8d6e095d237c49fbf7a02b527541a86a5d1dc4444"}, + {file = "bcrypt-4.1.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fb931cd004a7ad36a89789caf18a54c20287ec1cd62161265344b9c4554fdb2e"}, + {file = "bcrypt-4.1.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:12f40f78dcba4aa7d1354d35acf45fae9488862a4fb695c7eeda5ace6aae273f"}, + {file = "bcrypt-4.1.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2ade10e8613a3b8446214846d3ddbd56cfe9205a7d64742f0b75458c868f7492"}, + {file = "bcrypt-4.1.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f33b385c3e80b5a26b3a5e148e6165f873c1c202423570fdf45fe34e00e5f3e5"}, + {file = "bcrypt-4.1.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:755b9d27abcab678e0b8fb4d0abdebeea1f68dd1183b3f518bad8d31fa77d8be"}, + {file = "bcrypt-4.1.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a7a7b8a87e51e5e8ca85b9fdaf3a5dc7aaf123365a09be7a27883d54b9a0c403"}, + {file = "bcrypt-4.1.1-cp37-abi3-win32.whl", hash = "sha256:3d6c4e0d6963c52f8142cdea428e875042e7ce8c84812d8e5507bd1e42534e07"}, + {file = "bcrypt-4.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:14d41933510717f98aac63378b7956bbe548986e435df173c841d7f2bd0b2de7"}, + {file = "bcrypt-4.1.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:24c2ebd287b5b11016f31d506ca1052d068c3f9dc817160628504690376ff050"}, + {file = "bcrypt-4.1.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:476aa8e8aca554260159d4c7a97d6be529c8e177dbc1d443cb6b471e24e82c74"}, + {file = "bcrypt-4.1.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:12611c4b0a8b1c461646228344784a1089bc0c49975680a2f54f516e71e9b79e"}, + {file = "bcrypt-4.1.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6450538a0fc32fb7ce4c6d511448c54c4ff7640b2ed81badf9898dcb9e5b737"}, + {file = "bcrypt-4.1.1.tar.gz", hash = "sha256:df37f5418d4f1cdcff845f60e747a015389fa4e63703c918330865e06ad80007"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + [[package]] name = "bech32" version = "1.2.0" @@ -75,47 +119,180 @@ files = [ {file = "bech32-1.2.0.tar.gz", hash = "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899"}, ] +[[package]] +name = "bitarray" +version = "2.8.4" +description = "efficient arrays of booleans -- C extension" +optional = false +python-versions = "*" +files = [ + {file = "bitarray-2.8.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:69be498a39ac29ea4f8e4dce36e64342d4fe813eeffa7bd9ead4ce18309fb903"}, + {file = "bitarray-2.8.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6acbfa5b85717c91bfa1bc1702c1cc6a3d1500f832f2c3c040f0d4668c75b2b5"}, + {file = "bitarray-2.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:305f1aa2a3aedd033ab2ab1fc930c5f0a987bf993f3ecc83a224db237a95cd18"}, + {file = "bitarray-2.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8f7a1a4793c4dec2dc7c6c1fac5370123a24c6dabc7312fbce8766a0d5c40c8"}, + {file = "bitarray-2.8.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87acfa228524b8564ba5d5a431ff6c708721ff7755f718992184bb9a81365f0e"}, + {file = "bitarray-2.8.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45782033c17ea2d1c9967128bc69aee1417210b104fbda35d4da77d907afb3c5"}, + {file = "bitarray-2.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f343ea39f61f899bac145aac260dd456a98df59e4258ad8d395892b6b4759b20"}, + {file = "bitarray-2.8.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52c8501aa71a353dbe8dd6440bbd3449d8ffcae843bff139f87b9a84149315ce"}, + {file = "bitarray-2.8.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bb23c806f175a88db60b3894bca4956f6d557ed0571b2fcc7818c1c83f000759"}, + {file = "bitarray-2.8.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:efbe1d873f916fa31235b8acec6a686e7b7e47e3e95490cbe8b257dabaa14d3b"}, + {file = "bitarray-2.8.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0690c5483e31d7e4d7f26b045baf7f9d0aa30e91fcf1c5117095652d149b1a96"}, + {file = "bitarray-2.8.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2cc0c09edd3fa893303513d3fb9a0d335f20b19b3f0276fe752bf88ffd5522c0"}, + {file = "bitarray-2.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:045b813c3567818673f2dcd9c0b41a63214c5f5a9c230ede76ac211fbcf0185a"}, + {file = "bitarray-2.8.4-cp310-cp310-win32.whl", hash = "sha256:ddfd3632e5f04619d780f60e85a5fe082a8eebce33aefb08b6783779ff04d017"}, + {file = "bitarray-2.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:c19c27056cb34b352c064ac0d58ac7f7da29cd225cb3140b8ff69455e6858966"}, + {file = "bitarray-2.8.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6d83fda6e4d83742d60f522ce3bd61ce0d4690c19b73dc79ee8da2a48f2ef065"}, + {file = "bitarray-2.8.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de1f491b329e424f7e7b2874624a604c163ea05341f709cd47c1a46f4930ca97"}, + {file = "bitarray-2.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c8e25c5530bd6bb5a96ad11de2dc16bebbbec8b4e2c1600bf1ce78cbf36c96e6"}, + {file = "bitarray-2.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:602b429cedf3631cb3b36a7e08f484972b2e13bb0fc1e240b71935aef32bb9d9"}, + {file = "bitarray-2.8.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:102b988fdbb0b221bdb71dac7d96475bfa47a767ee0fc1014a9ad5be46ebd20b"}, + {file = "bitarray-2.8.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:665f88d204d4a8fd0fe63fea66c1a420b331887e72a2b10778d97d22182e8474"}, + {file = "bitarray-2.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:965e52d079e8746abe6d15e8b1da7b65d9f1ccb5bceb1aa410072f09a1cdb3fd"}, + {file = "bitarray-2.8.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b619c691c94f2770373a91144bbbe42056a993fa95aba67f87a7625f71384040"}, + {file = "bitarray-2.8.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c27b57205a6946de4dedb169d42f63d8f61e51a70e3096ffce18680c8407616c"}, + {file = "bitarray-2.8.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2f8484bea6cededfacc2921fd5715e3132467d1df50f941635b91c9920dfd66f"}, + {file = "bitarray-2.8.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e7b705a7c3bb5c7a86a2e4bf5d4607e22194d821e050b5f5605a69ded99dc5c3"}, + {file = "bitarray-2.8.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:afb4e6edfeb6797165a25e5ea221992043c46b3475f7d4d96e2c25271dfea4d8"}, + {file = "bitarray-2.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2bb731fe68c07d5a3aeb9df798008e41999c933ed81786c7688b190f5082e079"}, + {file = "bitarray-2.8.4-cp311-cp311-win32.whl", hash = "sha256:22a0d11bf53553e2de20eb1dbf507bba32a6c28a2b84232ff5f28289ba9ec659"}, + {file = "bitarray-2.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:8ace24f1b028dee7168556e0a83c1f608abe63f4b82dc05b26ad43495d8717bf"}, + {file = "bitarray-2.8.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1009f6a4117d50c2e9e4a2d6d5a03d0cb030f649dd410fbbef4d3f3a9aca40c9"}, + {file = "bitarray-2.8.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9a53bf859e4b54ad06dda20aa42a16dd36b03f11626beacc41b570f25cfcb471"}, + {file = "bitarray-2.8.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4991d8c2b4ccccd1ea9115dae5dc51b60b562bc627784e53c31aae5699a55932"}, + {file = "bitarray-2.8.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7832ecd8adc5ef9f0af7c376ea4ab8ba66077da45e1d00da9f93366cbfb70dfe"}, + {file = "bitarray-2.8.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:644fc022c5b3973472e39930f43c113865f9ba1b4e918b52f5921d709af0e9e3"}, + {file = "bitarray-2.8.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99cd2f5dd46e886a63bc08dbb44ae63b16eeff94d714be55ce41ff86604bbc97"}, + {file = "bitarray-2.8.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e938552f8fd83ecdde6f43d7f91854fa2604cc7c7e2929fed78c3779c843ba6"}, + {file = "bitarray-2.8.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6910610f1f54e7b9e5aa5311acff812e5ae2ca5f6c041a40c9201f768c4a6893"}, + {file = "bitarray-2.8.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8becb576748328b2fdf9740a973e62f41de83702a92761e0ff441b65ebe25fce"}, + {file = "bitarray-2.8.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8b1c84803dd57f8b81397dcc24eca73bc44f1c5da36b440f358372b50c7bb7da"}, + {file = "bitarray-2.8.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:2e15d244cb7dab42cb1f31933da3b66d6405b1db969917460c094ba8441ea5a0"}, + {file = "bitarray-2.8.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:55d52dd5af45dfb09e9b107749b4fcad4a3774d5429345faa47ab459ae478de0"}, + {file = "bitarray-2.8.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ef2dbbb2924c5048bea586ddf204607c8e91fbe70b95a7dce1d5b5403f2ef06f"}, + {file = "bitarray-2.8.4-cp312-cp312-win32.whl", hash = "sha256:7ecd20dfef83d3180d9f851476e5e3d9a76973e24432721f7cc8cac52a646d3a"}, + {file = "bitarray-2.8.4-cp312-cp312-win_amd64.whl", hash = "sha256:e7ac4f3cc1cdbe5b31bce988260ac12ae0e273ec6108bf35de66384599fabc25"}, + {file = "bitarray-2.8.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ebdaea27ada24e53d673c46a8a4bba8e1904fa8589512bd3146382d877ab4be9"}, + {file = "bitarray-2.8.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf6e39e296422588c39eaa8bea17c3d5af9335c7273691615e6aa262f3a1c469"}, + {file = "bitarray-2.8.4-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bbd70d2a0be93349ee76652992164d89dab54e55cb05d302d4375851b60d173"}, + {file = "bitarray-2.8.4-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed4e54d4425c9f5eb766ff8ee4b992fe0011575a7da5daa8bf898675c684808c"}, + {file = "bitarray-2.8.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f18e53a47619ef092cb28ac1f1f2b457ad68177369a5c02a1da930f5f0a43d78"}, + {file = "bitarray-2.8.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a1232d989dc37b2b0d760ed3cd040f848a7578417d0bda24e544e73f5d6b02a"}, + {file = "bitarray-2.8.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3df0ac492e4e795e26710ee20cfd25c7bfd81c3866490078fcc7d97ccc74b01f"}, + {file = "bitarray-2.8.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:06d9ad81d56547b2b256e70a819eb4eefa4e7e21595b06b4102666a71eb4b961"}, + {file = "bitarray-2.8.4-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:7bc9808782e3147fb71b44129f3dfabfbe839bc35954f9f7f3dd8dd4c149413c"}, + {file = "bitarray-2.8.4-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:56cc56c382167360a94e36a54a3a14320ecbe9e8ca672574c739703136d0b5e0"}, + {file = "bitarray-2.8.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:de8b30056fe36947d50597680aa016f5a9a59f2621b496ca0fe8ad037ee63f76"}, + {file = "bitarray-2.8.4-cp36-cp36m-win32.whl", hash = "sha256:d80a356e6123b0910171ab7b2ce4d058146170748f11b7ec3c005da54bfbc059"}, + {file = "bitarray-2.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:00bb1de6d3c68e18fb16c6c7390e68bc656a60dfde4004d5649c792b8871a531"}, + {file = "bitarray-2.8.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6db1bc132b3ee46bb79a1d86bfadce71d581943156004e481045ce903f1979db"}, + {file = "bitarray-2.8.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27b824ae449cd38c8c77349ae7d27dc11662c9c40806729943dd175c91334a4c"}, + {file = "bitarray-2.8.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7947134bc4b28a00c07616e07013680628954bc93daa4cbab99a6d7aea402809"}, + {file = "bitarray-2.8.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c392d44cc072255e88efc4335be67ebdfb88ae4b3757bd573c49fae35e23470"}, + {file = "bitarray-2.8.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c787dbacf218cde121611706e2bb6a64e3fb566a828bab7d608c6c96cfec8a4"}, + {file = "bitarray-2.8.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c59e589f162dffb8bea47fb8108961891df5d54d3a1c59660f211a53084438cd"}, + {file = "bitarray-2.8.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d1efcf28571909ea4c12184d51bd953370fd28ec227b1ded7cb88563c17d42a"}, + {file = "bitarray-2.8.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:afe8bedc9893a358a29430e98164a902816fd7787f92b476193a0de7aae4b985"}, + {file = "bitarray-2.8.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:64e6e316452d8018d11954698f9898a2ee69fe2f1093333c2650a4b91246c675"}, + {file = "bitarray-2.8.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:02f8002eac8ba7796e05690f317056c6ddd40ac88f73d1dd3405c5d4df15a61d"}, + {file = "bitarray-2.8.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d26fdf9d680eb9738e8b58ab7946cb35ed8b076dac823189f2614d732565e89a"}, + {file = "bitarray-2.8.4-cp37-cp37m-win32.whl", hash = "sha256:9e52a186b68b5995c3756f243e286ea701470273ba938b9f83a0ef055edeb95e"}, + {file = "bitarray-2.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:3baf866f2674241b02ab9547acaae2f705e7e9ca5a620484e8b09a25fc625def"}, + {file = "bitarray-2.8.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2c9d06891a565bdc572dc8a2d76275fc3d51e63ddff51c3e03a9a95b600ca673"}, + {file = "bitarray-2.8.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:242f9ddfed9e7b70edb2888056af1710dfbf3767342d6ef1c110fe1d3b346ad6"}, + {file = "bitarray-2.8.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9245d3181346f1f608b56cb45fb21c080558426dac566074a2c4145daa411588"}, + {file = "bitarray-2.8.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eacc7d5ad4b120131da4c6cecd8ded5e545dab3654de592cf8901a7acfd58c18"}, + {file = "bitarray-2.8.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:228e20443c841585454e95e17cf66610c9c53c3a1c66f3a9bc90a1ce31218b9d"}, + {file = "bitarray-2.8.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3449769a8f6c6a39d3d8c8760d265ff754107715c8ad3d66e90961ea463e6284"}, + {file = "bitarray-2.8.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25e882da07d5735ee089cec12dc75d55b90434e607aae5522515f23132612091"}, + {file = "bitarray-2.8.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80eca1ef96a3b379026bcf531d7cbfbfad767da37ba4e90bc529e6695f88ba09"}, + {file = "bitarray-2.8.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6c8ebc5b2cf89b4dd2d407312eeec4ed1f999863a6d29d1d1834696f6db08ac8"}, + {file = "bitarray-2.8.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9849d06f254fffd45d35ba2b39694dbc839f6c5cca8990a509b3058588f23d77"}, + {file = "bitarray-2.8.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:2ff712ba8259599135d24fcc555dbca2dc32ff5d18e8efb8d47456d2467e630f"}, + {file = "bitarray-2.8.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e3fc442c3ae66c4f9a0d35f2c2a0e36f6a9c125b94c3db1ee8fa5af4dca51a57"}, + {file = "bitarray-2.8.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:275f4deacd4cee28311cee611cea64bf5ec197da4a95c23b2af00ecc1dee6e97"}, + {file = "bitarray-2.8.4-cp38-cp38-win32.whl", hash = "sha256:b349092caf10b6b0585e0ff0ed17e5fc8a88c3bdacb37b38778de4a1ae568827"}, + {file = "bitarray-2.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:ed37c74e33c67f805e046c0e4d1af2007d4087d01748fa47a56ee3501c1bb597"}, + {file = "bitarray-2.8.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3be310edafc506a4f7c405d7d2d97274ab3ec3f2cbd2793705ccdb692559a009"}, + {file = "bitarray-2.8.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c334ab66657dc0250281d1deaaa0243bb2072da0939fc89cbce4513a79b7ebdc"}, + {file = "bitarray-2.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c6ab07a20fe548f4830bc3d795d4e8193616379abb8715fcf0391ca599cf4f4b"}, + {file = "bitarray-2.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3994b319e6f18040652b769ceb09e28b5546bffa29138019b788bafa8577478f"}, + {file = "bitarray-2.8.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803dc8ca520db0db6e14bc61c52666a2344b5ff45c9c4524967f1920779ef64f"}, + {file = "bitarray-2.8.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a2f6cd2861922bf951451cd19c0f658d93ac313424ec705c59768626eb4b1f0"}, + {file = "bitarray-2.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98df463206a76ef02d8662490bafc6ca2d6dec10cfff3dda90798c0e4f330151"}, + {file = "bitarray-2.8.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da7086efb3d388078874b2fafd5042a5c6464e08ecb68bf3813c3b9d54d236b4"}, + {file = "bitarray-2.8.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:343876f1a38d9b2857f18f7d606be49b11344dc3b9c46f072760dec364a35a54"}, + {file = "bitarray-2.8.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0488c1eaf83c993fc672115176cc6c27070d5abd5e673499ed46eeb87de52169"}, + {file = "bitarray-2.8.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:aadc4b8ac31ef4ac31f13ab416d5891ff1886b0c3115e88b4be53d3ce08e235f"}, + {file = "bitarray-2.8.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:bb05962feb649bbb5589eab89b9fa57679ce8285e647195bee76c8c0821fcf22"}, + {file = "bitarray-2.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:67accba68ceb3cb57bae9ed86ddd075043e373c4af6243e20c8f00153c5f374a"}, + {file = "bitarray-2.8.4-cp39-cp39-win32.whl", hash = "sha256:0adf959b63e314ea74c7d67ca6732c2a840769a7bcfe779d52d777ac6877d671"}, + {file = "bitarray-2.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:d0fc43f5f5ae113ad60b502ec1efee42218c21a1e00dd1bd7c82d00b25cf72ad"}, + {file = "bitarray-2.8.4-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:63e1bb1c98d6d3004e44cb1958393c631e79c640877086a7f403c223c18687cb"}, + {file = "bitarray-2.8.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea66a30fb0b9d3109db950b490f6aa211fb15162f097b20141b1aeb5057a670"}, + {file = "bitarray-2.8.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8aaea18c41dacf2bf1a6f81960c196f85e3991c9387c3d9bff97976be2c195a4"}, + {file = "bitarray-2.8.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad2b129e43998292f89f138dfda32ec1b9ba31e68b35a61948bc10bf53e94444"}, + {file = "bitarray-2.8.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6647e03def035371ce0ce073912d6594ed197f799aa34641f0acce343a8f7cca"}, + {file = "bitarray-2.8.4-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d31416540af1ad2994a33cf7f2e98e1e8f50722e410afc54ae99bdd6039a4f87"}, + {file = "bitarray-2.8.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c936d73deca901b600fb73c9aaf3630dd358f5ce35c5d5e1ea804b33796ecb5"}, + {file = "bitarray-2.8.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc936c0cea105c7773e6b8cc58ed2a3b168a3da9bbdec7466cee9725198607a9"}, + {file = "bitarray-2.8.4-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9978b0968acbc2d9160758e9f63af0fbda62f121ae596ad56cb06a8afd3d5aea"}, + {file = "bitarray-2.8.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:dcae87cbf2058a33286ce50e627bdd1a4875579103f6b933546ffb1a34ab8c2e"}, + {file = "bitarray-2.8.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b5d1d4300706891d197cf21b39f41b3c8047d081676d82eb8dcfeb8d0073c52b"}, + {file = "bitarray-2.8.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7095d8f486435ffcc42014aebba27c05b2a3b38d5d3630ebe77734db7653b272"}, + {file = "bitarray-2.8.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06bcd5c171ffedb7544ad9e5b77827cd3a3ccb0dd924ef703802743b8abcf303"}, + {file = "bitarray-2.8.4-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6328f73d4e623d4fff966cbe623f3e2b3378bdbfb6937ec492aba3fd9927862f"}, + {file = "bitarray-2.8.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2d20ee30ea7640df29013021d130bee932d701f01b2f1cbbc1ba14f3954a6b1f"}, + {file = "bitarray-2.8.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:91a570f291a4d7ea4473f37b5e1ce377d771a8567a7a6b5f7b482023bd81b3ef"}, + {file = "bitarray-2.8.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18561539cf8ca5d1970b2b78a44a1b12ae21a18183664a080525c081a44b3997"}, + {file = "bitarray-2.8.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1abea439874652c3ad6ca6a6e893cfe4f2e2c149294dbe2a5c1cf7e2e1ef200"}, + {file = "bitarray-2.8.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a0f347672c5a8b67c36937872c75baec81e351f2209dc691608d3f76fa9e44e"}, + {file = "bitarray-2.8.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cdd58e73a2e1bff848067a65afb77a7dcd1884050c22d18a0a7af5cf2428a3ee"}, + {file = "bitarray-2.8.4.tar.gz", hash = "sha256:2c0ba71445ee0932e510f1b0248f53b7a52926f1f78c93b868fcbe6536e61a1d"}, +] + [[package]] name = "bitstring" -version = "3.1.9" +version = "4.1.4" description = "Simple construction, analysis and modification of binary data." optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "bitstring-3.1.9-py2-none-any.whl", hash = "sha256:e3e340e58900a948787a05e8c08772f1ccbe133f6f41fe3f0fa19a18a22bbf4f"}, - {file = "bitstring-3.1.9-py3-none-any.whl", hash = "sha256:0de167daa6a00c9386255a7cac931b45e6e24e0ad7ea64f1f92a64ac23ad4578"}, - {file = "bitstring-3.1.9.tar.gz", hash = "sha256:a5848a3f63111785224dca8bb4c0a75b62ecdef56a042c8d6be74b16f7e860e7"}, + {file = "bitstring-4.1.4-py3-none-any.whl", hash = "sha256:da46c4d6f8f3fb75a85566fdd33d5083ba8b8f268ed76f34eefe5a00da426192"}, + {file = "bitstring-4.1.4.tar.gz", hash = "sha256:94f3f1c45383ebe8fd4a359424ffeb75c2f290760ae8fcac421b44f89ac85213"}, ] +[package.dependencies] +bitarray = ">=2.8.0,<3.0.0" + [[package]] name = "black" -version = "23.9.1" +version = "24.3.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-23.9.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301"}, - {file = "black-23.9.1-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100"}, - {file = "black-23.9.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71"}, - {file = "black-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7"}, - {file = "black-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80"}, - {file = "black-23.9.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f"}, - {file = "black-23.9.1-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe"}, - {file = "black-23.9.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186"}, - {file = "black-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f"}, - {file = "black-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300"}, - {file = "black-23.9.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948"}, - {file = "black-23.9.1-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855"}, - {file = "black-23.9.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204"}, - {file = "black-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377"}, - {file = "black-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573"}, - {file = "black-23.9.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c"}, - {file = "black-23.9.1-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325"}, - {file = "black-23.9.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393"}, - {file = "black-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9"}, - {file = "black-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f"}, - {file = "black-23.9.1-py3-none-any.whl", hash = "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9"}, - {file = "black-23.9.1.tar.gz", hash = "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d"}, + {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, + {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, + {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, + {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, + {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, + {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, + {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, + {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, + {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, + {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, + {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, + {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, + {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, + {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, + {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, + {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, + {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, + {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, + {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, + {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, + {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, + {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, ] [package.dependencies] @@ -129,7 +306,7 @@ typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] @@ -154,13 +331,13 @@ secp256k1 = "*" [[package]] name = "certifi" -version = "2023.7.22" +version = "2023.11.17" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, ] [[package]] @@ -240,101 +417,101 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.3.0" +version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.3.0.tar.gz", hash = "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-win32.whl", hash = "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-win32.whl", hash = "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-win32.whl", hash = "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-win32.whl", hash = "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-win32.whl", hash = "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-win32.whl", hash = "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884"}, - {file = "charset_normalizer-3.3.0-py3-none-any.whl", hash = "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2"}, + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [[package]] @@ -486,34 +663,34 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "41.0.4" +version = "41.0.7" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839"}, - {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f"}, - {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714"}, - {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb"}, - {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13"}, - {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143"}, - {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397"}, - {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860"}, - {file = "cryptography-41.0.4-cp37-abi3-win32.whl", hash = "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd"}, - {file = "cryptography-41.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d"}, - {file = "cryptography-41.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67"}, - {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e"}, - {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829"}, - {file = "cryptography-41.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca"}, - {file = "cryptography-41.0.4-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d"}, - {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac"}, - {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9"}, - {file = "cryptography-41.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f"}, - {file = "cryptography-41.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91"}, - {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8"}, - {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6"}, - {file = "cryptography-41.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311"}, - {file = "cryptography-41.0.4.tar.gz", hash = "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, + {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, + {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, + {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, ] [package.dependencies] @@ -557,6 +734,36 @@ files = [ {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, ] +[[package]] +name = "distro" +version = "1.9.0" +description = "Distro - an OS platform information API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, + {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, +] + +[[package]] +name = "dnspython" +version = "2.4.2" +description = "DNS toolkit" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "dnspython-2.4.2-py3-none-any.whl", hash = "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8"}, + {file = "dnspython-2.4.2.tar.gz", hash = "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984"}, +] + +[package.extras] +dnssec = ["cryptography (>=2.6,<42.0)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=0.17.3)", "httpx (>=0.24.1)"] +doq = ["aioquic (>=0.9.20)"] +idna = ["idna (>=2.1,<4.0)"] +trio = ["trio (>=0.14,<0.23)"] +wmi = ["wmi (>=1.5.1,<2.0.0)"] + [[package]] name = "ecdsa" version = "0.18.0" @@ -575,6 +782,21 @@ six = ">=1.9.0" gmpy = ["gmpy"] gmpy2 = ["gmpy2"] +[[package]] +name = "email-validator" +version = "2.1.0.post1" +description = "A robust email address syntax and deliverability validation library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "email_validator-2.1.0.post1-py3-none-any.whl", hash = "sha256:c973053efbeddfef924dc0bd93f6e77a1ea7ee0fce935aea7103c7a3d6d2d637"}, + {file = "email_validator-2.1.0.post1.tar.gz", hash = "sha256:a4b0bd1cf55f073b924258d19321b1f3aa74b4b5a71a42c305575dba920e1a44"}, +] + +[package.dependencies] +dnspython = ">=2.0.0" +idna = ">=2.0.0" + [[package]] name = "embit" version = "0.7.0" @@ -608,13 +830,13 @@ tests = ["dj-database-url", "dj-email-url", "django-cache-url", "pytest"] [[package]] name = "exceptiongroup" -version = "1.1.3" +version = "1.2.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, - {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, ] [package.extras] @@ -622,39 +844,55 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.103.1" +version = "0.109.2" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "fastapi-0.103.1-py3-none-any.whl", hash = "sha256:5e5f17e826dbd9e9b5a5145976c5cd90bcaa61f2bf9a69aca423f2bcebe44d83"}, - {file = "fastapi-0.103.1.tar.gz", hash = "sha256:345844e6a82062f06a096684196aaf96c1198b25c06b72c1311b882aa2d8a35d"}, + {file = "fastapi-0.109.2-py3-none-any.whl", hash = "sha256:2c9bab24667293b501cad8dd388c05240c850b58ec5876ee3283c47d6e1e3a4d"}, + {file = "fastapi-0.109.2.tar.gz", hash = "sha256:f3817eac96fe4f65a2ebb4baa000f394e55f5fccdaf7f75250804bc58f354f73"}, ] [package.dependencies] -anyio = ">=3.7.1,<4.0.0" pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.27.0,<0.28.0" -typing-extensions = ">=4.5.0" +starlette = ">=0.36.3,<0.37.0" +typing-extensions = ">=4.8.0" [package.extras] -all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "fastapi-sso" +version = "0.9.1" +description = "FastAPI plugin to enable SSO to most common providers (such as Facebook login, Google login and login via Microsoft Office 365 Account)" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "fastapi_sso-0.9.1-py3-none-any.whl", hash = "sha256:94ad5a3e5710bef423c829224358740aa349d8934ce58cf943364d9bb18e6aeb"}, + {file = "fastapi_sso-0.9.1.tar.gz", hash = "sha256:0a9a3abdbb5ed20787ff47b749cd23e25a48e8a42230767a1f897f585223337f"}, +] + +[package.dependencies] +fastapi = ">=0.80" +httpx = ">=0.23.0" +oauthlib = ">=3.1.0" +pydantic = {version = ">=1.8.0", extras = ["email"]} [[package]] name = "filelock" -version = "3.12.4" +version = "3.13.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"}, - {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] -typing = ["typing-extensions (>=4.7.1)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] [[package]] name = "grpcio" @@ -783,13 +1021,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "identify" -version = "2.5.30" +version = "2.5.32" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.30-py2.py3-none-any.whl", hash = "sha256:afe67f26ae29bab007ec21b03d4114f41316ab9dd15aa8736a167481e108da54"}, - {file = "identify-2.5.30.tar.gz", hash = "sha256:f302a4256a15c849b91cfcdcec052a8ce914634b2f77ae87dad29cd749f2d88d"}, + {file = "identify-2.5.32-py2.py3-none-any.whl", hash = "sha256:0b7656ef6cba81664b783352c73f8c24b39cf82f926f78f4550eda928e5e0545"}, + {file = "identify-2.5.32.tar.gz", hash = "sha256:5d9979348ec1a21c768ae07e0a652924538e8bce67313a73cb0f681cf08ba407"}, ] [package.extras] @@ -797,24 +1035,24 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] name = "importlib-resources" -version = "6.1.0" +version = "6.1.1" description = "Read resources from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_resources-6.1.0-py3-none-any.whl", hash = "sha256:aa50258bbfa56d4e33fbd8aa3ef48ded10d1735f11532b8df95388cc6bdb7e83"}, - {file = "importlib_resources-6.1.0.tar.gz", hash = "sha256:9d48dcccc213325e810fd723e7fbb45ccb39f6cf5c31f00cf2b965f5f10f3cb9"}, + {file = "importlib_resources-6.1.1-py3-none-any.whl", hash = "sha256:e8bf90d8213b486f428c9c39714b920041cb02c184686a3dee24905aaa8105d6"}, + {file = "importlib_resources-6.1.1.tar.gz", hash = "sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a"}, ] [package.dependencies] @@ -835,15 +1073,26 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "itsdangerous" +version = "2.1.2" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.7" +files = [ + {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, + {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, +] + [[package]] name = "jinja2" -version = "3.0.1" +version = "3.1.3" description = "A very fast and expressive template engine." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, - {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, ] [package.dependencies] @@ -852,15 +1101,29 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "json5" +version = "0.9.17" +description = "A Python implementation of the JSON5 data format." +optional = false +python-versions = ">=3.8" +files = [ + {file = "json5-0.9.17-py2.py3-none-any.whl", hash = "sha256:f8ec1ecf985951d70f780f6f877c4baca6a47b6e61e02c4cd190138d10a7805a"}, + {file = "json5-0.9.17.tar.gz", hash = "sha256:717d99d657fa71b7094877b1d921b1cce40ab444389f6d770302563bb7dfd9ae"}, +] + +[package.extras] +dev = ["hypothesis"] + [[package]] name = "jsonschema" -version = "4.19.1" +version = "4.20.0" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.8" files = [ - {file = "jsonschema-4.19.1-py3-none-any.whl", hash = "sha256:cd5f1f9ed9444e554b38ba003af06c0a8c2868131e56bfbef0550fb450c0330e"}, - {file = "jsonschema-4.19.1.tar.gz", hash = "sha256:ec84cc37cfa703ef7cd4928db24f9cb31428a5d0fa77747b8b51a847458e0bbf"}, + {file = "jsonschema-4.20.0-py3-none-any.whl", hash = "sha256:ed6231f0429ecf966f5bc8dfef245998220549cbbcf140f913b7464c52c3b6b3"}, + {file = "jsonschema-4.20.0.tar.gz", hash = "sha256:4f614fd46d8d61258610998997743ec5492a648b33cf478c1ddc23ed4598a5fa"}, ] [package.dependencies] @@ -951,13 +1214,13 @@ files = [ [[package]] name = "limits" -version = "3.6.0" +version = "3.7.0" description = "Rate limiting utilities" optional = false python-versions = ">=3.7" files = [ - {file = "limits-3.6.0-py3-none-any.whl", hash = "sha256:32fe29a398352c71bc43d53773117d47e22c5ea4200aef28d3f5fdee10334cd7"}, - {file = "limits-3.6.0.tar.gz", hash = "sha256:57a9c69fd37ad1e4fa3886dff8d035227e1f6af87f47e9118627e72cf1ced3bf"}, + {file = "limits-3.7.0-py3-none-any.whl", hash = "sha256:c528817b7fc15f3e86ad091ba3e40231f6430a91b753db864767684cda8a7f2e"}, + {file = "limits-3.7.0.tar.gz", hash = "sha256:124c6a04d2f4b20990fb1de019eec9474d6c1346c70d8fd0561609b86998b64a"}, ] [package.dependencies] @@ -980,18 +1243,19 @@ rediscluster = ["redis (>=4.2.0,!=4.5.2,!=4.5.3)"] [[package]] name = "lnurl" -version = "0.3.6" +version = "0.4.2" description = "LNURL implementation for Python." optional = false -python-versions = ">=3.6" +python-versions = ">=3.9,<4.0" files = [ - {file = "lnurl-0.3.6-py3-none-any.whl", hash = "sha256:579982fd8c4d25bc84c61c74ec45cb7999fa1fa2426f5d5aeb0160ba333b9c92"}, - {file = "lnurl-0.3.6.tar.gz", hash = "sha256:8af07460115a48f3122a5a9c9a6062bee3897d5f6ab4c9a60f6561a83a8234f6"}, + {file = "lnurl-0.4.2-py3-none-any.whl", hash = "sha256:93f79ae7e0b0c66fed5b29ac1520e85e3e2c8648561a4b42974f0b7bffd34d84"}, + {file = "lnurl-0.4.2.tar.gz", hash = "sha256:c5e708b255d5333a0c08ceffe90ae4be6d2d09eb51dc8c35d19d8aa4cb21842a"}, ] [package.dependencies] -bech32 = "*" -pydantic = "*" +bech32 = ">=1.2.0,<2.0.0" +pydantic = ">=1,<2" +requests = ">=2.31.0,<3.0.0" [[package]] name = "loguru" @@ -1038,6 +1302,16 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -1092,38 +1366,38 @@ tests = ["pytest", "pytz", "simplejson"] [[package]] name = "mypy" -version = "1.5.1" +version = "1.7.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f33592ddf9655a4894aef22d134de7393e95fcbdc2d15c1ab65828eee5c66c70"}, - {file = "mypy-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:258b22210a4a258ccd077426c7a181d789d1121aca6db73a83f79372f5569ae0"}, - {file = "mypy-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9ec1f695f0c25986e6f7f8778e5ce61659063268836a38c951200c57479cc12"}, - {file = "mypy-1.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:abed92d9c8f08643c7d831300b739562b0a6c9fcb028d211134fc9ab20ccad5d"}, - {file = "mypy-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a156e6390944c265eb56afa67c74c0636f10283429171018446b732f1a05af25"}, - {file = "mypy-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ac9c21bfe7bc9f7f1b6fae441746e6a106e48fc9de530dea29e8cd37a2c0cc4"}, - {file = "mypy-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51cb1323064b1099e177098cb939eab2da42fea5d818d40113957ec954fc85f4"}, - {file = "mypy-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:596fae69f2bfcb7305808c75c00f81fe2829b6236eadda536f00610ac5ec2243"}, - {file = "mypy-1.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:32cb59609b0534f0bd67faebb6e022fe534bdb0e2ecab4290d683d248be1b275"}, - {file = "mypy-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:159aa9acb16086b79bbb0016145034a1a05360626046a929f84579ce1666b315"}, - {file = "mypy-1.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f6b0e77db9ff4fda74de7df13f30016a0a663928d669c9f2c057048ba44f09bb"}, - {file = "mypy-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26f71b535dfc158a71264e6dc805a9f8d2e60b67215ca0bfa26e2e1aa4d4d373"}, - {file = "mypy-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc3a600f749b1008cc75e02b6fb3d4db8dbcca2d733030fe7a3b3502902f161"}, - {file = "mypy-1.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:26fb32e4d4afa205b24bf645eddfbb36a1e17e995c5c99d6d00edb24b693406a"}, - {file = "mypy-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:82cb6193de9bbb3844bab4c7cf80e6227d5225cc7625b068a06d005d861ad5f1"}, - {file = "mypy-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4a465ea2ca12804d5b34bb056be3a29dc47aea5973b892d0417c6a10a40b2d65"}, - {file = "mypy-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9fece120dbb041771a63eb95e4896791386fe287fefb2837258925b8326d6160"}, - {file = "mypy-1.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d28ddc3e3dfeab553e743e532fb95b4e6afad51d4706dd22f28e1e5e664828d2"}, - {file = "mypy-1.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:57b10c56016adce71fba6bc6e9fd45d8083f74361f629390c556738565af8eeb"}, - {file = "mypy-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:ff0cedc84184115202475bbb46dd99f8dcb87fe24d5d0ddfc0fe6b8575c88d2f"}, - {file = "mypy-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8f772942d372c8cbac575be99f9cc9d9fb3bd95c8bc2de6c01411e2c84ebca8a"}, - {file = "mypy-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5d627124700b92b6bbaa99f27cbe615c8ea7b3402960f6372ea7d65faf376c14"}, - {file = "mypy-1.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:361da43c4f5a96173220eb53340ace68cda81845cd88218f8862dfb0adc8cddb"}, - {file = "mypy-1.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:330857f9507c24de5c5724235e66858f8364a0693894342485e543f5b07c8693"}, - {file = "mypy-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:c543214ffdd422623e9fedd0869166c2f16affe4ba37463975043ef7d2ea8770"}, - {file = "mypy-1.5.1-py3-none-any.whl", hash = "sha256:f757063a83970d67c444f6e01d9550a7402322af3557ce7630d3c957386fa8f5"}, - {file = "mypy-1.5.1.tar.gz", hash = "sha256:b031b9601f1060bf1281feab89697324726ba0c0bae9d7cd7ab4b690940f0b92"}, + {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, + {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, + {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, + {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, + {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, + {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, + {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, + {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, + {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, + {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, + {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, + {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, + {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, + {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, + {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, + {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, + {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, + {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, + {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, ] [package.dependencies] @@ -1134,6 +1408,7 @@ typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] +mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] @@ -1161,6 +1436,45 @@ files = [ [package.dependencies] setuptools = "*" +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.6" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + +[[package]] +name = "openai" +version = "1.12.0" +description = "The official Python library for the openai API" +optional = false +python-versions = ">=3.7.1" +files = [ + {file = "openai-1.12.0-py3-none-any.whl", hash = "sha256:a54002c814e05222e413664f651b5916714e4700d041d5cf5724d3ae1a3e3481"}, + {file = "openai-1.12.0.tar.gz", hash = "sha256:99c5d257d09ea6533d689d1cc77caa0ac679fa21efef8893d8b0832a86877f1b"}, +] + +[package.dependencies] +anyio = ">=3.5.0,<5" +distro = ">=1.7.0,<2" +httpx = ">=0.23.0,<1" +pydantic = ">=1.9.0,<3" +sniffio = "*" +tqdm = ">4" +typing-extensions = ">=4.7,<5" + +[package.extras] +datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] + [[package]] name = "openapi-schema-validator" version = "0.6.2" @@ -1196,13 +1510,13 @@ openapi-schema-validator = ">=0.6.0,<0.7.0" [[package]] name = "outcome" -version = "1.2.0" +version = "1.3.0.post0" description = "Capture the outcome of Python function calls." optional = false python-versions = ">=3.7" files = [ - {file = "outcome-1.2.0-py2.py3-none-any.whl", hash = "sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5"}, - {file = "outcome-1.2.0.tar.gz", hash = "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672"}, + {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, + {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, ] [package.dependencies] @@ -1219,6 +1533,23 @@ files = [ {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] +[[package]] +name = "passlib" +version = "1.7.4" +description = "comprehensive password hashing framework supporting over 30 schemes" +optional = false +python-versions = "*" +files = [ + {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, + {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, +] + +[package.extras] +argon2 = ["argon2-cffi (>=18.2.0)"] +bcrypt = ["bcrypt (>=3.1.0)"] +build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] +totp = ["cryptography"] + [[package]] name = "pathable" version = "0.4.3" @@ -1243,13 +1574,13 @@ files = [ [[package]] name = "platformdirs" -version = "3.11.0" +version = "4.1.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, - {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, + {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, + {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, ] [package.extras] @@ -1273,13 +1604,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "3.4.0" +version = "3.5.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.8" files = [ - {file = "pre_commit-3.4.0-py2.py3-none-any.whl", hash = "sha256:96d529a951f8b677f730a7212442027e8ba53f9b04d217c4c67dc56c393ad945"}, - {file = "pre_commit-3.4.0.tar.gz", hash = "sha256:6bbd5129a64cad4c0dfaeeb12cd8f7ea7e15b77028d985341478c8af3c759522"}, + {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, + {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, ] [package.dependencies] @@ -1393,6 +1724,17 @@ files = [ [package.dependencies] cryptography = ">=2.5" +[[package]] +name = "pyasn1" +version = "0.5.1" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "pyasn1-0.5.1-py2.py3-none-any.whl", hash = "sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58"}, + {file = "pyasn1-0.5.1.tar.gz", hash = "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c"}, +] + [[package]] name = "pycparser" version = "2.21" @@ -1406,43 +1748,43 @@ files = [ [[package]] name = "pycryptodomex" -version = "3.19.0" +version = "3.19.1" description = "Cryptographic library for Python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ - {file = "pycryptodomex-3.19.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff64fd720def623bf64d8776f8d0deada1cc1bf1ec3c1f9d6f5bb5bd098d034f"}, - {file = "pycryptodomex-3.19.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:61056a1fd3254f6f863de94c233b30dd33bc02f8c935b2000269705f1eeeffa4"}, - {file = "pycryptodomex-3.19.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:258c4233a3fe5a6341780306a36c6fb072ef38ce676a6d41eec3e591347919e8"}, - {file = "pycryptodomex-3.19.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e45bb4635b3c4e0a00ca9df75ef6295838c85c2ac44ad882410cb631ed1eeaa"}, - {file = "pycryptodomex-3.19.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:a12144d785518f6491ad334c75ccdc6ad52ea49230b4237f319dbb7cef26f464"}, - {file = "pycryptodomex-3.19.0-cp27-cp27m-win32.whl", hash = "sha256:1789d89f61f70a4cd5483d4dfa8df7032efab1118f8b9894faae03c967707865"}, - {file = "pycryptodomex-3.19.0-cp27-cp27m-win_amd64.whl", hash = "sha256:eb2fc0ec241bf5e5ef56c8fbec4a2634d631e4c4f616a59b567947a0f35ad83c"}, - {file = "pycryptodomex-3.19.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c9a68a2f7bd091ccea54ad3be3e9d65eded813e6d79fdf4cc3604e26cdd6384f"}, - {file = "pycryptodomex-3.19.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:8df69e41f7e7015a90b94d1096ec3d8e0182e73449487306709ec27379fff761"}, - {file = "pycryptodomex-3.19.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:917033016ecc23c8933205585a0ab73e20020fdf671b7cd1be788a5c4039840b"}, - {file = "pycryptodomex-3.19.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:e8e5ecbd4da4157889fce8ba49da74764dd86c891410bfd6b24969fa46edda51"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:a77b79852175064c822b047fee7cf5a1f434f06ad075cc9986aa1c19a0c53eb0"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:5b883e1439ab63af976656446fb4839d566bb096f15fc3c06b5a99cde4927188"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3866d68e2fc345162b1b9b83ef80686acfe5cec0d134337f3b03950a0a8bf56"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74eb1f73f788facece7979ce91594dc177e1a9b5d5e3e64697dd58299e5cb4d"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cb51096a6a8d400724104db8a7e4f2206041a1f23e58924aa3d8d96bcb48338"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a588a1cb7781da9d5e1c84affd98c32aff9c89771eac8eaa659d2760666f7139"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:d4dd3b381ff5a5907a3eb98f5f6d32c64d319a840278ceea1dcfcc65063856f3"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:263de9a96d2fcbc9f5bd3a279f14ea0d5f072adb68ebd324987576ec25da084d"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-win32.whl", hash = "sha256:67c8eb79ab33d0fbcb56842992298ddb56eb6505a72369c20f60bc1d2b6fb002"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-win_amd64.whl", hash = "sha256:09c9401dc06fb3d94cb1ec23b4ea067a25d1f4c6b7b118ff5631d0b5daaab3cc"}, - {file = "pycryptodomex-3.19.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:edbe083c299835de7e02c8aa0885cb904a75087d35e7bab75ebe5ed336e8c3e2"}, - {file = "pycryptodomex-3.19.0-pp27-pypy_73-win32.whl", hash = "sha256:136b284e9246b4ccf4f752d435c80f2c44fc2321c198505de1d43a95a3453b3c"}, - {file = "pycryptodomex-3.19.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5d73e9fa3fe830e7b6b42afc49d8329b07a049a47d12e0ef9225f2fd220f19b2"}, - {file = "pycryptodomex-3.19.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b2f1982c5bc311f0aab8c293524b861b485d76f7c9ab2c3ac9a25b6f7655975"}, - {file = "pycryptodomex-3.19.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb040b5dda1dff1e197d2ef71927bd6b8bfcb9793bc4dfe0bb6df1e691eaacb"}, - {file = "pycryptodomex-3.19.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:800a2b05cfb83654df80266692f7092eeefe2a314fa7901dcefab255934faeec"}, - {file = "pycryptodomex-3.19.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c01678aee8ac0c1a461cbc38ad496f953f9efcb1fa19f5637cbeba7544792a53"}, - {file = "pycryptodomex-3.19.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2126bc54beccbede6eade00e647106b4f4c21e5201d2b0a73e9e816a01c50905"}, - {file = "pycryptodomex-3.19.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b801216c48c0886742abf286a9a6b117e248ca144d8ceec1f931ce2dd0c9cb40"}, - {file = "pycryptodomex-3.19.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:50cb18d4dd87571006fd2447ccec85e6cec0136632a550aa29226ba075c80644"}, - {file = "pycryptodomex-3.19.0.tar.gz", hash = "sha256:af83a554b3f077564229865c45af0791be008ac6469ef0098152139e6bd4b5b6"}, + {file = "pycryptodomex-3.19.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b5c336dc698650283ad06f8c0237a984087d0af9f403ff21d633507335628156"}, + {file = "pycryptodomex-3.19.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c9cb88ed323be1aa642b3c17cd5caa1a03c3a8fbad092d48ecefe88e328ffae3"}, + {file = "pycryptodomex-3.19.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:0b42e2743893f386dfb58fe24a4c8be5305c3d1c825d5f23d9e63fd0700d1110"}, + {file = "pycryptodomex-3.19.1-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10c2eed4efdfa084b602ab922e699a0a2ba82053baebfc8afcaf27489def7955"}, + {file = "pycryptodomex-3.19.1-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:e94a7e986b117b72e9472f8eafdd81748dafff30815401f9760f759f1debe9ef"}, + {file = "pycryptodomex-3.19.1-cp27-cp27m-win32.whl", hash = "sha256:23707238b024b36c35dd3428f5af6c1f0c5ef54c21e387a2063633717699b8b2"}, + {file = "pycryptodomex-3.19.1-cp27-cp27m-win_amd64.whl", hash = "sha256:c1ae2fb8d5d6771670436dcc889b293e363c97647a6d31c21eebc12b7b760010"}, + {file = "pycryptodomex-3.19.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:d7a77391fd351ff1bdf8475558ddc6e92950218cb905419ee14aa02f370f1054"}, + {file = "pycryptodomex-3.19.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:c9332b04bf3f838327087b028f690f4ddb9341eb014a0221e79b9c19a77f7555"}, + {file = "pycryptodomex-3.19.1-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:beb5f0664f49b6093da179ee8e27c1d670779f50b9ece0886ce491bb8bd63728"}, + {file = "pycryptodomex-3.19.1-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:d45d0d35a238d838b872598fa865bbfb31aaef9aeeda77c68b04ef79f9a469dc"}, + {file = "pycryptodomex-3.19.1-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:ed3bdda44cc05dd13eee697ab9bea6928531bb7b218e68e66d0d3eb2ebab043e"}, + {file = "pycryptodomex-3.19.1-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ae75eea2e908383fd4c659fdcfe9621a72869e3e3ee73904227e93b7f7b80b54"}, + {file = "pycryptodomex-3.19.1-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:371bbe0be17b4dd8cc0c2f378d75ea33f00d5a39884c09a672016ac40145a5fa"}, + {file = "pycryptodomex-3.19.1-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96000b837bcd8e3bf86b419924a056c978e45027281e4318650c81c25a3ef6cc"}, + {file = "pycryptodomex-3.19.1-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:011e859026ecbd15b8e720e8992361186e582cf726c50bde6ff8c0c05e820ddf"}, + {file = "pycryptodomex-3.19.1-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:76414d39df6b45bcc4f38cf1ba2031e0f4b8e99d1ba3c2eee31ffe1b9f039733"}, + {file = "pycryptodomex-3.19.1-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:1c04cfff163c05d033bf28e3c4429d8222796738c7b6c1638b9d7090b904611e"}, + {file = "pycryptodomex-3.19.1-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:de5a43901e47e7a6938490fc5de3074f6e35c8b481a75b227c0d24d6099bd41d"}, + {file = "pycryptodomex-3.19.1-cp35-abi3-win32.whl", hash = "sha256:f24f49fc6bd706d87048654d6be6c7c967d6836d4879e3a7c439275fab9948ad"}, + {file = "pycryptodomex-3.19.1-cp35-abi3-win_amd64.whl", hash = "sha256:f8b3d9e7c17c1ffc1fa5b11c0bbab8a5df3de8596bb32ad30281b21e5ede4bf5"}, + {file = "pycryptodomex-3.19.1-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:ac562e239d98cfef763866c0aee4586affb0d58c592202f06c87241af99db241"}, + {file = "pycryptodomex-3.19.1-pp27-pypy_73-win32.whl", hash = "sha256:39eb1f82ac3ba3e39d866f38e480e8fa53fcdd22260340f05f54a8188d47d510"}, + {file = "pycryptodomex-3.19.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bc4b7bfaac56e6dfd62044847443a3d110c7abea7fcb0d68c1aea64ed3a6697"}, + {file = "pycryptodomex-3.19.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dffe067d5fff14dba4d18ff7d459cc2a47576d82dafbff13a8f1199c3353e41"}, + {file = "pycryptodomex-3.19.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aab7941c2ff53eb63cb26252770e4f14386d79ce07baeffbf98a1323c1646545"}, + {file = "pycryptodomex-3.19.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3f3c58971784fba0e014bc3f8aed1197b86719631e1b597d36d7354be5598312"}, + {file = "pycryptodomex-3.19.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5ca98de2e5ac100e57a7116309723360e8f799f722509e376dc396cdf65eec9c"}, + {file = "pycryptodomex-3.19.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8a97b1acd36e9ce9d4067d94a8be99c458f0eb8070828639302a95cfcf0770b"}, + {file = "pycryptodomex-3.19.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62f51a63d73153482729904381dd2de86800b0733a8814ee8f072fa73e5c92fb"}, + {file = "pycryptodomex-3.19.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9919a1edd2a83c4dfb69f1d8a4c0c5efde7147ef15b07775633372b80c90b5d8"}, + {file = "pycryptodomex-3.19.1.tar.gz", hash = "sha256:0b7154aff2272962355f8941fd514104a88cb29db2d8f43a29af900d6398eb1c"}, ] [[package]] @@ -1491,6 +1833,7 @@ files = [ ] [package.dependencies] +email-validator = {version = ">=1.0.3", optional = true, markers = "extra == \"email\""} typing-extensions = ">=4.2.0" [package.extras] @@ -1525,18 +1868,18 @@ pyln-proto = ">=23" [[package]] name = "pyln-proto" -version = "23.5.2" +version = "23.11" description = "This package implements some of the Lightning Network protocol in pure python. It is intended for protocol testing and some minor tooling only. It is not deemed secure enough to handle any amount of real funds (you have been warned!)." optional = false -python-versions = ">=3.7,<4.0" +python-versions = ">=3.8,<4.0" files = [ - {file = "pyln_proto-23.5.2-py3-none-any.whl", hash = "sha256:db0f137397bbb02e3142db227596025ea69ffbc1b1ba18d87f7645873e7e8d8a"}, - {file = "pyln_proto-23.5.2.tar.gz", hash = "sha256:3a04e2707e1bb2d78a74089748b202aca891211af647f305f8fbbe0ae25a6bc6"}, + {file = "pyln_proto-23.11-py3-none-any.whl", hash = "sha256:7f3e9df5fac242db4e27b5879e1df2bd318ec8f7b125132ea7a60ce17340bded"}, + {file = "pyln_proto-23.11.tar.gz", hash = "sha256:e7056386be1527fd2c49a3db9228a9f5fd44cd5cdc9b1d431b21112137dd5957"}, ] [package.dependencies] base58 = ">=2.1.1,<3.0.0" -bitstring = ">=3,<4" +bitstring = ">=4.1.0,<5.0.0" coincurve = ">=18,<19" cryptography = ">=41,<42" PySocks = ">=1,<2" @@ -1569,13 +1912,13 @@ files = [ [[package]] name = "pytest" -version = "7.4.2" +version = "7.4.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, - {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, ] [package.dependencies] @@ -1625,6 +1968,34 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "pytest-httpserver" +version = "1.0.10" +description = "pytest-httpserver is a httpserver for pytest" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_httpserver-1.0.10-py3-none-any.whl", hash = "sha256:d40e0cc3d61ed6e4d80f52a796926d557a7db62b17e43b3e258a78a3c34becb9"}, + {file = "pytest_httpserver-1.0.10.tar.gz", hash = "sha256:77b9fbc2eb0a129cfbbacc8fe57e8cafe071d506489f31fe31e62f1b332d9905"}, +] + +[package.dependencies] +Werkzeug = ">=2.0.0" + +[[package]] +name = "pytest-md" +version = "0.2.0" +description = "Plugin for generating Markdown reports for pytest results" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pytest-md-0.2.0.tar.gz", hash = "sha256:3b248d5b360ea5198e05b4f49c7442234812809a63137ec6cdd3643a40cf0112"}, + {file = "pytest_md-0.2.0-py3-none-any.whl", hash = "sha256:4c4cd16fea6d1485e87ee254558712c804a96d2aa9674b780e7eb8fb6526e1d1"}, +] + +[package.dependencies] +pytest = ">=4.2.1" + [[package]] name = "python-crontab" version = "3.0.0" @@ -1671,6 +2042,27 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "python-jose" +version = "3.3.0" +description = "JOSE implementation in Python" +optional = false +python-versions = "*" +files = [ + {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, + {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, +] + +[package.dependencies] +ecdsa = "!=0.15" +pyasn1 = "*" +rsa = "*" + +[package.extras] +cryptography = ["cryptography (>=3.4.0)"] +pycrypto = ["pyasn1", "pycrypto (>=2.6.0,<2.7.0)"] +pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"] + [[package]] name = "pywebpush" version = "1.14.0" @@ -1700,6 +2092,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1707,8 +2100,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1725,6 +2125,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1732,6 +2133,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1910,30 +2312,44 @@ files = [ {file = "rpds_py-0.10.3.tar.gz", hash = "sha256:fcc1ebb7561a3e24a6588f7c6ded15d80aec22c66a070c757559b57b17ffd1cb"}, ] +[[package]] +name = "rsa" +version = "4.9" +description = "Pure-Python RSA implementation" +optional = false +python-versions = ">=3.6,<4" +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + [[package]] name = "ruff" -version = "0.0.291" -description = "An extremely fast Python linter, written in Rust." +version = "0.3.3" +description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.0.291-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:b97d0d7c136a85badbc7fd8397fdbb336e9409b01c07027622f28dcd7db366f2"}, - {file = "ruff-0.0.291-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:6ab44ea607967171e18aa5c80335237be12f3a1523375fa0cede83c5cf77feb4"}, - {file = "ruff-0.0.291-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04b384f2d36f00d5fb55313d52a7d66236531195ef08157a09c4728090f2ef0"}, - {file = "ruff-0.0.291-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b727c219b43f903875b7503a76c86237a00d1a39579bb3e21ce027eec9534051"}, - {file = "ruff-0.0.291-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87671e33175ae949702774071b35ed4937da06f11851af75cd087e1b5a488ac4"}, - {file = "ruff-0.0.291-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b75f5801547f79b7541d72a211949754c21dc0705c70eddf7f21c88a64de8b97"}, - {file = "ruff-0.0.291-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b09b94efdcd162fe32b472b2dd5bf1c969fcc15b8ff52f478b048f41d4590e09"}, - {file = "ruff-0.0.291-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d5b56bc3a2f83a7a1d7f4447c54d8d3db52021f726fdd55d549ca87bca5d747"}, - {file = "ruff-0.0.291-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13f0d88e5f367b2dc8c7d90a8afdcfff9dd7d174e324fd3ed8e0b5cb5dc9b7f6"}, - {file = "ruff-0.0.291-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b3eeee1b1a45a247758ecdc3ab26c307336d157aafc61edb98b825cadb153df3"}, - {file = "ruff-0.0.291-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6c06006350c3bb689765d71f810128c9cdf4a1121fd01afc655c87bab4fb4f83"}, - {file = "ruff-0.0.291-py3-none-musllinux_1_2_i686.whl", hash = "sha256:fd17220611047de247b635596e3174f3d7f2becf63bd56301fc758778df9b629"}, - {file = "ruff-0.0.291-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5383ba67ad360caf6060d09012f1fb2ab8bd605ab766d10ca4427a28ab106e0b"}, - {file = "ruff-0.0.291-py3-none-win32.whl", hash = "sha256:1d5f0616ae4cdc7a938b493b6a1a71c8a47d0300c0d65f6e41c281c2f7490ad3"}, - {file = "ruff-0.0.291-py3-none-win_amd64.whl", hash = "sha256:8a69bfbde72db8ca1c43ee3570f59daad155196c3fbe357047cd9b77de65f15b"}, - {file = "ruff-0.0.291-py3-none-win_arm64.whl", hash = "sha256:d867384a4615b7f30b223a849b52104214442b5ba79b473d7edd18da3cde22d6"}, - {file = "ruff-0.0.291.tar.gz", hash = "sha256:c61109661dde9db73469d14a82b42a88c7164f731e6a3b0042e71394c1c7ceed"}, + {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:973a0e388b7bc2e9148c7f9be8b8c6ae7471b9be37e1cc732f8f44a6f6d7720d"}, + {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8"}, + {file = "ruff-0.3.3-py3-none-win32.whl", hash = "sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0"}, + {file = "ruff-0.3.3-py3-none-win_amd64.whl", hash = "sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc"}, + {file = "ruff-0.3.3-py3-none-win_arm64.whl", hash = "sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61"}, + {file = "ruff-0.3.3.tar.gz", hash = "sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d"}, ] [[package]] @@ -1973,17 +2389,17 @@ cffi = ">=1.3.0" [[package]] name = "setuptools" -version = "68.2.2" +version = "69.0.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, - {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, + {file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"}, + {file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] @@ -2011,13 +2427,13 @@ files = [ [[package]] name = "slowapi" -version = "0.1.8" +version = "0.1.9" description = "A rate limiting extension for Starlette and Fastapi" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "slowapi-0.1.8-py3-none-any.whl", hash = "sha256:629fc415575bbffcd9d8621cc3ce326a78402c5f9b7b50b127979118d485c72e"}, - {file = "slowapi-0.1.8.tar.gz", hash = "sha256:8cc268f5a7e3624efa3f7bd2859b895f9f2376c4ed4e0378dd2f7f3343ca608e"}, + {file = "slowapi-0.1.9-py3-none-any.whl", hash = "sha256:cfad116cfb84ad9d763ee155c1e5c5cbf00b0d47399a769b227865f5df576e36"}, + {file = "slowapi-0.1.9.tar.gz", hash = "sha256:639192d0f1ca01b1c6d95bf6c71d794c3a9ee189855337b4821f7f457dddad77"}, ] [package.dependencies] @@ -2129,13 +2545,13 @@ starlette = "*" [[package]] name = "starlette" -version = "0.27.0" +version = "0.36.3" description = "The little ASGI library that shines." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, - {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, + {file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"}, + {file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"}, ] [package.dependencies] @@ -2143,7 +2559,7 @@ anyio = ">=3.4.0,<5" typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] [[package]] name = "tomli" @@ -2156,17 +2572,73 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "tqdm" +version = "4.66.2" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"}, + {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "types-passlib" +version = "1.7.7.13" +description = "Typing stubs for passlib" +optional = false +python-versions = "*" +files = [ + {file = "types-passlib-1.7.7.13.tar.gz", hash = "sha256:f152639f1f2103d7f59a56e2aec5f9398a75a80830991d0d68aac5c2b9c32a77"}, + {file = "types_passlib-1.7.7.13-py3-none-any.whl", hash = "sha256:414b5ee9c88313357c9261cfcf816509b1e8e4673f0796bd61e9ef249f6fe076"}, +] + [[package]] name = "types-protobuf" -version = "4.24.0.2" +version = "4.24.0.4" description = "Typing stubs for protobuf" optional = false +python-versions = ">=3.7" +files = [ + {file = "types-protobuf-4.24.0.4.tar.gz", hash = "sha256:57ab42cb171dfdba2c74bb5b50c250478538cc3c5ed95b8b368929ad0c9f90a5"}, + {file = "types_protobuf-4.24.0.4-py3-none-any.whl", hash = "sha256:131ab7d0cbc9e444bc89c994141327dcce7bcaeded72b1acb72a94827eb9c7af"}, +] + +[[package]] +name = "types-pyasn1" +version = "0.5.0.1" +description = "Typing stubs for pyasn1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "types-pyasn1-0.5.0.1.tar.gz", hash = "sha256:023e903f5920ec9585555235f95bb2d2756b7b58023d3f94890ee8d1d4d9d1ff"}, + {file = "types_pyasn1-0.5.0.1-py3-none-any.whl", hash = "sha256:1bbbe3fcf16a65064e4a5bd7f1be43c375ba241054f8f361b5e6c61c8deb3935"}, +] + +[[package]] +name = "types-python-jose" +version = "3.3.4.8" +description = "Typing stubs for python-jose" +optional = false python-versions = "*" files = [ - {file = "types-protobuf-4.24.0.2.tar.gz", hash = "sha256:598bb2290b9b0ea65f4f63569a09deaa4475edd7bb0d8589057a38deb0b19f4f"}, - {file = "types_protobuf-4.24.0.2-py3-none-any.whl", hash = "sha256:b86b0deefd1cb1582d355be4fd7a2a807cf49c993d9744d3c9fbe1cbf1e6b044"}, + {file = "types-python-jose-3.3.4.8.tar.gz", hash = "sha256:3c316675c3cee059ccb9aff87358254344915239fa7f19cee2787155a7db14ac"}, + {file = "types_python_jose-3.3.4.8-py3-none-any.whl", hash = "sha256:95592273443b45dc5cc88f7c56aa5a97725428753fb738b794e63ccb4904954e"}, ] +[package.dependencies] +types-pyasn1 = "*" + [[package]] name = "typing-extensions" version = "4.8.0" @@ -2180,18 +2652,17 @@ files = [ [[package]] name = "urllib3" -version = "2.0.6" +version = "2.1.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "urllib3-2.0.6-py3-none-any.whl", hash = "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2"}, - {file = "urllib3-2.0.6.tar.gz", hash = "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564"}, + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -2260,24 +2731,62 @@ test = ["Cython (>=0.29.32,<0.30.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "my [[package]] name = "virtualenv" -version = "20.24.5" +version = "20.25.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"}, - {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"}, + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<4" +platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +[[package]] +name = "wallycore" +version = "1.0.0" +description = "libwally Bitcoin library" +optional = true +python-versions = "*" +files = [ + {file = "wallycore-1.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce04189be2f971302aca571ae6d128a0786151fa1bf256a15959701e7ba2d2e2"}, + {file = "wallycore-1.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c680ddcb4cce2a3cf81afcec3fc4c7bc55ba8268f66903ebf97c70cafb413739"}, + {file = "wallycore-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37669b829ea7004f0ed87207ffdc2f041257eea70cda924349dfe0c7ffc7f6c0"}, + {file = "wallycore-1.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a77628d395551fd020cca7016d2e642fac2b3b3eedf80693bdd741eccf05618"}, + {file = "wallycore-1.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b632ed51f1a43cf6fe814dae33ffabfdfb72b55f2cf8b130607a6fa542310be5"}, + {file = "wallycore-1.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7728f6e15d02b25734ba18fea582db4e2421d2d5837338fa9d849eda75c9dcb8"}, + {file = "wallycore-1.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:ff5cf4445761be0a35bd9570bd91cf25c6f9abd591f228530de2ec19dc6afdc9"}, + {file = "wallycore-1.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6d1281e395b3857f7c974d923947bbe20afd913b247b9765fc62331dcbe6c59d"}, + {file = "wallycore-1.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:05225c7a84cc50abeabde50949dff07dbbbbb3d3b8fe11acc71a30660833eac3"}, + {file = "wallycore-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:587db2ea63f980588597a75e655f6dafa3063ed11b423327aafa33767f6b35c4"}, + {file = "wallycore-1.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22b7793f42a1001f117843cd313d3e65cb8d3fcda7f32e931dd5b145b1b8252"}, + {file = "wallycore-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c3bc30982b0baa5df4d8f24fb9f7db3d340c6c86cb26b4a7e84ffddbb40c5d"}, + {file = "wallycore-1.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d44c542efc5e892aeb5dad313281a979d8250d266ce91409fb2e5d824ea035a2"}, + {file = "wallycore-1.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:e09e8d3a46b7b740a90ba6220c2a619ecc30830ad084c213d0705bccac45829a"}, + {file = "wallycore-1.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:726eb298178c8194c4efe71252e845c82f8d79036d1cf7ce7e87af5335a4affd"}, + {file = "wallycore-1.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d84c2c327fb7459bca63758abf9e1a9f654981fe3a972222c92ce8f4872b3d7a"}, + {file = "wallycore-1.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0ed6852115b638a954e252e70e0dacdd4466d01e984a2ef88445124bc17dbc05"}, + {file = "wallycore-1.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d1e22c7294544a8f725e962631428c486a13cdff3a4e7f8738e9bb9077a692b"}, + {file = "wallycore-1.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76852c92dfe32378923f1032ab9f62552ba663578efd86660a4d14606fddc4ab"}, + {file = "wallycore-1.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ccc0d59600cccab4caec609d0228c6f422015daa949d97621cc78f45f3d6c1d7"}, + {file = "wallycore-1.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:5358913e7d93cce8b0b03d55c9c93c5c7237014346400ee3c7bc48ea049e7f51"}, + {file = "wallycore-1.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65536ba06c5ea85668f93ca809765de8d5444ea92d92bab5975606d96ebf59a5"}, + {file = "wallycore-1.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6bd3df53ddd9c7194a3c29a40ccaa4634148f11c9bbfec3bb327eeca54a2e15e"}, + {file = "wallycore-1.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f2bccdeef22d5352f30d8f4b0c57e1a29962aba12122d50aa0ea1354e78f3a45"}, + {file = "wallycore-1.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78473a1ae340e4ca8e7692ac9c7e9e887aff82a420d7a1ac17de05afe18d71c7"}, + {file = "wallycore-1.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:541676fea2e15415004c26eee207662843daa28d7bef1e42836ddd180aa8e23b"}, + {file = "wallycore-1.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:293881ad25763f514fc5106dd88a7eaee38f7b46aa1a5d4e5076b18c77c0b87b"}, + {file = "wallycore-1.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:74e40f229a08ae8d83cefce7249d819a373584d83bc5e4a91cb9f315c5d8e9fa"}, + {file = "wallycore-1.0.0.tar.gz", hash = "sha256:6ba37e579324ab4662aa68a5183a264cee0efd4e831aaef5787d22247bdc199d"}, +] + [[package]] name = "websocket-client" version = "1.6.3" @@ -2373,6 +2882,23 @@ files = [ {file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"}, ] +[[package]] +name = "werkzeug" +version = "3.0.2" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "werkzeug-3.0.2-py3-none-any.whl", hash = "sha256:3aac3f5da756f93030740bc235d3e09449efcf65f2f55e3602e1d851b8f48795"}, + {file = "werkzeug-3.0.2.tar.gz", hash = "sha256:e39b645a6ac92822588e7b39a692e7828724ceae0b0d702ef96701f90e70128d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + [[package]] name = "win32-setctime" version = "1.1.0" @@ -2389,86 +2915,81 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] [[package]] name = "wrapt" -version = "1.15.0" +version = "1.16.0" description = "Module for decorators, wrappers and monkey patching." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -files = [ - {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, - {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, - {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, - {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, - {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, - {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, - {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, - {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, - {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, - {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, - {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, - {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, - {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, - {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, - {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, - {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, - {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, - {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, - {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] [[package]] @@ -2486,7 +3007,10 @@ files = [ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +[extras] +liquid = ["wallycore"] + [metadata] lock-version = "2.0" python-versions = "^3.10 | ^3.9" -content-hash = "8fa58347ff00053e7e81feb763555ea6e892d23a6d953a592dea3fe4ba424b79" +content-hash = "4c11cc117beb703ebece5fac43adbabae76804f084c39ef90a67edcfb56795d7" diff --git a/pyproject.toml b/pyproject.toml index f423ceb9d8..12ddefef24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "lnbits" -version = "0.11.1" +version = "0.12.5" description = "LNbits, free and open-source Lightning wallet and accounts system." authors = ["Alan Bits "] @@ -9,10 +9,10 @@ python = "^3.10 | ^3.9" bech32 = "1.2.0" click = "8.1.7" ecdsa = "0.18.0" -fastapi = "0.103.1" +fastapi = "0.109.2" httpx = "0.25.0" -jinja2 = "3.0.1" -lnurl = "0.3.6" +jinja2 = "3.1.3" +lnurl = "0.4.2" psycopg2-binary = "2.9.7" pydantic = "1.10.9" pyqrcode = "1.2.1" @@ -29,12 +29,17 @@ grpcio = "1.58.0" protobuf = "4.24.3" pyln-client = "23.8" pywebpush = "1.14.0" -slowapi = "0.1.8" -python-dotenv = "1.0.0" +slowapi = "0.1.9" websocket-client = "1.6.3" -pycryptodomex = "3.19.0" +pycryptodomex = "3.19.1" packaging = "23.1" bolt11 = "2.0.5" +# needed for new login methods: username-password, google-auth, github-auth +bcrypt = "^4.1.1" +python-jose = "^3.3.0" +passlib = "^1.7.4" +itsdangerous = "^2.1.2" +fastapi-sso = "^0.9.1" # needed for boltz, lnurldevice, watchonly extensions embit = "0.7.0" # needed for cashu, lnurlp, nostrclient, nostrmarket, nostrrelay extensions @@ -43,9 +48,14 @@ secp256k1 = "0.14.0" environs = "9.5.0" # needed for scheduler extension python-crontab = "3.0.0" +# needed for liquid support boltz +wallycore = {version = "^1.0.0", optional = true} + +[tool.poetry.extras] +liquid = ["wallycore"] [tool.poetry.group.dev.dependencies] -black = "^23.7.0" +black = "^24.3.0" pytest-asyncio = "^0.21.0" pytest = "^7.3.2" pytest-cov = "^4.1.0" @@ -53,10 +63,17 @@ mypy = "^1.5.1" types-protobuf = "^4.24.0.2" pre-commit = "^3.2.2" openapi-spec-validator = "^0.6.0" -ruff = "^0.0.291" +ruff = "^0.3.2" # not our dependency but needed indirectly by openapi-spec-validator # we want to use 0.10.3 because newer versions are broken on nix rpds-py = "0.10.3" +types-passlib = "^1.7.7.13" +types-python-jose = "^3.3.4.8" +openai = "^1.12.0" +json5 = "^0.9.17" +asgi-lifespan = "^2.1.0" +pytest-md = "^0.2.0" +pytest-httpserver = "^1.0.10" [build-system] requires = ["poetry-core>=1.0.0"] @@ -68,7 +85,9 @@ lnbits-cli = "lnbits.commands:main" [tool.pyright] include = [ - "lnbits" + "lnbits", + "tests", + "tools", ] exclude = [ "lnbits/wallets/lnd_grpc_files", @@ -77,7 +96,11 @@ exclude = [ ] [tool.mypy] -files = "lnbits" +files = [ + "lnbits", + "tests", + "tools", +] exclude = [ "^lnbits/wallets/lnd_grpc_files", "^lnbits/extensions", @@ -105,6 +128,8 @@ module = [ "pyln.client.*", "py_vapid.*", "pywebpush.*", + "fastapi_sso.sso.*", + "json5.*", ] ignore_missing_imports = "True" @@ -130,53 +155,33 @@ extend-exclude = """( # Same as Black. + 10% rule of black line-length = 88 -# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. -# (`I`) is for `isort`. -select = ["E", "F", "I"] +# Exclude generated files. +extend-exclude = [ + "lnbits/wallets/lnd_grpc_files" +] + +[tool.ruff.lint] +# Enable: +# F - pyflakes +# E - pycodestyle errors +# W - pycodestyle warnings +# I - isort +# A - flake8-builtins +# C - mccabe +select = ["F", "E", "W", "I", "A", "C"] ignore = [] # Allow autofix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] unfixable = [] -# Exclude a variety of commonly ignored directories. -exclude = [ - "lnbits/static", - "lnbits/extensions", - "lnbits/wallets/lnd_grpc_files", - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".mypy_cache", - ".nox", - ".pants.d", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - "__pypackages__", - "_build", - "buck-out", - "build", - "dist", - "node_modules", - "venv", -] - # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" -# Assume Python -# target-version = "py39" - # Ignore unused imports in __init__.py files. -[tool.ruff.extend-per-file-ignores] +[tool.ruff.lint.extend-per-file-ignores] "__init__.py" = ["F401", "F403"] -[tool.ruff.mccabe] -# Unlike Flake8, default to a complexity level of 10. -max-complexity = 10 +[tool.ruff.lint.mccabe] +# TODO: Decrease this to 10. +max-complexity = 16 diff --git a/tests/conftest.py b/tests/conftest.py index f370b1bd4e..2d210f2f35 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ from time import time import uvloop +from asgi_lifespan import LifespanManager uvloop.install() @@ -20,7 +21,7 @@ ) from lnbits.core.models import CreateInvoice from lnbits.core.services import update_wallet_balance -from lnbits.core.views.api import api_payments_create_invoice +from lnbits.core.views.payment_api import api_payments_create_invoice from lnbits.db import DB_TYPE, SQLITE, Database from lnbits.settings import settings from tests.helpers import ( @@ -35,6 +36,7 @@ settings.lnbits_data_folder = "./tests/data" settings.lnbits_admin_ui = True settings.lnbits_extensions_default_install = [] +settings.lnbits_extensions_deactivate_all = True @pytest_asyncio.fixture(scope="session") @@ -49,16 +51,16 @@ def event_loop(): async def app(): clean_database(settings) app = create_app() - await app.router.startup() - yield app - await app.router.shutdown() + async with LifespanManager(app) as manager: + settings.first_install = False + yield manager.app @pytest_asyncio.fixture(scope="session") async def client(app): - client = AsyncClient(app=app, base_url=f"http://{settings.host}:{settings.port}") - yield client - await client.aclose() + url = f"http://{settings.host}:{settings.port}" + async with AsyncClient(app=app, base_url=url) as client: + yield client @pytest.fixture(scope="session") diff --git a/tests/core/test_db.py b/tests/core/test_db.py index b10a20638e..97cebb22e9 100644 --- a/tests/core/test_db.py +++ b/tests/core/test_db.py @@ -34,5 +34,4 @@ async def test_create_wallet_and_delete_wallet(app, to_user): assert del_wallet.deleted is True del_wallet = await get_wallet_for_key(wallet.inkey) - assert del_wallet is not None - assert del_wallet.deleted is True + assert del_wallet is None diff --git a/tests/core/test_db_fetch_page.py b/tests/core/test_db_fetch_page.py new file mode 100644 index 0000000000..ba0c8409d6 --- /dev/null +++ b/tests/core/test_db_fetch_page.py @@ -0,0 +1,74 @@ +import pytest +import pytest_asyncio + +from tests.helpers import DbTestModel + + +@pytest_asyncio.fixture(scope="session") +async def fetch_page(db): + await db.execute("DROP TABLE IF EXISTS test_db_fetch_page") + await db.execute( + """ + CREATE TABLE test_db_fetch_page ( + id TEXT PRIMARY KEY, + value TEXT NOT NULL, + name TEXT NOT NULL + ) + """ + ) + await db.execute( + """ + INSERT INTO test_db_fetch_page (id, name, value) VALUES + ('1', 'Alice', 'foo'), + ('2', 'Bob', 'bar'), + ('3', 'Carol', 'bar'), + ('4', 'Dave', 'bar'), + ('5', 'Dave', 'foo') + """ + ) + yield + await db.execute("DROP TABLE test_db_fetch_page") + + +@pytest.mark.asyncio +async def test_db_fetch_page_simple(fetch_page, db): + row = await db.fetch_page( + query="select * from test_db_fetch_page", + model=DbTestModel, + ) + + assert row + assert row.total == 5 + assert len(row.data) == 5 + + +@pytest.mark.asyncio +async def test_db_fetch_page_group_by(fetch_page, db): + row = await db.fetch_page( + query="select max(id) as id, name from test_db_fetch_page", + model=DbTestModel, + group_by=["name"], + ) + assert row + assert row.total == 4 + + +@pytest.mark.asyncio +async def test_db_fetch_page_group_by_multiple(fetch_page, db): + row = await db.fetch_page( + query="select max(id) as id, name, value from test_db_fetch_page", + model=DbTestModel, + group_by=["value", "name"], + ) + assert row + assert row.total == 5 + + +@pytest.mark.asyncio +async def test_db_fetch_page_group_by_evil(fetch_page, db): + with pytest.raises(ValueError, match="Value for GROUP BY is invalid"): + await db.fetch_page( + query="select * from test_db_fetch_page", + model=DbTestModel, + group_by=["name;"], + ) diff --git a/tests/core/test_helpers_query.py b/tests/core/test_helpers_query.py index 4597366553..3988eacf04 100644 --- a/tests/core/test_helpers_query.py +++ b/tests/core/test_helpers_query.py @@ -1,27 +1,21 @@ import pytest -from pydantic import BaseModel from lnbits.helpers import ( insert_query, update_query, ) +from tests.helpers import DbTestModel - -class TestModel(BaseModel): - id: int - name: str - - -test = TestModel(id=1, name="test") +test = DbTestModel(id=1, name="test", value="yes") @pytest.mark.asyncio async def test_helpers_insert_query(): q = insert_query("test_helpers_query", test) - assert q == "INSERT INTO test_helpers_query (id, name) VALUES (?, ?)" + assert q == "INSERT INTO test_helpers_query (id, name, value) VALUES (?, ?, ?)" @pytest.mark.asyncio async def test_helpers_update_query(): q = update_query("test_helpers_query", test) - assert q == "UPDATE test_helpers_query SET id = ?, name = ? WHERE id = ?" + assert q == "UPDATE test_helpers_query SET id = ?, name = ?, value = ? WHERE id = ?" diff --git a/tests/core/views/test_admin_api.py b/tests/core/views/test_admin_api.py index 62a3743a56..3220bf8f81 100644 --- a/tests/core/views/test_admin_api.py +++ b/tests/core/views/test_admin_api.py @@ -5,13 +5,13 @@ @pytest.mark.asyncio async def test_admin_get_settings_permission_denied(client, from_user): - response = await client.get(f"/admin/api/v1/settings/?usr={from_user.id}") + response = await client.get(f"/admin/api/v1/settings?usr={from_user.id}") assert response.status_code == 401 @pytest.mark.asyncio async def test_admin_get_settings(client, superuser): - response = await client.get(f"/admin/api/v1/settings/?usr={superuser.id}") + response = await client.get(f"/admin/api/v1/settings?usr={superuser.id}") assert response.status_code == 200 result = response.json() assert "super_user" not in result @@ -21,7 +21,7 @@ async def test_admin_get_settings(client, superuser): async def test_admin_update_settings(client, superuser): new_site_title = "UPDATED SITETITLE" response = await client.put( - f"/admin/api/v1/settings/?usr={superuser.id}", + f"/admin/api/v1/settings?usr={superuser.id}", json={"lnbits_site_title": new_site_title}, ) assert response.status_code == 200 @@ -34,7 +34,7 @@ async def test_admin_update_settings(client, superuser): @pytest.mark.asyncio async def test_admin_update_noneditable_settings(client, superuser): response = await client.put( - f"/admin/api/v1/settings/?usr={superuser.id}", + f"/admin/api/v1/settings?usr={superuser.id}", json={"super_user": "UPDATED"}, ) assert response.status_code == 400 diff --git a/tests/core/views/test_api.py b/tests/core/views/test_api.py index b90818fb74..5cb88f6f05 100644 --- a/tests/core/views/test_api.py +++ b/tests/core/views/test_api.py @@ -6,14 +6,16 @@ from lnbits import bolt11 from lnbits.core.crud import get_standalone_payment, update_payment_details from lnbits.core.models import CreateInvoice, Payment +from lnbits.core.services import fee_reserve_total from lnbits.core.views.admin_api import api_auditor -from lnbits.core.views.api import api_payment +from lnbits.core.views.payment_api import api_payment from lnbits.settings import settings from lnbits.wallets import get_wallet_class from ...helpers import ( cancel_invoice, get_random_invoice_data, + get_real_invoice, is_fake, is_regtest, pay_real_invoice, @@ -761,13 +763,13 @@ async def test_pay_hold_invoice_check_pending_and_fail_cancel_payment_task_in_me # status should still be available and be False status = await payment_db.check_status() - assert status.paid is False + assert not status.paid # now the payment should be gone after the status check - payment_db_after_status_check = await get_standalone_payment( - invoice_obj.payment_hash - ) - assert payment_db_after_status_check is None + # payment_db_after_status_check = await get_standalone_payment( + # invoice_obj.payment_hash + # ) + # assert payment_db_after_status_check is None @pytest.mark.asyncio @@ -845,3 +847,32 @@ async def listen(): assert payment_by_checking_id.pending is False assert payment_by_checking_id.bolt11 == payment_not_pending.bolt11 assert payment_by_checking_id.payment_hash == payment_not_pending.payment_hash + + +@pytest.mark.asyncio +async def test_check_fee_reserve(client, adminkey_headers_from): + # if regtest, create a real invoice, otherwise create an internal invoice + # call /api/v1/payments/fee-reserve?invoice=... with it and check if the fee reserve + # is correct + payment_request = "" + if is_regtest: + real_invoice = get_real_invoice(1000) + payment_request = real_invoice["payment_request"] + + else: + create_invoice = CreateInvoice(out=False, amount=1000, memo="test") + response = await client.post( + "/api/v1/payments", + json=create_invoice.dict(), + headers=adminkey_headers_from, + ) + assert response.status_code < 300 + invoice = response.json() + payment_request = invoice["payment_request"] + + response = await client.get( + f"/api/v1/payments/fee-reserve?invoice={payment_request}", + ) + assert response.status_code < 300 + fee_reserve = response.json() + assert fee_reserve["fee_reserve"] == fee_reserve_total(1000_000) diff --git a/tests/core/views/test_generic.py b/tests/core/views/test_generic.py index 5dd1025a0f..2a6f937f19 100644 --- a/tests/core/views/test_generic.py +++ b/tests/core/views/test_generic.py @@ -49,7 +49,7 @@ async def test_get_extensions_wrong_user(client): async def test_get_extensions_no_user(client): response = await client.get("extensions") # bad request - assert response.status_code == 400, f"{response.url} {response.status_code}" + assert response.status_code == 401, f"{response.url} {response.status_code}" # check GET /extensions: enable extension diff --git a/tests/core/views/test_node_api.py b/tests/core/views/test_node_api.py index c7fb5db785..1799c5ec01 100644 --- a/tests/core/views/test_node_api.py +++ b/tests/core/views/test_node_api.py @@ -41,8 +41,9 @@ async def public_node_client(node_client): @pytest.mark.asyncio -async def test_node_info_not_found(client): - response = await client.get("/node/api/v1/info") +async def test_node_info_not_found(client, from_super_user): + settings.lnbits_node_ui = False + response = await client.get("/node/api/v1/info", params={"usr": from_super_user.id}) assert response.status_code == HTTPStatus.SERVICE_UNAVAILABLE @@ -59,7 +60,7 @@ async def test_public_node_info(public_node_client): @pytest.mark.asyncio -async def test_node_info(node_client, from_super_user): +async def test_node_info(node_client): response = await node_client.get("/node/api/v1/info") assert response.status_code == 200 @@ -108,6 +109,7 @@ async def get_channels(): [channel for channel in data if channel.state == ChannelState.ACTIVE] ) assert close, "No active channel found" + assert close.point, "No channel point found" response = await node_client.delete( "/node/api/v1/channels", @@ -145,23 +147,23 @@ async def get_channels(): @pytest.mark.asyncio async def test_peer_management(node_client): connect_uri = get_unconnected_node_uri() - id = connect_uri.split("@")[0] + peer_id = connect_uri.split("@")[0] response = await node_client.post("/node/api/v1/peers", json={"uri": connect_uri}) assert response.status_code == 200 response = await node_client.get("/node/api/v1/peers") assert response.status_code == 200 - assert any(peer["id"] == id for peer in response.json()) + assert any(peer["id"] == peer_id for peer in response.json()) - response = await node_client.delete(f"/node/api/v1/peers/{id}") + response = await node_client.delete(f"/node/api/v1/peers/{peer_id}") assert response.status_code == 200 await asyncio.sleep(0.1) response = await node_client.get("/node/api/v1/peers") assert response.status_code == 200 - assert not any(peer["id"] == id for peer in response.json()) + assert not any(peer["id"] == peer_id for peer in response.json()) - response = await node_client.delete(f"/node/api/v1/peers/{id}") + response = await node_client.delete(f"/node/api/v1/peers/{peer_id}") assert response.status_code == 400 diff --git a/tests/helpers.py b/tests/helpers.py index cf47e16cd2..0719119c41 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -5,17 +5,24 @@ import string import time from subprocess import PIPE, Popen, TimeoutExpired -from typing import Tuple +from typing import Dict, List, Optional, Tuple, Union -import psycopg2 from loguru import logger -from sqlalchemy.engine.url import make_url +from psycopg2 import connect +from psycopg2.errors import InvalidCatalogName +from pydantic import BaseModel from lnbits import core -from lnbits.db import DB_TYPE, POSTGRES +from lnbits.db import DB_TYPE, POSTGRES, FromRowModel from lnbits.wallets import get_wallet_class, set_wallet_class +class DbTestModel(FromRowModel): + id: int + name: str + value: Optional[str] = None + + def get_random_string(N: int = 10): return "".join( random.SystemRandom().choice(string.ascii_uppercase + string.digits) @@ -36,7 +43,7 @@ async def get_random_invoice_data(): docker_lightning_cli = [ "docker", "exec", - "lnbits-legend-lnd-1-1", + "lnbits-lnd-1-1", "lncli", "--network", "regtest", @@ -46,7 +53,7 @@ async def get_random_invoice_data(): docker_bitcoin_cli = [ "docker", "exec", - "lnbits-legend-bitcoind-1-1" "bitcoin-cli", + "lnbits-bitcoind-1-1" "bitcoin-cli", "-rpcuser=lnbits", "-rpcpassword=lnbits", "-regtest", @@ -56,7 +63,7 @@ async def get_random_invoice_data(): docker_lightning_unconnected_cli = [ "docker", "exec", - "lnbits-legend-lnd-2-1", + "lnbits-lnd-2-1", "lncli", "--network", "regtest", @@ -158,24 +165,133 @@ def pay_onchain(address: str, sats: int) -> str: def clean_database(settings): if DB_TYPE == POSTGRES: - db_url = make_url(settings.lnbits_database_url) - - conn = psycopg2.connect(settings.lnbits_database_url) + conn = connect(settings.lnbits_database_url) conn.autocommit = True with conn.cursor() as cur: try: cur.execute("DROP DATABASE lnbits_test") - except psycopg2.errors.InvalidCatalogName: + except InvalidCatalogName: pass cur.execute("CREATE DATABASE lnbits_test") - - db_url.database = "lnbits_test" - settings.lnbits_database_url = str(db_url) - core.db.__init__("database") - conn.close() else: - # FIXME: do this once mock data is removed from test data folder + # TODO: do this once mock data is removed from test data folder # os.remove(settings.lnbits_data_folder + "/database.sqlite3") pass + + +def rest_wallet_fixtures_from_json(path) -> List["WalletTest"]: + with open(path) as f: + data = json.load(f) + + funding_sources = data["funding_sources"] + + tests: Dict[str, List[WalletTest]] = { + fs_name: [] for fs_name in funding_sources + } + + for fn_name in data["functions"]: + fn = data["functions"][fn_name] + + for test in fn["tests"]: + """create an unit test for each funding source""" + + for fs_name in funding_sources: + t = WalletTest( + **{ + "funding_source": FundingSourceConfig( + **funding_sources[fs_name] + ), + "function": fn_name, + **test, + "mocks": [], + } + ) + if "mocks" in test: + if fs_name not in test["mocks"]: + t.skip = True + tests[fs_name].append(t) + continue + + test_mocks_names = test["mocks"][fs_name] + + fs_mocks = fn["mocks"][fs_name] + for mock_name in fs_mocks: + for test_mock in test_mocks_names[mock_name]: + # different mocks that result in the same + # return value for the tested function + _mock = fs_mocks[mock_name] | test_mock + mock = Mock(**_mock) + + unique_test = WalletTest(**t.dict()) + unique_test.description = ( + f"""{t.description}:{mock.description or ""}""" + ) + unique_test.mocks = t.mocks + [mock] + unique_test.skip = mock.skip + + tests[fs_name].append(unique_test) + else: + # add the test without mocks + tests[fs_name].append(t) + + all_tests = sum([tests[fs_name] for fs_name in tests], []) + return all_tests + + +class FundingSourceConfig(BaseModel): + wallet_class: str + settings: dict + + +class FunctionMock(BaseModel): + uri: str + query_params: Optional[dict] + headers: dict + method: str + + +class TestMock(BaseModel): + skip: Optional[bool] + description: Optional[str] + request_type: Optional[str] + request_body: Optional[dict] + response_type: str + response: Union[str, dict] + + +class Mock(FunctionMock, TestMock): + pass + + +class FunctionMocks(BaseModel): + mocks: Dict[str, FunctionMock] + + +class FunctionTest(BaseModel): + description: str + call_params: dict + expect: dict + mocks: Dict[str, List[Dict[str, TestMock]]] + + +class FunctionData(BaseModel): + """Data required for testing this function""" + + "Function level mocks that apply for all tests of this function" + mocks: List[FunctionMock] = [] + + "All the tests for this function" + tests: List[FunctionTest] = [] + + +class WalletTest(BaseModel): + skip: Optional[bool] + function: str + description: str + funding_source: FundingSourceConfig + call_params: Optional[dict] = {} + expect: Optional[dict] + expect_error: Optional[dict] + mocks: List[Mock] = [] diff --git a/tests/wallets/fixtures.json b/tests/wallets/fixtures.json new file mode 100644 index 0000000000..75275067fa --- /dev/null +++ b/tests/wallets/fixtures.json @@ -0,0 +1,1514 @@ +{ + "funding_sources": { + "corelightningrest": { + "wallet_class": "CoreLightningRestWallet", + "settings": { + "corelightning_rest_url": "http://127.0.0.1:8555", + "corelightning_rest_macaroon": "eNcRyPtEdMaCaRoOn" + } + }, + "lndrest": { + "wallet_class": "LndRestWallet", + "settings": { + "lnd_rest_endpoint": "http://127.0.0.1:8555", + "lnd_rest_macaroon": "eNcRyPtEdMaCaRoOn", + "lnd_rest_cert": "" + } + }, + "alby": { + "wallet_class": "AlbyWallet", + "settings": { + "alby_api_endpoint": "http://127.0.0.1:8555", + "alby_access_token": "mock-alby-access-token" + } + } + }, + "functions": { + "status": { + "mocks": { + "corelightningrest": { + "status_endpoint": { + "uri": "/v1/channel/localremotebal", + "headers": { + "macaroon": "eNcRyPtEdMaCaRoOn", + "encodingtype": "hex", + "accept": "application/json", + "User-Agent": "LNbits/Tests" + }, + "method": "GET" + } + }, + "lndrest": { + "status_endpoint": { + "uri": "/v1/balance/channels", + "headers": { + "Grpc-Metadata-macaroon": "eNcRyPtEdMaCaRoOn", + "User-Agent": "LNbits/Tests" + }, + "method": "GET" + } + }, + "alby": { + "status_endpoint": { + "uri": "/balance", + "headers": { + "Authorization": "Bearer mock-alby-access-token", + "User-Agent": "LNbits/Tests" + }, + "method": "GET" + } + } + }, + "tests": [ + { + "description": "success", + "call_params": {}, + "expect": { + "error_message": null, + "balance_msat": 55000 + }, + "mocks": { + "corelightningrest": { + "status_endpoint": [ + { + "response_type": "json", + "response": { + "localBalance": 55 + } + } + ] + }, + "lndrest": { + "status_endpoint": [ + { + "response_type": "json", + "response": { + "balance": 55 + } + } + ] + }, + "alby": { + "status_endpoint": [ + { + "response_type": "json", + "response": { + "balance": 55, + "unit": "sat" + } + } + ] + } + } + }, + { + "description": "error", + "call_params": {}, + "expect": { + "error_message": "Server error: '\"test-error\"'", + "balance_msat": 0 + }, + "mocks": { + "corelightningrest": { + "status_endpoint": [ + { + "response_type": "json", + "response": { + "error": "\"test-error\"" + } + } + ] + }, + "lndrest": { + "status_endpoint": [ + { + "response_type": "json", + "response": "test-error" + } + ] + }, + "alby": { + "status_endpoint": [ + { + "response_type": "json", + "response": { + "message": "\"test-error\"", + "unit": "sats" + } + } + ] + } + } + }, + { + "description": "missing data", + "call_params": {}, + "expect": { + "error_message": "no data", + "balance_msat": 0 + }, + "mocks": { + "corelightningrest": { + "status_endpoint": [ + { + "response_type": "json", + "response": {} + } + ] + }, + "lndrest": { + "status_endpoint": [ + { + "response_type": "json", + "response": {} + } + ] + }, + "alby": { + "status_endpoint": [ + { + "response_type": "json", + "response": {} + } + ] + } + } + }, + { + "description": "bad json", + "call_params": {}, + "expect": { + "error_message": "Server error: 'invalid json response'", + "balance_msat": 0 + }, + "mocks": { + "corelightningrest": { + "status_endpoint": [ + { + "response_type": "data", + "response": "data-not-json" + } + ] + }, + "lndrest": { + "status_endpoint": [ + { + "response_type": "data", + "response": "data-not-json" + } + ] + }, + "alby": { + "status_endpoint": [ + { + "response_type": "data", + "response": "data-not-json" + } + ] + } + } + }, + { + "description": "http 404", + "call_params": {}, + "expect": { + "error_message": "Unable to connect to http://127.0.0.1:8555.", + "balance_msat": 0 + }, + "mocks": { + "corelightningrest": { + "status_endpoint": [ + { + "response_type": "response", + "response": { + "response": "Not Found", + "status": 404 + } + } + ] + }, + "lndrest": { + "status_endpoint": [ + { + "response_type": "response", + "response": { + "response": "Not Found", + "status": 404 + } + } + ] + }, + "alby": { + "status_endpoint": [ + { + "response_type": "response", + "response": { + "response": "Not Found", + "status": 404 + } + } + ] + } + } + }, + { + "description": "no mocks", + "call_params": {}, + "expect": { + "error_message": "Unable to connect to http://127.0.0.1:8555.", + "balance_msat": 0 + } + } + ] + }, + "create_invoice": { + "mocks": { + "corelightningrest": { + "create_invoice_endpoint": { + "uri": "/v1/invoice/genInvoice", + "headers": { + "macaroon": "eNcRyPtEdMaCaRoOn", + "encodingtype": "hex", + "accept": "application/json", + "User-Agent": "LNbits/Tests" + }, + "method": "POST" + } + }, + "lndrest": { + "create_invoice_endpoint": { + "uri": "/v1/invoices", + "headers": { + "Grpc-Metadata-macaroon": "eNcRyPtEdMaCaRoOn", + "User-Agent": "LNbits/Tests" + }, + "method": "POST" + } + }, + "alby": { + "create_invoice_endpoint": { + "uri": "/invoices", + "headers": { + "Authorization": "Bearer mock-alby-access-token", + "User-Agent": "LNbits/Tests" + }, + "method": "POST" + } + } + }, + "tests": [ + { + "description": "success", + "call_params": { + "amount": 555, + "memo": "Test Invoice", + "label": "test-label" + }, + "expect": { + "success": true, + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", + "payment_request": "lnbc5550n1pnq9jg3sp52rvwstvjcypjsaenzdh0h30jazvzsf8aaye0julprtth9kysxtuspp5e5s3z7felv4t9zrcc6wpn7ehvjl5yzewanzl5crljdl3jgeffyhqdq2f38xy6t5wvxqzjccqpjrzjq0yzeq76ney45hmjlnlpvu0nakzy2g35hqh0dujq8ujdpr2e42pf2rrs6vqpgcsqqqqqqqqqqqqqqeqqyg9qxpqysgqwftcx89k5pp28435pgxfl2vx3ksemzxccppw2j9yjn0ngr6ed7wj8ztc0d5kmt2mvzdlcgrludhz7jncd5l5l9w820hc4clpwhtqj3gq62g66n", + "error_message": null + }, + "mocks": { + "corelightningrest": { + "create_invoice_endpoint": [ + { + "request_type": "data", + "request_body": { + "amount": 555000, + "description": "Test Invoice", + "label": "test-label" + }, + "response_type": "json", + "response": { + "payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", + "bolt11": "lnbc5550n1pnq9jg3sp52rvwstvjcypjsaenzdh0h30jazvzsf8aaye0julprtth9kysxtuspp5e5s3z7felv4t9zrcc6wpn7ehvjl5yzewanzl5crljdl3jgeffyhqdq2f38xy6t5wvxqzjccqpjrzjq0yzeq76ney45hmjlnlpvu0nakzy2g35hqh0dujq8ujdpr2e42pf2rrs6vqpgcsqqqqqqqqqqqqqqeqqyg9qxpqysgqwftcx89k5pp28435pgxfl2vx3ksemzxccppw2j9yjn0ngr6ed7wj8ztc0d5kmt2mvzdlcgrludhz7jncd5l5l9w820hc4clpwhtqj3gq62g66n" + } + } + ] + }, + "lndrest": { + "create_invoice_endpoint": [ + { + "request_type": "json", + "request_body": { + "value": 555, + "memo": "Test Invoice", + "private": true + }, + "response_type": "json", + "response": { + "r_hash": "41UmpD0E6YVZTA36uEiBT1JLHHhlmOyaY77dstcmrJY=", + "payment_request": "lnbc5550n1pnq9jg3sp52rvwstvjcypjsaenzdh0h30jazvzsf8aaye0julprtth9kysxtuspp5e5s3z7felv4t9zrcc6wpn7ehvjl5yzewanzl5crljdl3jgeffyhqdq2f38xy6t5wvxqzjccqpjrzjq0yzeq76ney45hmjlnlpvu0nakzy2g35hqh0dujq8ujdpr2e42pf2rrs6vqpgcsqqqqqqqqqqqqqqeqqyg9qxpqysgqwftcx89k5pp28435pgxfl2vx3ksemzxccppw2j9yjn0ngr6ed7wj8ztc0d5kmt2mvzdlcgrludhz7jncd5l5l9w820hc4clpwhtqj3gq62g66n" + } + } + ] + }, + "alby": { + "create_invoice_endpoint": [ + { + "request_type": "json", + "request_body": { + "value": 555, + "memo": "Test Invoice" + }, + "response_type": "json", + "response": { + "payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", + "payment_request": "lnbc5550n1pnq9jg3sp52rvwstvjcypjsaenzdh0h30jazvzsf8aaye0julprtth9kysxtuspp5e5s3z7felv4t9zrcc6wpn7ehvjl5yzewanzl5crljdl3jgeffyhqdq2f38xy6t5wvxqzjccqpjrzjq0yzeq76ney45hmjlnlpvu0nakzy2g35hqh0dujq8ujdpr2e42pf2rrs6vqpgcsqqqqqqqqqqqqqqeqqyg9qxpqysgqwftcx89k5pp28435pgxfl2vx3ksemzxccppw2j9yjn0ngr6ed7wj8ztc0d5kmt2mvzdlcgrludhz7jncd5l5l9w820hc4clpwhtqj3gq62g66n" + } + } + ] + } + } + }, + { + "description": "error", + "call_params": { + "amount": 555, + "memo": "Test Invoice", + "label": "test-label" + }, + "expect": { + "success": false, + "checking_id": null, + "payment_request": null, + "error_message": "Server error: 'Test Error'" + }, + "mocks": { + "corelightningrest": { + "create_invoice_endpoint": [ + { + "request_type": "data", + "request_body": { + "amount": 555000, + "description": "Test Invoice", + "label": "test-label" + }, + "response_type": "json", + "response": { + "error": "Test Error" + } + } + ] + }, + "lndrest": { + "create_invoice_endpoint": [ + { + "request_type": "json", + "request_body": { + "value": 555, + "memo": "Test Invoice", + "private": true + }, + "response_type": "json", + "response": { + "error": "Test Error" + } + } + ] + }, + "alby": { + "create_invoice_endpoint": [] + } + } + }, + { + "description": "missing data", + "call_params": { + "amount": 555, + "memo": "Test Invoice", + "label": "test-label" + }, + "expect": { + "success": false, + "checking_id": null, + "payment_request": null, + "error_message": "Server error: 'missing required fields'" + }, + "mocks": { + "corelightningrest": { + "create_invoice_endpoint": [ + { + "request_type": "data", + "request_body": { + "amount": 555000, + "description": "Test Invoice", + "label": "test-label" + }, + "response_type": "json", + "response": { + "some_field": "but the required fields are mising" + } + } + ] + }, + "lndrest": { + "create_invoice_endpoint": [ + { + "request_type": "json", + "request_body": { + "value": 555, + "memo": "Test Invoice", + "private": true + }, + "response_type": "json", + "response": { + "some_field": "but the required fields are mising" + } + } + ] + }, + "alby": { + "create_invoice_endpoint": [ + { + "description": "missing payment request", + "request_type": "json", + "request_body": { + "value": 555, + "memo": "Test Invoice" + }, + "response_type": "json", + "response": { + "payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96" + } + } + ] + } + } + }, + { + "description": "bad json", + "call_params": { + "amount": 555, + "memo": "Test Invoice", + "label": "test-label" + }, + "expect": { + "success": false, + "checking_id": null, + "payment_request": null, + "error_message": "Server error: 'invalid json response'" + }, + "mocks": { + "corelightningrest": { + "create_invoice_endpoint": [ + { + "request_type": "data", + "request_body": { + "amount": 555000, + "description": "Test Invoice", + "label": "test-label" + }, + "response_type": "data", + "response": "data-not-json" + } + ] + }, + "lndrest": { + "create_invoice_endpoint": [ + { + "request_type": "json", + "request_body": { + "value": 555, + "memo": "Test Invoice", + "private": true + }, + "response_type": "data", + "response": "data-not-json" + } + ] + }, + "alby": { + "create_invoice_endpoint": [ + { + "request_type": "json", + "request_body": { + "value": 555, + "memo": "Test Invoice" + }, + "response_type": "data", + "response": "data-not-json" + } + ] + } + } + }, + { + "description": "http 404", + "call_params": { + "amount": 555, + "memo": "Test Invoice", + "label": "test-label" + }, + "expect": { + "success": false, + "checking_id": null, + "payment_request": null, + "error_message": "Unable to connect to http://127.0.0.1:8555." + }, + "mocks": { + "corelightningrest": { + "create_invoice_endpoint": [ + { + "request_type": "data", + "request_body": { + "amount": 555000, + "description": "Test Invoice", + "label": "test-label" + }, + "response_type": "response", + "response": { + "response": "Not Found", + "status": 404 + } + } + ] + }, + "lndrest": { + "create_invoice_endpoint": [ + { + "request_type": "json", + "request_body": { + "value": 555, + "memo": "Test Invoice", + "private": true + }, + "response_type": "response", + "response": { + "response": "Not Found", + "status": 404 + } + } + ] + }, + "alby": { + "create_invoice_endpoint": [ + { + "request_type": "json", + "request_body": { + "value": 555, + "memo": "Test Invoice" + }, + "response_type": "response", + "response": { + "response": "Not Found", + "status": 404 + } + } + ] + } + } + }, + { + "description": "no mocks", + "call_params": { + "amount": 555, + "memo": "Test Invoice", + "label": "test-label" + }, + "expect": { + "success": false, + "checking_id": null, + "payment_request": null, + "error_message": "Unable to connect to http://127.0.0.1:8555." + } + } + ] + }, + "pay_invoice": { + "mocks": { + "corelightningrest": { + "pay_invoice_endpoint": { + "uri": "/v1/pay", + "headers": { + "macaroon": "eNcRyPtEdMaCaRoOn", + "encodingtype": "hex", + "accept": "application/json", + "User-Agent": "LNbits/Tests" + }, + "method": "POST" + } + }, + "lndrest": { + "pay_invoice_endpoint": { + "uri": "/v1/channels/transactions", + "headers": { + "Grpc-Metadata-macaroon": "eNcRyPtEdMaCaRoOn", + "User-Agent": "LNbits/Tests" + }, + "method": "POST" + } + }, + "alby": { + "pay_invoice_endpoint": { + "uri": "/payments/bolt11", + "headers": { + "Authorization": "Bearer mock-alby-access-token", + "User-Agent": "LNbits/Tests" + }, + "method": "POST" + } + } + }, + "tests": [ + { + "description": "success", + "call_params": { + "bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu", + "fee_limit_msat": 25000 + }, + "expect": { + "success": true, + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", + "fee_msat": 50, + "preimage": "0000000000000000000000000000000000000000000000000000000000000000", + "error_message": null + }, + "mocks": { + "corelightningrest": { + "pay_invoice_endpoint": [ + { + "request_type": "data", + "request_body": { + "invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu", + "maxfeepercent": "119.04761905", + "exemptfee": 0 + }, + "response_type": "json", + "response": { + "payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", + "payment_preimage": "0000000000000000000000000000000000000000000000000000000000000000", + "msatoshi": 21000, + "msatoshi_sent": 21050, + "status": "paid" + } + } + ] + }, + "lndrest": { + "pay_invoice_endpoint": [ + { + "request_type": "json", + "request_body": { + "payment_request": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu", + "fee_limit": 25000 + }, + "response_type": "json", + "response": { + "payment_hash": "41UmpD0E6YVZTA36uEiBT1JLHHhlmOyaY77dstcmrJY=", + "payment_route": { + "total_fees_msat": 50 + }, + "payment_preimage": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + } + ] + }, + "alby": { + "pay_invoice_endpoint": [ + { + "skip": true, + "request_type": "json", + "request_body": { + "invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu" + }, + "response_type": "json", + "response": { + "payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", + "fee": 50, + "payment_preimage": "0000000000000000000000000000000000000000000000000000000000000000" + } + } + ] + } + } + }, + { + "description": "error", + "call_params": { + "bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu", + "fee_limit_msat": 25000 + }, + "expect": { + "success": false, + "checking_id": null, + "fee_msat": null, + "preimage": null, + "error_message": "Test Error" + }, + "mocks": { + "corelightningrest": { + "pay_invoice_endpoint": [ + { + "request_type": "data", + "request_body": { + "invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu", + "maxfeepercent": "119.04761905", + "exemptfee": 0 + }, + "response_type": "json", + "response": { + "error": "Test Error" + } + } + ] + }, + "lndrest": { + "pay_invoice_endpoint": [ + { + "request_type": "json", + "request_body": { + "payment_request": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu", + "fee_limit": 25000 + }, + "response_type": "json", + "response": { + "payment_error": "Test Error" + } + } + ] + }, + "alby": { + "pay_invoice_endpoint": [] + } + } + }, + { + "description": "missing data", + "call_params": { + "bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu", + "fee_limit_msat": 25000 + }, + "expect": { + "success": false, + "checking_id": null, + "fee_msat": null, + "preimage": null, + "error_message": "Server error: 'missing required fields'" + }, + "mocks": { + "corelightningrest": { + "pay_invoice_endpoint": [ + { + "request_type": "data", + "request_body": { + "invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu", + "maxfeepercent": "119.04761905", + "exemptfee": 0 + }, + "response_type": "json", + "response": {} + } + ] + }, + "lndrest": { + "pay_invoice_endpoint": [ + { + "request_type": "json", + "request_body": { + "payment_request": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu", + "fee_limit": 25000 + }, + "response_type": "json", + "response": {} + } + ] + }, + "alby": { + "pay_invoice_endpoint": [ + { + "request_type": "json", + "request_body": { + "invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu" + }, + "response_type": "json", + "response": {} + } + ] + } + } + }, + { + "description": "bad json", + "call_params": { + "bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu", + "fee_limit_msat": 25000 + }, + "expect": { + "success": false, + "checking_id": null, + "fee_msat": null, + "preimage": null, + "error_message": "Server error: 'invalid json response'" + }, + "mocks": { + "corelightningrest": { + "pay_invoice_endpoint": [ + { + "request_type": "data", + "request_body": { + "invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu", + "maxfeepercent": "119.04761905", + "exemptfee": 0 + }, + "response_type": "data", + "response": "data-not-json" + } + ] + }, + "lndrest": { + "pay_invoice_endpoint": [ + { + "request_type": "json", + "request_body": { + "payment_request": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu", + "fee_limit": 25000 + }, + "response_type": "data", + "response": "data-not-json" + } + ] + }, + "alby": { + "pay_invoice_endpoint": [ + { + "request_type": "json", + "request_body": { + "invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu" + }, + "response_type": "data", + "response": "data-not-json" + } + ] + } + } + }, + { + "description": "http 404", + "call_params": { + "bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu", + "fee_limit_msat": 25000 + }, + "expect": { + "success": false, + "checking_id": null, + "fee_msat": null, + "preimage": null, + "error_message": "Unable to connect to http://127.0.0.1:8555." + }, + "mocks": { + "corelightningrest": { + "pay_invoice_endpoint": [ + { + "request_type": "data", + "request_body": { + "invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu", + "maxfeepercent": "119.04761905", + "exemptfee": 0 + }, + "response_type": "response", + "response": { + "response": "Not Found", + "status": 404 + } + } + ] + }, + "lndrest": { + "pay_invoice_endpoint": [ + { + "request_type": "json", + "request_body": { + "payment_request": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu", + "fee_limit": 25000 + }, + "response_type": "response", + "response": { + "response": "Not Found", + "status": 404 + } + } + ] + }, + "alby": { + "pay_invoice_endpoint": [ + { + "request_type": "json", + "request_body": { + "invoice": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu" + }, + "response_type": "response", + "response": { + "response": "Not Found", + "status": 404 + } + } + ] + } + } + }, + { + "description": "no mocks", + "call_params": { + "bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu", + "fee_limit_msat": 25000 + }, + "expect": { + "success": false, + "checking_id": null, + "fee_msat": null, + "preimage": null, + "error_message": "Unable to connect to http://127.0.0.1:8555." + } + } + ] + }, + "get_invoice_status": { + "mocks": { + "corelightningrest": { + "get_invoice_status_endpoint": { + "uri": "/v1/invoice/listInvoices", + "query_params": { + "payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96" + }, + "headers": { + "macaroon": "eNcRyPtEdMaCaRoOn", + "encodingtype": "hex", + "accept": "application/json", + "User-Agent": "LNbits/Tests" + }, + "method": "GET" + } + }, + "lndrest": { + "get_invoice_status_endpoint": { + "uri": "/v1/invoice/e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", + "headers": { + "Grpc-Metadata-macaroon": "eNcRyPtEdMaCaRoOn", + "User-Agent": "LNbits/Tests" + }, + "method": "GET" + } + }, + "alby": { + "get_invoice_status_endpoint": { + "uri": "/invoices/e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", + "headers": { + "Authorization": "Bearer mock-alby-access-token", + "User-Agent": "LNbits/Tests" + }, + "method": "GET" + } + } + }, + "tests": [ + { + "description": "paid", + "call_params": { + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96" + }, + "expect": { + "success": true, + "failed": false, + "pending": false + }, + "mocks": { + "corelightningrest": { + "get_invoice_status_endpoint": [ + { + "response_type": "json", + "response": { + "invoices": [ + { + "status": "paid" + } + ] + } + }, + { + "response_type": "json", + "response": { + "invoices": [ + { + "status": "complete" + } + ] + } + } + ] + }, + "lndrest": { + "get_invoice_status_endpoint": [ + { + "response_type": "json", + "response": { + "settled": true + } + } + ] + }, + "alby": { + "get_invoice_status_endpoint": [ + { + "response_type": "json", + "response": { + "state": "SETTLED" + } + } + ] + } + } + }, + { + "description": "failed", + "description1": "pending should be false in the 'expect', this is a bug", + "call_params": { + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96" + }, + "expect": { + "success": false, + "failed": true, + "pending": true + }, + "mocks": { + "corelightningrest": { + "get_invoice_status_endpoint": [ + { + "response_type": "json", + "response": { + "invoices": [ + { + "status": "failed" + } + ] + } + } + ] + }, + "lndrest": { + "description": "lndrest.py doesn't handle the 'failed' status for `get_invoice_status`", + "get_invoice_status_endpoint": [] + }, + "alby": { + "description": "alby.py doesn't handle the 'failed' status for `get_invoice_status`", + "get_invoice_status_endpoint": [] + } + } + }, + { + "description": "pending", + "call_params": { + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96" + }, + "expect": { + "success": false, + "failed": false, + "pending": true + }, + "mocks": { + "corelightningrest": { + "get_invoice_status_endpoint": [ + { + "description": "no data", + "response_type": "json", + "response": {} + }, + { + "description": "error status", + "response_type": "json", + "response": { + "error": "test-error" + } + }, + { + "description": "bad json", + "response_type": "data", + "response": "data-not-json" + }, + { + "description": "http 404", + "response_type": "response", + "response": { + "response": "Not Found", + "status": 404 + } + } + ] + }, + "lndrest": { + "get_invoice_status_endpoint": [ + { + "description": "error status", + "response_type": "json", + "response": {} + }, + { + "description": "error status", + "response_type": "json", + "response": { + "seetled": false + } + }, + { + "description": "bad json", + "response_type": "data", + "response": "data-not-json" + }, + { + "description": "http 404", + "response_type": "response", + "response": { + "response": "Not Found", + "status": 404 + } + } + ] + }, + "alby": { + "get_invoice_status_endpoint": [ + { + "description": "error status", + "response_type": "json", + "response": {} + }, + { + "description": "error status", + "response_type": "json", + "response": { + "status": "CREATED" + } + }, + { + "description": "bad json", + "response_type": "data", + "response": "data-not-json" + }, + { + "description": "http 404", + "response_type": "response", + "response": { + "response": "Not Found", + "status": 404 + } + } + ] + } + } + }, + { + "description": "no mocks", + "call_params": { + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96" + }, + "expect": { + "success": false, + "failed": false, + "pending": true + } + } + ] + }, + "get_payment_status": { + "mocks": { + "corelightningrest": { + "get_payment_status_endpoint": { + "uri": "/v1/pay/listPays", + "query_params": { + "payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96" + }, + "headers": { + "macaroon": "eNcRyPtEdMaCaRoOn", + "encodingtype": "hex", + "accept": "application/json", + "User-Agent": "LNbits/Tests" + }, + "method": "GET" + } + }, + "lndrest": { + "get_payment_status_endpoint": { + "uri": "/v2/router/track/41UmpD0E6YVZTA36uEiBT1JLHHhlmOyaY77dstcmrJY=", + "headers": { + "Grpc-Metadata-macaroon": "eNcRyPtEdMaCaRoOn", + "User-Agent": "LNbits/Tests" + }, + "method": "GET" + } + }, + "alby": { + "get_payment_status_endpoint": { + "uri": "/invoices/e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", + "headers": { + "Authorization": "Bearer mock-alby-access-token", + "User-Agent": "LNbits/Tests" + }, + "method": "GET" + } + } + }, + "tests": [ + { + "description": "paid", + "call_params": { + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96" + }, + "expect": { + "preimage": "0000000000000000000000000000000000000000000000000000000000000000", + "success": true, + "failed": false, + "pending": false + }, + "mocks": { + "corelightningrest": { + "get_payment_status_endpoint": [ + { + "response_type": "json", + "response": { + "pays": [ + { + "status": "complete", + "amount_msat": "21000msat", + "amount_sent_msat": "-22000msat", + "preimage": "0000000000000000000000000000000000000000000000000000000000000000" + } + ] + } + } + ] + }, + "lndrest": { + "get_payment_status_endpoint": [ + { + "response_type": "stream", + "response": { + "result": { + "status": "SUCCEEDED", + "fee_msat": 1000, + "payment_preimage": "0000000000000000000000000000000000000000000000000000000000000000" + } + } + } + ] + }, + "alby": { + "get_payment_status_endpoint": [ + { + "skip": true, + "response_type": "json", + "response": { + "result": { + "status": "SETTLED" + } + } + } + ] + } + } + }, + { + "description": "failed", + "description1": "pending should be false in the 'expect', this is a bug", + "call_params": { + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96" + }, + "expect": { + "preimage": null, + "success": false, + "failed": true, + "pending": true + }, + "mocks": { + "corelightningrest": { + "get_payment_status_endpoint": [ + { + "response_type": "json", + "response": { + "pays": [ + { + "status": "failed" + } + ] + } + } + ] + }, + "lndrest": { + "get_payment_status_endpoint": [ + { + "response_type": "stream", + "response": { + "result": { + "status": "FAILED" + } + } + }, + { + "description": "error code 5", + "response_type": "stream", + "response": { + "error": { + "code": 5, + "message": "payment isn't initiated" + } + } + } + ] + }, + "alby": { + "get_payment_status_endpoint": [] + } + } + }, + { + "description": "pending", + "call_params": { + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96" + }, + "expect": { + "preimage": null, + "success": false, + "failed": false, + "pending": true + }, + "mocks": { + "corelightningrest": { + "get_payment_status_endpoint": [ + { + "description": "pending status", + "response_type": "json", + "response": { + "pays": [ + { + "status": "pending" + } + ] + } + }, + { + "description": "no data", + "response_type": "json", + "response": {} + }, + { + "description": "error status", + "response_type": "json", + "response": { + "error": "test-error" + } + }, + { + "description": "bad json", + "response_type": "data", + "response": "data-not-json" + }, + { + "description": "http 404", + "response_type": "response", + "response": { + "response": "Not Found", + "status": 404 + } + } + ] + }, + "lndrest": { + "get_payment_status_endpoint": [ + { + "description": "UNKNOWN", + "response_type": "stream", + "response": { + "result": { + "status": "UNKNOWN" + } + } + }, + { + "description": "IN_FLIGHT", + "response_type": "stream", + "response": { + "result": { + "status": "IN_FLIGHT" + } + } + }, + { + "description": "error code 4", + "response_type": "stream", + "response": { + "error": { + "code": 5, + "message": "should not fail" + } + } + }, + { + "description": "no data", + "response_type": "stream", + "response": {} + }, + { + "description": "bad json", + "response_type": "stream", + "response": "data-not-json" + }, + { + "description": "http 404", + "response_type": "response", + "response": { + "response": "Not Found", + "status": 404 + } + } + ] + }, + "alby": { + "get_payment_status_endpoint": [ + { + "description": "CREATED", + "response_type": "stream", + "response": { + "result": { + "state": "CREATED" + } + } + }, + { + "description": "no data", + "response_type": "stream", + "response": {} + }, + { + "description": "bad json", + "response_type": "stream", + "response": "data-not-json" + }, + { + "description": "http 404", + "response_type": "response", + "response": { + "response": "Not Found", + "status": 404 + } + } + ] + } + } + }, + { + "description": "no mocks", + "call_params": { + "checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96" + }, + "expect": { + "preimage": null, + "success": false, + "failed": false, + "pending": true + } + } + ] + } + } +} diff --git a/tests/wallets/test_rest_wallets.py b/tests/wallets/test_rest_wallets.py new file mode 100644 index 0000000000..2cf26f6dd7 --- /dev/null +++ b/tests/wallets/test_rest_wallets.py @@ -0,0 +1,143 @@ +import importlib +import json +from typing import Dict, Union +from urllib.parse import urlencode + +import pytest +from pytest_httpserver import HTTPServer +from werkzeug.wrappers import Response + +from lnbits.core.models import BaseWallet +from tests.helpers import ( + FundingSourceConfig, + Mock, + WalletTest, + rest_wallet_fixtures_from_json, +) + +wallets_module = importlib.import_module("lnbits.wallets") + +# todo: +# - tests for extra fields +# - tests for paid_invoices_stream +# - test particular validations + + +# specify where the server should bind to +@pytest.fixture(scope="session") +def httpserver_listen_address(): + return ("127.0.0.1", 8555) + + +def build_test_id(test: WalletTest): + return f"{test.funding_source}.{test.function}({test.description})" + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "test_data", + rest_wallet_fixtures_from_json("tests/wallets/fixtures.json"), + ids=build_test_id, +) +async def test_rest_wallet(httpserver: HTTPServer, test_data: WalletTest): + if test_data.skip: + pytest.skip() + + for mock in test_data.mocks: + _apply_mock(httpserver, mock) + + wallet = _load_funding_source(test_data.funding_source) + await _check_assertions(wallet, test_data) + + +def _apply_mock(httpserver: HTTPServer, mock: Mock): + + request_data: Dict[str, Union[str, dict]] = {} + request_type = getattr(mock.dict(), "request_type", None) + # request_type = mock.request_type <--- this des not work for whatever reason!!! + + if request_type == "data": + assert isinstance(mock.response, dict), "request data must be JSON" + request_data["data"] = urlencode(mock.response) + elif request_type == "json": + request_data["json"] = mock.response + + if mock.query_params: + request_data["query_string"] = mock.query_params + + req = httpserver.expect_request( + uri=mock.uri, + headers=mock.headers, + method=mock.method, + **request_data, # type: ignore + ) + + server_response: Union[str, dict, Response] = mock.response + response_type = mock.response_type + if response_type == "response": + assert isinstance(server_response, dict), "server response must be JSON" + server_response = Response(**server_response) + elif response_type == "stream": + response_type = "response" + server_response = Response(iter(json.dumps(server_response).splitlines())) + + respond_with = f"respond_with_{response_type}" + + getattr(req, respond_with)(server_response) + + +async def _check_assertions(wallet, _test_data: WalletTest): + test_data = _test_data.dict() + tested_func = _test_data.function + call_params = _test_data.call_params + + if "expect" in test_data: + await _assert_data(wallet, tested_func, call_params, _test_data.expect) + # if len(_test_data.mocks) == 0: + # # all calls should fail after this method is called + # await wallet.cleanup() + # # same behaviour expected is server canot be reached + # # or if the connection was closed + # await _assert_data(wallet, tested_func, call_params, _test_data.expect) + elif "expect_error" in test_data: + await _assert_error(wallet, tested_func, call_params, _test_data.expect_error) + else: + assert False, "Expected outcome not specified" + + +async def _assert_data(wallet, tested_func, call_params, expect): + resp = await getattr(wallet, tested_func)(**call_params) + for key in expect: + received = getattr(resp, key) + expected = expect[key] + assert ( + getattr(resp, key) == expect[key] + ), f"""Field "{key}". Received: "{received}". Expected: "{expected}".""" + + +async def _assert_error(wallet, tested_func, call_params, expect_error): + error_module = importlib.import_module(expect_error["module"]) + error_class = getattr(error_module, expect_error["class"]) + with pytest.raises(error_class) as e_info: + await getattr(wallet, tested_func)(**call_params) + + assert e_info.match(expect_error["message"]) + + +def _load_funding_source(funding_source: FundingSourceConfig) -> BaseWallet: + custom_settings = funding_source.settings | {"user_agent": "LNbits/Tests"} + original_settings = {} + + settings = getattr(wallets_module, "settings") + + for s in custom_settings: + original_settings[s] = getattr(settings, s) + setattr(settings, s, custom_settings[s]) + + fs_instance: BaseWallet = getattr(wallets_module, funding_source.wallet_class)() + + # rollback settings (global variable) + for s in original_settings: + setattr(settings, s, original_settings[s]) + + return fs_instance diff --git a/tools/conv.py b/tools/conv.py index 1d1a42483f..381dc378d6 100644 --- a/tools/conv.py +++ b/tools/conv.py @@ -48,7 +48,7 @@ def check_db_versions(sqdb): postgres = get_postgres_cursor() postgres.execute("SELECT * FROM public.dbversions;") - dbpost = dict(postgres.fetchall()) + dbpost = dict(postgres.fetchall()) # type: ignore for key, value in dblite.items(): if key in dblite and key in dbpost: @@ -104,7 +104,7 @@ def insert_to_pg(query, data): connection.close() -def migrate_core(file: str, exclude_tables: List[str] = None): +def migrate_core(file: str, exclude_tables: List[str] = []): print(f"Migrating core: {file}") migrate_db(file, "public", exclude_tables) print("✅ Migrated core") @@ -118,12 +118,12 @@ def migrate_ext(file: str): print(f"✅ Migrated ext: {schema}") -def migrate_db(file: str, schema: str, exclude_tables: List[str] = None): +def migrate_db(file: str, schema: str, exclude_tables: List[str] = []): # first we check if this file exists: assert os.path.isfile(file), f"{file} does not exist!" - sq = get_sqlite_cursor(file) - tables = sq.execute( + cursor = get_sqlite_cursor(file) + tables = cursor.execute( """ SELECT name FROM sqlite_master WHERE type='table' AND name not like 'sqlite?_%' escape '?' @@ -139,21 +139,21 @@ def migrate_db(file: str, schema: str, exclude_tables: List[str] = None): if exclude_tables and tableName in exclude_tables: continue - columns = sq.execute(f"PRAGMA table_info({tableName})").fetchall() + columns = cursor.execute(f"PRAGMA table_info({tableName})").fetchall() q = build_insert_query(schema, tableName, columns) - data = sq.execute(f"SELECT * FROM {tableName};").fetchall() + data = cursor.execute(f"SELECT * FROM {tableName};").fetchall() if len(data) == 0: print(f"🛑 You sneaky dev! Table {tableName} is empty!") insert_to_pg(q, data) - sq.close() + cursor.close() def build_insert_query(schema, tableName, columns): - to_columns = ", ".join(map(lambda column: f'"{column[1].lower()}"', columns)) - values = ", ".join(map(lambda column: to_column_type(column[2]), columns)) + to_columns = ", ".join([f'"{column[1].lower()}"' for column in columns]) + values = ", ".join([to_column_type(column[2]) for column in columns]) return f""" INSERT INTO {schema}.{tableName}({to_columns}) VALUES ({values}); diff --git a/tools/i18n-ai-tool.py b/tools/i18n-ai-tool.py new file mode 100644 index 0000000000..bd94a3f08d --- /dev/null +++ b/tools/i18n-ai-tool.py @@ -0,0 +1,128 @@ +# 1. Always check the results of the procedure +# 2. Always run "npx prettier -w lnbits/static/i18n/XX.js" to reformat the result + +import os +import re +import sys + +import json5 +from openai import OpenAI + +if len(sys.argv) < 2: + print("Usage: python3 tools/i18n-tool.py [language]") + sys.exit(1) +lang = sys.argv[1] + +assert os.getenv("OPENAI_API_KEY"), "OPENAI_API_KEY env var not set" + + +def load_language(lang: str) -> dict: + s = open(f"lnbits/static/i18n/{lang}.js", "rt").read() + prefix = "window.localisation.%s = {\n" % lang + assert s.startswith(prefix) + s = s[len(prefix) - 2 :] + json = json5.loads(s) + assert isinstance(json, dict) + return json + + +def save_language(lang: str, data) -> None: + with open(f"lnbits/static/i18n/{lang}.js", "wt") as f: + f.write("window.localisation.%s = {\n" % lang) + row = 0 + for k, v in data.items(): + row += 1 + f.write(" %s:\n" % k) + if "'" in v: + f.write(' "%s"' % v) + else: + f.write(" '%s'" % v) + if row == len(data): + f.write("\n") + else: + f.write(",\n") + f.write("}\n") + + +def string_variables_match(str1: str, str2: str) -> bool: + pat = re.compile(r"%\{[a-z0-9_]*\}") + m1 = re.findall(pat, str1) + m2 = re.findall(pat, str2) + return sorted(m1) == sorted(m2) + + +def translate_string(lang_from, lang_to, text): + target = { + "de": "German", + "es": "Spanish", + "jp": "Japan", + "cn": "Chinese", + "fr": "French", + "it": "Italian", + "pi": "Pirate", + "nl": "Dutch", + "we": "Welsh", + "pl": "Polish", + "pt": "Portuguese", + "br": "Brazilian Portugese", + "cs": "Czech", + "sk": "Slovak", + "kr": "Korean", + "fi": "Finnish", + }[lang_to] + client = OpenAI() + try: + chat_completion = client.chat.completions.create( + messages=[ + { + "role": "system", + "content": "You are a language expert that speaks all languages in the world. You are about to translate text from English to another language. The text is a part of the software you are translating. If the given text contains a phrase enclosed by curly preceded with a percent sign, do not translate the given phrase, just keep it verbatim. So for example, the phrase %{amount} translated to target language should still be kept as %{amount}. Never output anything else, just the translated string.", # noqa: E501 + }, + { + "role": "user", + "content": f"Translate the following string from English to {target}: {text}", # noqa: E501 + }, + ], + model="gpt-4-1106-preview", # aka GPT-4 Turbo + ) + assert chat_completion.choices[0].message.content, "No response from GPT-4" + translated = chat_completion.choices[0].message.content.strip() + # return translated string only if variables were not broken + if string_variables_match(text, translated): + return translated + else: + return None + except Exception: + return None + + +data_en = load_language("en") +data = load_language(lang) + +missing = set(data_en.keys()) - set(data.keys()) +print(f"Missing {len(missing)} keys in language '{lang}'") + +if len(missing) > 0: + new = {} + for k in data_en: + if k in data: + new[k] = data[k] + else: + print(f"Translating key '{k}'") + print(f"{data_en[k]}") + translated = translate_string("en", lang, data_en[k]) + print("->") + if translated: + print(f"{translated}") + new[k] = translated + else: + print("ERROR") + print() + save_language(lang, new) +else: + # check whether variables match for each string + for k in data_en: + if not string_variables_match(data_en[k], data[k]): + print(f"Variables mismatch ({k}):") + print(data_en[k]) + print(data[k]) diff --git a/tools/i18n-check.py b/tools/i18n-check.py new file mode 100644 index 0000000000..1c92dc800d --- /dev/null +++ b/tools/i18n-check.py @@ -0,0 +1,78 @@ +import os +import re + + +def get_translation_ids_from_source(): + # find all HTML files in selected directories + files = [] + for start in ["lnbits/core/templates", "lnbits/templates", "lnbits/static/js"]: + for check_dir, _, filenames in os.walk(start): + for filename in filenames: + if filename.endswith(".html") or filename.endswith(".js"): + fn = os.path.join(check_dir, filename) + files.append(fn) + # find all $t('...') and $t("...") calls in HTML files + # and extract the string inside the quotes + p1 = re.compile(r"\$t\('([^']*)'") + p2 = re.compile(r'\$t\("([^"]*)"') + ids = [] + for fn in files: + with open(fn, "rt") as f: + text = f.read() + m1 = re.findall(p1, text) + m2 = re.findall(p2, text) + for m in m1: + ids.append(m) + for m in m2: + ids.append(m) + return ids + + +def get_translation_ids_for_language(language): + ids = [] + for line in open(f"lnbits/static/i18n/{language}.js", "rt"): + # extract ids from lines like that start with exactly 2 spaces + if line.startswith(" ") and not line.startswith(" "): + m = line[2:].split(":")[0] + ids.append(m) + return ids + + +src_ids = get_translation_ids_from_source() +print(f"Number of ids from source: {len(src_ids)}") + +en_ids = get_translation_ids_for_language("en") +missing = set(src_ids) - set(en_ids) +extra = set(en_ids) - set(src_ids) +if len(missing) > 0: + print() + print(f'Missing ids in language "en": {len(missing)}') + for i in sorted(missing): + print(f" {i}") +if len(extra) > 0: + print() + print(f'Extraneous ids in language "en": {len(extra)}') + for i in sorted(extra): + print(f" {i}") + +languages = [] + +for *_, filenames in os.walk("lnbits/static/i18n"): + for filename in filenames: + if filename.endswith(".js") and filename not in ["i18n.js", "en.js"]: + languages.append(filename.split(".")[0]) + +for lang in sorted(languages): + ids = get_translation_ids_for_language(lang) + missing = set(en_ids) - set(ids) + extra = set(ids) - set(en_ids) + if len(missing) > 0: + print() + print(f'Missing ids in language "{lang}": {len(missing)}') + for i in sorted(missing): + print(f" {i}") + if len(extra) > 0: + print() + print(f'Extraneous ids in language "{lang}": {len(extra)}') + for i in sorted(extra): + print(f" {i}") diff --git a/tools/optipng.sh b/tools/optipng.sh new file mode 100644 index 0000000000..890d74c87e --- /dev/null +++ b/tools/optipng.sh @@ -0,0 +1 @@ +optipng -o 7 docs/logos/*.png lnbits/static/images/*.png lnbits/static/images/logos/*.png diff --git a/tools/preimage.py b/tools/preimage.py new file mode 100644 index 0000000000..48b5d05191 --- /dev/null +++ b/tools/preimage.py @@ -0,0 +1,8 @@ +import hashlib +import os + +preimage = os.urandom(32) +preimage_hash = hashlib.sha256(preimage).hexdigest() + +print(f"preimage hash: {preimage_hash}") +print(f"preimage: {preimage.hex()}")