async postgres

add timestamp conversation

pending fixed now

statement

repair conv

remove cleandbsetting
This commit is contained in:
dni ⚡
2024-09-05 12:38:50 +02:00
parent 08ea83c28e
commit 835f56cee6
10 changed files with 156 additions and 102 deletions
+2 -2
View File
@@ -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
+2 -1
View File
@@ -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)
+8 -8
View File
@@ -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
+2 -3
View File
@@ -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))
+60 -55
View File
@@ -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:
Generated
+69 -1
View File
@@ -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"
+3 -2
View File
@@ -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"]
-2
View File
@@ -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
+1 -23
View File
@@ -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
+9 -5
View File
@@ -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