refactor: clean up __init__ file following some Flask conventions
Flask extensions are loaded in a way that makes them easily reusable by blueprints. In this commit we are also adding `environs` to manage .env and settings: breaking changes! - FLASK_APP=lnbits.app - LNBITS_ALLOWED_USERS needs to be empty now to allow all users (NOT "all")
This commit is contained in:
parent
ffa3c3f6a6
commit
1bc5e144d3
3
Pipfile
3
Pipfile
|
@ -10,12 +10,13 @@ python_version = "3.7"
|
|||
bitstring = "*"
|
||||
cerberus = "*"
|
||||
ecdsa = "*"
|
||||
lnurl = "*"
|
||||
environs = "*"
|
||||
flask = "*"
|
||||
flask-assets = "*"
|
||||
flask-compress = "*"
|
||||
flask-cors = "*"
|
||||
flask-talisman = "*"
|
||||
lnurl = "*"
|
||||
pyscss = "*"
|
||||
requests = "*"
|
||||
shortuuid = "*"
|
||||
|
|
53
Pipfile.lock
generated
53
Pipfile.lock
generated
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "d21f745fb8f799aaca868b4c97000f31e455063a8241366b1e0b0cd381489a0e"
|
||||
"sha256": "2270f2525e54e976b09491e458033d25ec5bbdea9e74d417e787df33031c6948"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.8"
|
||||
"python_version": "3.7"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
|
@ -100,6 +100,14 @@
|
|||
"index": "pypi",
|
||||
"version": "==0.16.0"
|
||||
},
|
||||
"environs": {
|
||||
"hashes": [
|
||||
"sha256:a98005aab7613b6fe7a1af7192a5163f72a52d3348d3918e6c7a2a32e4012779",
|
||||
"sha256:bf3fd6bc54fcfd7f512ddcb80a7781f0ced2b0c83dd123d619e9468ecdaaf537"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==8.0.0"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
"sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060",
|
||||
|
@ -209,6 +217,14 @@
|
|||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"marshmallow": {
|
||||
"hashes": [
|
||||
"sha256:67bf4cae9d3275b3fc74bd7ff88a7c98ee8c57c94b251a67b031dc293ecc4b76",
|
||||
"sha256:a2a5eefb4b75a3b43f05be1cca0b6686adf56af7465c3ca629e5ad8d1e1fe13d"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.7.1"
|
||||
},
|
||||
"pydantic": {
|
||||
"hashes": [
|
||||
"sha256:1783c1d927f9e1366e0e0609ae324039b2479a1a282a98ed6a6836c9ed02002c",
|
||||
|
@ -239,6 +255,13 @@
|
|||
"index": "pypi",
|
||||
"version": "==1.3.7"
|
||||
},
|
||||
"python-dotenv": {
|
||||
"hashes": [
|
||||
"sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d",
|
||||
"sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423"
|
||||
],
|
||||
"version": "==0.14.0"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
|
||||
|
@ -263,6 +286,15 @@
|
|||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
|
||||
"sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
|
||||
"sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==3.7.4.3"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
|
||||
|
@ -375,6 +407,14 @@
|
|||
"index": "pypi",
|
||||
"version": "==17.8.0"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83",
|
||||
"sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.7.0"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
"sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437",
|
||||
|
@ -568,7 +608,16 @@
|
|||
"sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
|
||||
"sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==3.7.4.3"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
|
||||
"sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
import re
|
||||
import importlib
|
||||
import sqlite3
|
||||
|
||||
from flask import Flask
|
||||
from flask_assets import Environment, Bundle # type: ignore
|
||||
from flask_compress import Compress # type: ignore
|
||||
from flask_cors import CORS # type: ignore
|
||||
from flask_talisman import Talisman # type: ignore
|
||||
from os import getenv
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
from .core import core_app
|
||||
from .helpers import ExtensionManager
|
||||
from .settings import FORCE_HTTPS
|
||||
from .db import open_db, open_ext_db
|
||||
|
||||
|
||||
disabled_extensions = getenv("LNBITS_DISABLED_EXTENSIONS", "").split(",")
|
||||
|
||||
app = Flask(__name__)
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) # type: ignore
|
||||
valid_extensions = [ext for ext in ExtensionManager(disabled=disabled_extensions).extensions if ext.is_valid]
|
||||
|
||||
|
||||
# optimization & security
|
||||
# -----------------------
|
||||
|
||||
Compress(app)
|
||||
CORS(app)
|
||||
Talisman(
|
||||
app,
|
||||
force_https=FORCE_HTTPS,
|
||||
content_security_policy={
|
||||
"default-src": [
|
||||
"'self'",
|
||||
"'unsafe-eval'",
|
||||
"'unsafe-inline'",
|
||||
"blob:",
|
||||
"api.opennode.co",
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# blueprints / extensions
|
||||
# -----------------------
|
||||
|
||||
app.register_blueprint(core_app)
|
||||
|
||||
for ext in valid_extensions:
|
||||
try:
|
||||
ext_module = importlib.import_module(f"lnbits.extensions.{ext.code}")
|
||||
app.register_blueprint(getattr(ext_module, f"{ext.code}_ext"), url_prefix=f"/{ext.code}")
|
||||
except Exception:
|
||||
raise ImportError(f"Please make sure that the extension `{ext.code}` follows conventions.")
|
||||
|
||||
|
||||
# filters
|
||||
# -------
|
||||
|
||||
app.jinja_env.globals["DEBUG"] = app.config["DEBUG"]
|
||||
app.jinja_env.globals["EXTENSIONS"] = valid_extensions
|
||||
app.jinja_env.globals["SITE_TITLE"] = getenv("LNBITS_SITE_TITLE", "LNbits")
|
||||
|
||||
|
||||
# assets
|
||||
# ------
|
||||
|
||||
assets = Environment(app)
|
||||
assets.url = app.static_url_path
|
||||
assets.register("base_css", Bundle("scss/base.scss", filters="pyscss", output="css/base.css"))
|
||||
|
||||
|
||||
# commands
|
||||
# --------
|
||||
|
||||
|
||||
def migrate_databases():
|
||||
"""Creates the necessary databases if they don't exist already; or migrates them."""
|
||||
|
||||
from .core import migrations as core_migrations
|
||||
|
||||
with open_db() as core_db:
|
||||
try:
|
||||
rows = core_db.fetchall("SELECT * FROM dbversions")
|
||||
except sqlite3.OperationalError:
|
||||
# migration 3 wasn't ran
|
||||
core_migrations.m000_create_migrations_table(core_db)
|
||||
rows = core_db.fetchall("SELECT * FROM dbversions")
|
||||
|
||||
current_versions = {row["db"]: row["version"] for row in rows}
|
||||
matcher = re.compile(r"^m(\d\d\d)_")
|
||||
|
||||
def run_migration(db, migrations_module):
|
||||
db_name = migrations_module.__name__.split(".")[-2]
|
||||
for key, run_migration in migrations_module.__dict__.items():
|
||||
match = match = matcher.match(key)
|
||||
if match:
|
||||
version = int(match.group(1))
|
||||
if version > current_versions.get(db_name, 0):
|
||||
print(f"running migration {db_name}.{version}")
|
||||
run_migration(db)
|
||||
core_db.execute(
|
||||
"INSERT OR REPLACE INTO dbversions (db, version) VALUES (?, ?)", (db_name, version)
|
||||
)
|
||||
|
||||
run_migration(core_db, core_migrations)
|
||||
|
||||
for ext in valid_extensions:
|
||||
try:
|
||||
ext_migrations = importlib.import_module(f"lnbits.extensions.{ext.code}.migrations")
|
||||
with open_ext_db(ext.code) as db:
|
||||
run_migration(db, ext_migrations)
|
||||
except ImportError:
|
||||
raise ImportError(f"Please make sure that the extension `{ext.code}` has a migrations file.")
|
|
@ -1,4 +1,8 @@
|
|||
from lnbits import app, migrate_databases
|
||||
from .app import create_app
|
||||
from .commands import migrate_databases
|
||||
|
||||
|
||||
migrate_databases()
|
||||
|
||||
app = create_app()
|
||||
app.run()
|
||||
|
|
75
lnbits/app.py
Normal file
75
lnbits/app.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
import importlib
|
||||
|
||||
from flask import Flask
|
||||
from flask_assets import Bundle # type: ignore
|
||||
from flask_cors import CORS # type: ignore
|
||||
from flask_talisman import Talisman # type: ignore
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
from .commands import legacy_migrate
|
||||
from .core import core_app
|
||||
from .ext import assets, compress
|
||||
from .helpers import get_valid_extensions
|
||||
|
||||
|
||||
def create_app(config_object="lnbits.settings") -> Flask:
|
||||
"""Create application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/.
|
||||
:param config_object: The configuration object to use.
|
||||
"""
|
||||
app = Flask(__name__, static_folder="static")
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) # type: ignore
|
||||
app.config.from_object(config_object)
|
||||
|
||||
register_flask_extensions(app)
|
||||
register_blueprints(app)
|
||||
register_filters(app)
|
||||
register_commands(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def register_blueprints(app) -> None:
|
||||
"""Register Flask blueprints / LNbits extensions."""
|
||||
app.register_blueprint(core_app)
|
||||
|
||||
for ext in get_valid_extensions():
|
||||
try:
|
||||
ext_module = importlib.import_module(f"lnbits.extensions.{ext.code}")
|
||||
app.register_blueprint(getattr(ext_module, f"{ext.code}_ext"), url_prefix=f"/{ext.code}")
|
||||
except Exception:
|
||||
raise ImportError(f"Please make sure that the extension `{ext.code}` follows conventions.")
|
||||
|
||||
|
||||
def register_commands(app):
|
||||
"""Register Click commands."""
|
||||
app.cli.add_command(legacy_migrate)
|
||||
|
||||
|
||||
def register_flask_extensions(app):
|
||||
"""Register Flask extensions."""
|
||||
"""If possible we use the .init_app() option so that Blueprints can also use extensions."""
|
||||
CORS(app)
|
||||
Talisman(
|
||||
app,
|
||||
force_https=app.config["FORCE_HTTPS"],
|
||||
content_security_policy={
|
||||
"default-src": [
|
||||
"'self'",
|
||||
"'unsafe-eval'",
|
||||
"'unsafe-inline'",
|
||||
"blob:",
|
||||
"api.opennode.co",
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
assets.init_app(app)
|
||||
assets.register("base_css", Bundle("scss/base.scss", filters="pyscss", output="css/base.css"))
|
||||
compress.init_app(app)
|
||||
|
||||
|
||||
def register_filters(app):
|
||||
"""Jinja filters."""
|
||||
app.jinja_env.globals["DEBUG"] = app.config["DEBUG"]
|
||||
app.jinja_env.globals["EXTENSIONS"] = get_valid_extensions()
|
||||
app.jinja_env.globals["SITE_TITLE"] = app.config["LNBITS_SITE_TITLE"]
|
51
lnbits/commands.py
Normal file
51
lnbits/commands.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
import click
|
||||
import importlib
|
||||
import re
|
||||
import sqlite3
|
||||
|
||||
from .core import migrations as core_migrations
|
||||
from .db import open_db, open_ext_db
|
||||
from .helpers import get_valid_extensions
|
||||
|
||||
|
||||
@click.command("migrate")
|
||||
def legacy_migrate():
|
||||
migrate_databases()
|
||||
|
||||
|
||||
def migrate_databases():
|
||||
"""Creates the necessary databases if they don't exist already; or migrates them."""
|
||||
|
||||
with open_db() as core_db:
|
||||
try:
|
||||
rows = core_db.fetchall("SELECT * FROM dbversions")
|
||||
except sqlite3.OperationalError:
|
||||
# migration 3 wasn't ran
|
||||
core_migrations.m000_create_migrations_table(core_db)
|
||||
rows = core_db.fetchall("SELECT * FROM dbversions")
|
||||
|
||||
current_versions = {row["db"]: row["version"] for row in rows}
|
||||
matcher = re.compile(r"^m(\d\d\d)_")
|
||||
|
||||
def run_migration(db, migrations_module):
|
||||
db_name = migrations_module.__name__.split(".")[-2]
|
||||
for key, run_migration in migrations_module.__dict__.items():
|
||||
match = match = matcher.match(key)
|
||||
if match:
|
||||
version = int(match.group(1))
|
||||
if version > current_versions.get(db_name, 0):
|
||||
print(f"running migration {db_name}.{version}")
|
||||
run_migration(db)
|
||||
core_db.execute(
|
||||
"INSERT OR REPLACE INTO dbversions (db, version) VALUES (?, ?)", (db_name, version)
|
||||
)
|
||||
|
||||
run_migration(core_db, core_migrations)
|
||||
|
||||
for ext in get_valid_extensions():
|
||||
try:
|
||||
ext_migrations = importlib.import_module(f"lnbits.extensions.{ext.code}.migrations")
|
||||
with open_ext_db(ext.code) as db:
|
||||
run_migration(db, ext_migrations)
|
||||
except ImportError:
|
||||
raise ImportError(f"Please make sure that the extension `{ext.code}` has a migrations file.")
|
|
@ -1,10 +1,10 @@
|
|||
from flask import g, abort, redirect, request, render_template, send_from_directory, url_for
|
||||
from http import HTTPStatus
|
||||
from os import getenv, path
|
||||
from os import path
|
||||
|
||||
from lnbits.core import core_app
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.settings import SERVICE_FEE
|
||||
from lnbits.settings import LNBITS_ALLOWED_USERS, SERVICE_FEE
|
||||
|
||||
from ..crud import (
|
||||
create_account,
|
||||
|
@ -61,9 +61,8 @@ def wallet():
|
|||
user = get_user(create_account().id)
|
||||
else:
|
||||
user = get_user(user_id) or abort(HTTPStatus.NOT_FOUND, "User does not exist.")
|
||||
allowed_users = getenv("LNBITS_ALLOWED_USERS", "all")
|
||||
|
||||
if allowed_users != "all" and user_id not in allowed_users.split(","):
|
||||
if LNBITS_ALLOWED_USERS and user_id not in LNBITS_ALLOWED_USERS:
|
||||
abort(HTTPStatus.UNAUTHORIZED, "User not authorized.")
|
||||
|
||||
if not wallet_id:
|
||||
|
|
|
@ -2,11 +2,11 @@ from cerberus import Validator # type: ignore
|
|||
from flask import g, abort, jsonify, request
|
||||
from functools import wraps
|
||||
from http import HTTPStatus
|
||||
from os import getenv
|
||||
from typing import List, Union
|
||||
from uuid import UUID
|
||||
|
||||
from lnbits.core.crud import get_user, get_wallet_for_key
|
||||
from lnbits.settings import LNBITS_ALLOWED_USERS
|
||||
|
||||
|
||||
def api_check_wallet_key(key_type: str = "invoice"):
|
||||
|
@ -62,9 +62,8 @@ def check_user_exists(param: str = "usr"):
|
|||
@wraps(view)
|
||||
def wrapped_view(**kwargs):
|
||||
g.user = get_user(request.args.get(param, type=str)) or abort(HTTPStatus.NOT_FOUND, "User does not exist.")
|
||||
allowed_users = getenv("LNBITS_ALLOWED_USERS", "all")
|
||||
|
||||
if allowed_users != "all" and g.user.id not in allowed_users.split(","):
|
||||
if LNBITS_ALLOWED_USERS and g.user.id not in LNBITS_ALLOWED_USERS:
|
||||
abort(HTTPStatus.UNAUTHORIZED, "User not authorized.")
|
||||
|
||||
return view(**kwargs)
|
||||
|
|
6
lnbits/ext.py
Normal file
6
lnbits/ext.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from flask_assets import Environment # type: ignore
|
||||
from flask_compress import Compress # type: ignore
|
||||
|
||||
|
||||
assets = Environment()
|
||||
compress = Compress()
|
|
@ -4,7 +4,7 @@ import shortuuid # type: ignore
|
|||
|
||||
from typing import List, NamedTuple, Optional
|
||||
|
||||
from .settings import LNBITS_PATH
|
||||
from .settings import LNBITS_DISABLED_EXTENSIONS, LNBITS_PATH
|
||||
|
||||
|
||||
class Extension(NamedTuple):
|
||||
|
@ -17,8 +17,8 @@ class Extension(NamedTuple):
|
|||
|
||||
|
||||
class ExtensionManager:
|
||||
def __init__(self, *, disabled: list = []):
|
||||
self._disabled = disabled
|
||||
def __init__(self):
|
||||
self._disabled: List[str] = LNBITS_DISABLED_EXTENSIONS
|
||||
self._extension_folders: List[str] = [x[1] for x in os.walk(os.path.join(LNBITS_PATH, "extensions"))][0]
|
||||
|
||||
@property
|
||||
|
@ -48,5 +48,9 @@ class ExtensionManager:
|
|||
return output
|
||||
|
||||
|
||||
def get_valid_extensions() -> List[Extension]:
|
||||
return [extension for extension in ExtensionManager().extensions if extension.is_valid]
|
||||
|
||||
|
||||
def urlsafe_short_hash() -> str:
|
||||
return shortuuid.uuid()
|
||||
|
|
|
@ -1,14 +1,26 @@
|
|||
import importlib
|
||||
import os
|
||||
|
||||
from environs import Env # type: ignore
|
||||
from os import path
|
||||
from typing import List
|
||||
|
||||
|
||||
env = Env()
|
||||
env.read_env()
|
||||
|
||||
wallets_module = importlib.import_module("lnbits.wallets")
|
||||
wallet_class = getattr(wallets_module, os.getenv("LNBITS_BACKEND_WALLET_CLASS", "VoidWallet"))
|
||||
wallet_class = getattr(wallets_module, env.str("LNBITS_BACKEND_WALLET_CLASS", default="VoidWallet"))
|
||||
|
||||
LNBITS_PATH = os.path.dirname(os.path.realpath(__file__))
|
||||
LNBITS_DATA_FOLDER = os.getenv("LNBITS_DATA_FOLDER", os.path.join(LNBITS_PATH, "data"))
|
||||
ENV = env.str("FLASK_ENV", default="production")
|
||||
DEBUG = ENV == "development"
|
||||
|
||||
LNBITS_PATH = path.dirname(path.realpath(__file__))
|
||||
LNBITS_DATA_FOLDER = env.str("LNBITS_DATA_FOLDER", default=path.join(LNBITS_PATH, "data"))
|
||||
LNBITS_ALLOWED_USERS: List[str] = env.list("LNBITS_ALLOWED_USERS", default=[], subcast=str)
|
||||
LNBITS_DISABLED_EXTENSIONS: List[str] = env.list("LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str)
|
||||
LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits")
|
||||
|
||||
WALLET = wallet_class()
|
||||
DEFAULT_WALLET_NAME = os.getenv("LNBITS_DEFAULT_WALLET_NAME", "LNbits wallet")
|
||||
FORCE_HTTPS = os.getenv("LNBITS_FORCE_HTTPS", "1") == "1"
|
||||
SERVICE_FEE = float(os.getenv("LNBITS_SERVICE_FEE", "0.0"))
|
||||
DEFAULT_WALLET_NAME = env.str("LNBITS_DEFAULT_WALLET_NAME", default="LNbits wallet")
|
||||
FORCE_HTTPS = env.bool("LNBITS_FORCE_HTTPS", default=True)
|
||||
SERVICE_FEE = env.float("LNBITS_SERVICE_FEE", default=0.0)
|
||||
|
|
|
@ -6,6 +6,7 @@ certifi==2020.6.20
|
|||
chardet==3.0.4
|
||||
click==7.1.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
ecdsa==0.16.0
|
||||
environs==8.0.0
|
||||
flask-assets==2.0
|
||||
flask-compress==1.5.0
|
||||
flask-cors==3.0.9
|
||||
|
@ -16,8 +17,10 @@ itsdangerous==1.1.0; python_version >= '2.7' and python_version not in '3.0, 3.1
|
|||
jinja2==2.11.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
lnurl==0.3.5
|
||||
markupsafe==1.1.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
marshmallow==3.7.1; python_version >= '3.5'
|
||||
pydantic==1.6.1; python_version >= '3.6'
|
||||
pyscss==1.3.7
|
||||
python-dotenv==0.14.0
|
||||
requests==2.24.0
|
||||
shortuuid==1.0.1
|
||||
six==1.15.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import pytest
|
||||
|
||||
from lnbits import app
|
||||
from lnbits.app import create_app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app = create_app()
|
||||
app.config["TESTING"] = True
|
||||
|
||||
with app.test_client() as client:
|
||||
|
|
Loading…
Reference in New Issue
Block a user