Compare commits
7
Commits
v1.4.1-rc2
...
v1.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef9adc077b | ||
|
|
ffad5d79c4 | ||
|
|
09c35eaca0 | ||
|
|
52adc62bc4 | ||
|
|
0efa173084 | ||
|
|
724f8c9f63 | ||
|
|
c6981089e5 |
@@ -245,7 +245,7 @@ async def api_users_create_user_wallet(
|
||||
"/user/{user_id}/wallet/{wallet}/undelete", name="Reactivate deleted wallet"
|
||||
)
|
||||
async def api_users_undelete_user_wallet(user_id: str, wallet: str) -> SimpleStatus:
|
||||
wal = await get_wallet(wallet)
|
||||
wal = await get_wallet(wallet, deleted=True)
|
||||
if not wal:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
|
||||
Generated
+8
-7
@@ -1367,14 +1367,14 @@ pyjwt = ">=2.10.1,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.20.1"
|
||||
version = "3.20.3"
|
||||
description = "A platform independent file lock."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a"},
|
||||
{file = "filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c"},
|
||||
{file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"},
|
||||
{file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4172,20 +4172,21 @@ test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil",
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.31.2"
|
||||
version = "20.36.1"
|
||||
description = "Virtual Python Environment builder"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11"},
|
||||
{file = "virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af"},
|
||||
{file = "virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f"},
|
||||
{file = "virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
distlib = ">=0.3.7,<1"
|
||||
filelock = ">=3.12.2,<4"
|
||||
filelock = {version = ">=3.20.1,<4", markers = "python_version >= \"3.10\""}
|
||||
platformdirs = ">=3.9.1,<5"
|
||||
typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "lnbits"
|
||||
version = "1.4.1-rc2"
|
||||
version = "1.4.1"
|
||||
requires-python = ">=3.10,<3.13"
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
|
||||
|
||||
@@ -552,3 +552,61 @@ async def test_delete_user_success(http_client: AsyncClient, superuser_token):
|
||||
|
||||
wallets = await get_wallets(user_id=user_id)
|
||||
assert len(wallets) == 0
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_delete_and_undelete_wallet(http_client: AsyncClient, superuser_token):
|
||||
# Create a user
|
||||
tiny_id = shortuuid.uuid()[:8]
|
||||
user_data = {
|
||||
"username": f"walletuser_{tiny_id}",
|
||||
"password": "secret1234",
|
||||
"password_repeat": "secret1234",
|
||||
"email": f"walletuser_{tiny_id}@lnbits.com",
|
||||
}
|
||||
user_resp = await http_client.post(
|
||||
"/users/api/v1/user",
|
||||
json=user_data,
|
||||
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||
)
|
||||
assert user_resp.status_code == 200
|
||||
user = user_resp.json()
|
||||
user_id = user["id"]
|
||||
|
||||
# Create a wallet for the user
|
||||
wallet_resp = await http_client.post(
|
||||
f"/users/api/v1/user/{user_id}/wallet",
|
||||
json={"name": "Test Wallet"},
|
||||
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||
)
|
||||
assert wallet_resp.status_code == 200
|
||||
wallet = wallet_resp.json()
|
||||
wallet_id = wallet["id"]
|
||||
|
||||
# Delete the wallet (soft delete)
|
||||
delete_resp = await http_client.delete(
|
||||
f"/users/api/v1/user/{user_id}/wallet/{wallet_id}",
|
||||
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||
)
|
||||
assert delete_resp.status_code == 200
|
||||
assert delete_resp.json()["success"] is True
|
||||
|
||||
# Wallet should be marked as deleted
|
||||
wallets = await get_wallets(user_id=user_id, deleted=True)
|
||||
deleted_wallet = next((w for w in wallets if w.id == wallet_id), None)
|
||||
assert deleted_wallet is not None
|
||||
assert deleted_wallet.deleted is True
|
||||
|
||||
# Undelete the wallet
|
||||
undelete_resp = await http_client.put(
|
||||
f"/users/api/v1/user/{user_id}/wallet/{wallet_id}/undelete",
|
||||
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||
)
|
||||
assert undelete_resp.status_code == 200
|
||||
assert undelete_resp.json()["success"] is True
|
||||
|
||||
# Wallet should be active again
|
||||
wallets = await get_wallets(user_id=user_id, deleted=False)
|
||||
undeleted_wallet = next((w for w in wallets if w.id == wallet_id), None)
|
||||
assert undeleted_wallet is not None
|
||||
assert undeleted_wallet.deleted is False
|
||||
|
||||
+53
-16
@@ -8,19 +8,21 @@ import os
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.settings import settings
|
||||
|
||||
try:
|
||||
import psycopg2 # type: ignore
|
||||
except ImportError:
|
||||
print("Please install psycopg2")
|
||||
logger.warning("Please install psycopg2")
|
||||
sys.exit(1)
|
||||
|
||||
sqfolder = settings.lnbits_data_folder
|
||||
db_url = settings.lnbits_database_url
|
||||
|
||||
if db_url is None:
|
||||
print("missing LNBITS_DATABASE_URL")
|
||||
logger.warning("missing LNBITS_DATABASE_URL")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# parse postgres://lnbits:postgres@localhost:5432/lnbits
|
||||
@@ -66,7 +68,7 @@ def check_db_versions(sqdb):
|
||||
postgres.close()
|
||||
connection.close()
|
||||
|
||||
print("Database versions OK, converting")
|
||||
logger.info("Database versions OK, converting")
|
||||
|
||||
|
||||
def fix_id(seq, values):
|
||||
@@ -95,11 +97,11 @@ def insert_to_pg(query, data):
|
||||
cursor.execute(query, d)
|
||||
except Exception as exc:
|
||||
if args.ignore_errors:
|
||||
print(exc)
|
||||
print(f"Failed to insert {d}")
|
||||
logger.error(exc)
|
||||
logger.error(f"Failed to insert {d}")
|
||||
else:
|
||||
print("query:", query)
|
||||
print("data:", d)
|
||||
logger.error("query:", query)
|
||||
logger.error("data:", d)
|
||||
raise ValueError(f"Failed to insert {d}") from exc
|
||||
connection.commit()
|
||||
|
||||
@@ -110,17 +112,20 @@ def insert_to_pg(query, data):
|
||||
def migrate_core(file: str, exclude_tables: list[str] | None = None):
|
||||
if exclude_tables is None:
|
||||
exclude_tables = []
|
||||
print(f"Migrating core: {file}")
|
||||
logger.info(f"Migrating core: {file}")
|
||||
migrate_db(file, "public", exclude_tables)
|
||||
print("✅ Migrated core")
|
||||
logger.info("✅ Migrated core")
|
||||
|
||||
|
||||
def migrate_ext(file: str):
|
||||
filename = os.path.basename(file)
|
||||
schema = filename.replace("ext_", "").split(".")[0]
|
||||
print(f"Migrating ext: {schema} from file {file}")
|
||||
migrate_db(file, schema)
|
||||
print(f"✅ Migrated ext: {schema}")
|
||||
try:
|
||||
logger.info(f"Migrating ext: {schema} from file {file}")
|
||||
migrate_db(file, schema)
|
||||
logger.info(f"✅ Migrated ext: {schema}")
|
||||
except Exception as exc:
|
||||
logger.error(f"🛑 Failed to migrate extension {schema}: {exc}")
|
||||
|
||||
|
||||
def migrate_db(file: str, schema: str, exclude_tables: list[str] | None = None):
|
||||
@@ -139,7 +144,7 @@ def migrate_db(file: str, schema: str, exclude_tables: list[str] | None = None):
|
||||
|
||||
for table in tables:
|
||||
table_name = table[0]
|
||||
print(f"Migrating table {table_name}")
|
||||
logger.info(f"Migrating table {table_name}")
|
||||
# hard coded skip for dbversions (already produced during startup)
|
||||
if table_name == "dbversions":
|
||||
continue
|
||||
@@ -152,7 +157,7 @@ def migrate_db(file: str, schema: str, exclude_tables: list[str] | None = None):
|
||||
data = cursor.execute(f"SELECT * FROM {table_name};").fetchall()
|
||||
|
||||
if len(data) == 0:
|
||||
print(f"🛑 You sneaky dev! Table {table_name} is empty!")
|
||||
logger.warning(f"🛑 You sneaky dev! Table {table_name} is empty!")
|
||||
|
||||
insert_to_pg(q, data)
|
||||
cursor.close()
|
||||
@@ -161,12 +166,44 @@ def migrate_db(file: str, schema: str, exclude_tables: list[str] | None = None):
|
||||
def build_insert_query(schema, table_name, columns):
|
||||
to_columns = ", ".join([f'"{column[1].lower()}"' for column in columns])
|
||||
values = ", ".join([to_column_type(column[2]) for column in columns])
|
||||
on_conflict_update = build_on_conflict_query_statement(schema, table_name, columns)
|
||||
return f"""
|
||||
INSERT INTO {schema}.{table_name}({to_columns})
|
||||
VALUES ({values});
|
||||
VALUES ({values})
|
||||
{on_conflict_update}
|
||||
"""
|
||||
|
||||
|
||||
def build_on_conflict_query_statement(schema, table_name, columns):
|
||||
unique_cols = table_unique_columns(schema, table_name)
|
||||
if len(unique_cols) == 0:
|
||||
return ""
|
||||
return f"""
|
||||
ON CONFLICT ({", ".join([f'"{col}"' for col in unique_cols])})
|
||||
DO UPDATE SET
|
||||
{", ".join([
|
||||
f'"{column[1].lower()}"=EXCLUDED."{column[1].lower()}"'
|
||||
for column in columns
|
||||
])}
|
||||
"""
|
||||
|
||||
|
||||
def table_unique_columns(schema, table_name):
|
||||
cursor = get_postgres_cursor()
|
||||
query = f"""
|
||||
SELECT a.attname
|
||||
FROM pg_index i
|
||||
JOIN pg_attribute a ON a.attrelid = i.indrelid
|
||||
AND a.attnum = ANY(i.indkey)
|
||||
WHERE i.indrelid = '{schema}.{table_name}'::regclass
|
||||
AND i.indisunique;
|
||||
"""
|
||||
cursor.execute(query)
|
||||
columns = [row[0] for row in cursor.fetchall()]
|
||||
cursor.close()
|
||||
return columns
|
||||
|
||||
|
||||
def to_column_type(column_type):
|
||||
if column_type == "TIMESTAMP":
|
||||
return "to_timestamp(%s)"
|
||||
@@ -218,7 +255,7 @@ parser.add_argument(
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("Selected path: ", args.sqlite_path)
|
||||
logger.info("Selected path: ", args.sqlite_path)
|
||||
|
||||
if os.path.isdir(args.sqlite_path):
|
||||
exclude_tables = ["dbversions"]
|
||||
|
||||
@@ -800,11 +800,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.20.0"
|
||||
version = "3.20.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1238,7 +1238,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "lnbits"
|
||||
version = "1.4.1rc2"
|
||||
version = "1.4.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
@@ -2684,7 +2684,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.35.4"
|
||||
version = "20.36.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "distlib" },
|
||||
@@ -2692,9 +2692,9 @@ dependencies = [
|
||||
{ name = "platformdirs" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user