diff --git a/.env.example b/.env.example index 90ea0d1c7..03bcf03f9 100644 --- a/.env.example +++ b/.env.example @@ -226,12 +226,12 @@ LNBITS_HIDE_API=false LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos" # Database: to use SQLite, specify LNBITS_DATA_FOLDER -# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://... +# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgresql+asyncpg://... # 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_DATABASE_URL="postgresql+asyncpg://user:password@host:port/databasename" # the service fee (in percent) LNBITS_SERVICE_FEE=0.0 diff --git a/lnbits/app.py b/lnbits/app.py index 6ea992c74..3854c173c 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -62,6 +62,7 @@ from .middleware import ( ) from .requestvars import g from .tasks import ( + check_pending_payments, create_task, internal_invoice_listener, invoice_listener, @@ -409,7 +410,7 @@ def register_async_tasks(app: FastAPI): if not settings.lnbits_extensions_deactivate_all: create_task(check_and_register_extensions(app)) - # create_permanent_task(check_pending_payments) + create_permanent_task(check_pending_payments) create_permanent_task(invoice_listener) create_permanent_task(internal_invoice_listener) create_permanent_task(cache.invalidate_forever) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index b09fa0cdf..721f84bf0 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -199,10 +199,7 @@ async def get_user_password(user_id: str) -> Optional[str]: "SELECT pass FROM accounts WHERE id = :user", {"user": user_id}, ) - if not row: - return None - - return row[0] + return row.get("pass") # TODO: refactor not a crud function @@ -488,7 +485,7 @@ async def get_user_active_extensions_ids( """, {"user": user_id}, ) - return [e[0] for e in rows] + return [e.get("extension", "") for e in rows] async def update_user_extension_extra( @@ -682,7 +679,7 @@ async def get_wallet_for_key( async def get_total_balance(conn: Optional[Connection] = None): row = await (conn or db).fetchone("SELECT SUM(balance) FROM balances") - return 0 if row[0] is None else row[0] + return row.get("balance", 0) # wallet payments @@ -1066,10 +1063,13 @@ async def get_payments_history( results.insert( 0, PaymentHistoryPoint( - balance=balance, date=row[0], income=row[1], spending=row[2] + balance=balance, + date=row.get("date", 0), + income=row.get("income", 0), + spending=row.get("spending", 0), ), ) - balance -= row.income - row.spending + balance -= row.get("income", 0) - row.get("spending", 0) return results diff --git a/lnbits/core/models.py b/lnbits/core/models.py index 4afca23bc..37bcba11c 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -7,7 +7,6 @@ import json import time from dataclasses import dataclass from enum import Enum -from sqlite3 import Row from typing import Callable, Optional from ecdsa import SECP256k1, SigningKey @@ -238,7 +237,7 @@ class Payment(FromRowModel): return self.status == PaymentState.FAILED.value @classmethod - def from_row(cls, row: Row): + def from_row(cls, row: dict): return cls( checking_id=row["checking_id"], payment_hash=row["hash"] or "0" * 64, @@ -341,7 +340,7 @@ class TinyURL(BaseModel): time: float @classmethod - def from_row(cls, row: Row): + def from_row(cls, row: dict): return cls(**dict(row)) diff --git a/lnbits/db.py b/lnbits/db.py index d2baca4ed..26cdb57b8 100644 --- a/lnbits/db.py +++ b/lnbits/db.py @@ -7,11 +7,11 @@ import re import time from contextlib import asynccontextmanager from enum import Enum -from sqlite3 import Row from typing import Any, Generic, Literal, Optional, TypeVar from loguru import logger from pydantic import BaseModel, ValidationError, root_validator +from sqlalchemy import event from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine from sqlalchemy.sql import text @@ -23,31 +23,16 @@ SQLITE = "SQLITE" if settings.lnbits_database_url: database_uri = settings.lnbits_database_url - if database_uri.startswith("cockroachdb://"): DB_TYPE = COCKROACH else: + if not database_uri.startswith("postgresql+asyncpg://"): + raise ValueError( + "Please use the 'postgresql+asyncpg://...' " + "format for the database URL." + ) DB_TYPE = POSTGRES - from psycopg2.extensions import DECIMAL, new_type, register_type - - def _parse_timestamp(value, _): - if value is None: - return None - f = "%Y-%m-%d %H:%M:%S.%f" - if "." not in value: - f = "%Y-%m-%d %H:%M:%S" - return time.mktime(datetime.datetime.strptime(value, f).timetuple()) - - register_type( - new_type( - DECIMAL.values, - "DEC2FLOAT", - lambda value, curs: float(value) if value is not None else None, - ) - ) - - register_type(new_type((1184, 1114), "TIMESTAMP2INT", _parse_timestamp)) else: if not os.path.isdir(settings.lnbits_data_folder): os.mkdir(settings.lnbits_data_folder) @@ -161,15 +146,17 @@ class Connection(Compat): clean_values[key] = raw_value return clean_values - async def fetchall(self, query: str, values: Optional[dict] = None) -> list: + async def fetchall(self, query: str, values: Optional[dict] = None) -> list[dict]: params = self.rewrite_values(values) if values else {} result = await self.conn.execute(text(self.rewrite_query(query)), params) - return result.fetchall() + row = result.mappings().all() + result.close() + return row - async def fetchone(self, query: str, values: Optional[dict] = None): + async def fetchone(self, query: str, values: Optional[dict] = None) -> dict: params = self.rewrite_values(values) if values else {} result = await self.conn.execute(text(self.rewrite_query(query)), params) - row = result.fetchone() + row = result.mappings().first() result.close() return row @@ -209,9 +196,9 @@ class Connection(Compat): if rows: # no need for extra query if no pagination is specified if filters.offset or filters.limit: - count = await self.fetchone( + result = await self.fetchone( f""" - SELECT COUNT(*) FROM ( + SELECT COUNT(*) as count FROM ( {query} {clause} {group_by_string} @@ -219,20 +206,22 @@ class Connection(Compat): """, parsed_values, ) - count = int(count[0]) + count = int(result.get("count", 0)) else: count = len(rows) else: count = 0 return Page( - data=[model.from_row(row) for row in rows] if model else rows, + data=[model.from_row(row) for row in rows] if model else [], total=count, ) async def execute(self, query: str, values: Optional[dict] = None): params = self.rewrite_values(values) if values else {} - return await self.conn.execute(text(self.rewrite_query(query)), params) + result = await self.conn.execute(text(self.rewrite_query(query)), params) + await self.conn.commit() + return result class Database(Compat): @@ -257,6 +246,28 @@ class Database(Compat): self.engine: AsyncEngine = create_async_engine( database_uri, echo=settings.debug_database ) + + if self.type in {POSTGRES, COCKROACH}: + + @event.listens_for(self.engine.sync_engine, "connect") + def register_custom_types(dbapi_connection, *_): + def _parse_timestamp(value): + if value is None: + return None + f = "%Y-%m-%d %H:%M:%S.%f" + if "." not in value: + f = "%Y-%m-%d %H:%M:%S" + return time.mktime(datetime.datetime.strptime(value, f).timetuple()) + + dbapi_connection.run_async( + lambda connection: connection.set_type_codec( + "TIMESTAMP", + encoder=datetime.datetime.timestamp, + decoder=_parse_timestamp, + schema="pg_catalog", + ) + ) + self.lock = asyncio.Lock() logger.trace(f"database {self.type} added for {self.name}") @@ -274,28 +285,22 @@ class Database(Compat): if self.schema: if self.type in {POSTGRES, COCKROACH}: await wconn.execute( - f"CREATE SCHEMA IF NOT EXISTS {self.schema}", {} + f"CREATE SCHEMA IF NOT EXISTS {self.schema}" ) elif self.type == SQLITE: - await wconn.execute( - f"ATTACH '{self.path}' AS {self.schema}", {} - ) + await wconn.execute(f"ATTACH '{self.path}' AS {self.schema}") yield wconn finally: self.lock.release() - async def fetchall(self, query: str, values: Optional[dict] = None) -> list: + async def fetchall(self, query: str, values: Optional[dict] = None) -> list[dict]: async with self.connect() as conn: - result = await conn.execute(query, values) - return result.fetchall() + return await conn.fetchall(query, values) - async def fetchone(self, query: str, values: Optional[dict] = None): + async def fetchone(self, query: str, values: Optional[dict] = None) -> dict: async with self.connect() as conn: - result = await conn.execute(query, values) - row = result.fetchone() - result.close() - return row + return await conn.fetchone(query, values) async def fetch_page( self, @@ -367,8 +372,8 @@ class Operator(Enum): class FromRowModel(BaseModel): @classmethod - def from_row(cls, row: Row): - return cls(**dict(row)) + def from_row(cls, row: dict): + return cls(**row) class FilterModel(BaseModel): @@ -421,14 +426,14 @@ class Filter(BaseModel, Generic[TFilterModel]): return cls(field=field, op=op, values=values, model=model) - # @property - # def statement(self): - # if self.op in (Operator.INCLUDE, Operator.EXCLUDE): - # placeholders = ", ".join([placeholder] * len(self.values)) - # stmt = [f"{self.field} {self.op.as_sql} ({placeholders})"] - # else: - # stmt = [f"{self.field} {self.op.as_sql} {placeholder}"] * len(self.values) - # return " OR ".join(stmt) + @property + def statement(self): + if self.op in (Operator.INCLUDE, Operator.EXCLUDE): + placeholders = ", ".join([]) + stmt = [f"{self.field} {self.op.as_sql} ({placeholders})"] + else: + stmt = [f"{self.field} {self.op.as_sql} :{self.field}"] + return " OR ".join(stmt) class Filters(BaseModel, Generic[TFilterModel]): @@ -474,9 +479,9 @@ class Filters(BaseModel, Generic[TFilterModel]): def where(self, where_stmts: Optional[list[str]] = None) -> str: if not where_stmts: where_stmts = [] - # if self.filters: - # for page_filter in self.filters: - # where_stmts.append(page_filter.statement) + if self.filters: + for page_filter in self.filters: + where_stmts.append(page_filter.statement) if self.search and self.model: fields = self.model.__search_fields__ if DB_TYPE == POSTGRES: diff --git a/poetry.lock b/poetry.lock index 12117d39b..7916f8f98 100644 --- a/poetry.lock +++ b/poetry.lock @@ -65,6 +65,74 @@ files = [ {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, ] +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[package]] +name = "asyncpg" +version = "0.29.0" +description = "An asyncio PostgreSQL driver" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "asyncpg-0.29.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72fd0ef9f00aeed37179c62282a3d14262dbbafb74ec0ba16e1b1864d8a12169"}, + {file = "asyncpg-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52e8f8f9ff6e21f9b39ca9f8e3e33a5fcdceaf5667a8c5c32bee158e313be385"}, + {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e6823a7012be8b68301342ba33b4740e5a166f6bbda0aee32bc01638491a22"}, + {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746e80d83ad5d5464cfbf94315eb6744222ab00aa4e522b704322fb182b83610"}, + {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ff8e8109cd6a46ff852a5e6bab8b0a047d7ea42fcb7ca5ae6eaae97d8eacf397"}, + {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97eb024685b1d7e72b1972863de527c11ff87960837919dac6e34754768098eb"}, + {file = "asyncpg-0.29.0-cp310-cp310-win32.whl", hash = "sha256:5bbb7f2cafd8d1fa3e65431833de2642f4b2124be61a449fa064e1a08d27e449"}, + {file = "asyncpg-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:76c3ac6530904838a4b650b2880f8e7af938ee049e769ec2fba7cd66469d7772"}, + {file = "asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4"}, + {file = "asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b"}, + {file = "asyncpg-0.29.0-cp311-cp311-win32.whl", hash = "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675"}, + {file = "asyncpg-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175"}, + {file = "asyncpg-0.29.0-cp312-cp312-win32.whl", hash = "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02"}, + {file = "asyncpg-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe"}, + {file = "asyncpg-0.29.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0009a300cae37b8c525e5b449233d59cd9868fd35431abc470a3e364d2b85cb9"}, + {file = "asyncpg-0.29.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cad1324dbb33f3ca0cd2074d5114354ed3be2b94d48ddfd88af75ebda7c43cc"}, + {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:012d01df61e009015944ac7543d6ee30c2dc1eb2f6b10b62a3f598beb6531548"}, + {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000c996c53c04770798053e1730d34e30cb645ad95a63265aec82da9093d88e7"}, + {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e0bfe9c4d3429706cf70d3249089de14d6a01192d617e9093a8e941fea8ee775"}, + {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:642a36eb41b6313ffa328e8a5c5c2b5bea6ee138546c9c3cf1bffaad8ee36dd9"}, + {file = "asyncpg-0.29.0-cp38-cp38-win32.whl", hash = "sha256:a921372bbd0aa3a5822dd0409da61b4cd50df89ae85150149f8c119f23e8c408"}, + {file = "asyncpg-0.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:103aad2b92d1506700cbf51cd8bb5441e7e72e87a7b3a2ca4e32c840f051a6a3"}, + {file = "asyncpg-0.29.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5340dd515d7e52f4c11ada32171d87c05570479dc01dc66d03ee3e150fb695da"}, + {file = "asyncpg-0.29.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e17b52c6cf83e170d3d865571ba574577ab8e533e7361a2b8ce6157d02c665d3"}, + {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f100d23f273555f4b19b74a96840aa27b85e99ba4b1f18d4ebff0734e78dc090"}, + {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48e7c58b516057126b363cec8ca02b804644fd012ef8e6c7e23386b7d5e6ce83"}, + {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9ea3f24eb4c49a615573724d88a48bd1b7821c890c2effe04f05382ed9e8810"}, + {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8d36c7f14a22ec9e928f15f92a48207546ffe68bc412f3be718eedccdf10dc5c"}, + {file = "asyncpg-0.29.0-cp39-cp39-win32.whl", hash = "sha256:797ab8123ebaed304a1fad4d7576d5376c3a006a4100380fb9d517f0b59c1ab2"}, + {file = "asyncpg-0.29.0-cp39-cp39-win_amd64.whl", hash = "sha256:cce08a178858b426ae1aa8409b5cc171def45d4293626e7aa6510696d46decd8"}, + {file = "asyncpg-0.29.0.tar.gz", hash = "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.12.0\""} + +[package.extras] +docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"] + [[package]] name = "attrs" version = "23.2.0" @@ -3183,4 +3251,4 @@ liquid = ["wallycore"] [metadata] lock-version = "2.0" python-versions = "^3.10 | ^3.9" -content-hash = "56cbae093e02e5165df2c73d62b054e71f9d431fb704726b625473720496d473" +content-hash = "e722e73e2efaad876ac742628e068b5da1596f76a83c2188d39bf9719d08a28b" diff --git a/pyproject.toml b/pyproject.toml index 72e52871d..e58390ea6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,6 @@ fastapi = "0.112.0" httpx = "0.27.0" jinja2 = "3.1.4" lnurl = "0.5.3" -psycopg2-binary = "2.9.9" pydantic = "1.10.17" pyqrcode = "1.2.1" shortuuid = "1.0.13" @@ -28,6 +27,9 @@ sse-starlette = "1.8.2" typing-extensions = "4.12.2" uvicorn = "0.30.5" sqlalchemy = "1.4.52" +aiosqlite = "^0.20.0" +asyncpg = "^0.29.0" +sse-starlette = "1.6.5" uvloop = "0.19.0" websockets = "11.0.3" loguru = "0.7.2" @@ -59,7 +61,6 @@ wallycore = {version = "1.3.0", optional = true} # needed for breez funding source breez-sdk = {version = "0.5.2", optional = true} -aiosqlite = "^0.20.0" [tool.poetry.extras] breez = ["breez-sdk"] liquid = ["wallycore"] diff --git a/tests/conftest.py b/tests/conftest.py index bd45ca8ca..b1955b917 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,7 +25,6 @@ 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 ( - clean_database, get_random_invoice_data, ) @@ -47,7 +46,6 @@ def event_loop(): # use session scope to run once before and once after all tests @pytest_asyncio.fixture(scope="session") async def app(): - clean_database(settings) app = create_app() async with LifespanManager(app) as manager: settings.first_install = False diff --git a/tests/helpers.py b/tests/helpers.py index ebb5ac1b3..dd039cfa6 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -2,11 +2,7 @@ import random import string from typing import Optional -from psycopg2 import connect -from psycopg2.errors import InvalidCatalogName - -from lnbits import core -from lnbits.db import DB_TYPE, POSTGRES, FromRowModel +from lnbits.db import FromRowModel from lnbits.wallets import get_funding_source, set_funding_source @@ -35,21 +31,3 @@ set_funding_source() funding_source = get_funding_source() is_fake: bool = funding_source.__class__.__name__ == "FakeWallet" is_regtest: bool = not is_fake - - -def clean_database(settings): - if DB_TYPE == POSTGRES: - conn = connect(settings.lnbits_database_url) - conn.autocommit = True - with conn.cursor() as cur: - try: - cur.execute("DROP DATABASE lnbits_test") - except InvalidCatalogName: - pass - cur.execute("CREATE DATABASE lnbits_test") - core.db.__init__("database") - conn.close() - else: - # TODO: do this once mock data is removed from test data folder - # os.remove(settings.lnbits_data_folder + "/database.sqlite3") - pass diff --git a/tools/conv.py b/tools/conv.py index 292312b31..ff18936ab 100644 --- a/tools/conv.py +++ b/tools/conv.py @@ -1,5 +1,5 @@ # Python script to migrate an LNbits SQLite DB to Postgres -# All credits to @Fritz446 for the awesome work +# credits to @Fritz446 for the awesome work # pip install psycopg2 OR psycopg2-binary @@ -9,10 +9,14 @@ import sqlite3 import sys from typing import List, Optional -import psycopg2 - from lnbits.settings import settings +try: + import psycopg2 +except ImportError: + print("Please install psycopg2") + sys.exit(1) + sqfolder = settings.lnbits_data_folder db_url = settings.lnbits_database_url @@ -55,8 +59,8 @@ def check_db_versions(sqdb): version = dbpost[key] if value != version: raise Exception( - f"sqlite database version ({value}) of {key} doesn't match postgres" - f" database version {version}" + f"sqlite database version ({value}) of {key} doesn't match " + f"postgres database version {version}" ) connection = postgres.connection