This commit is contained in:
dni ⚡
2024-10-17 10:36:33 +02:00
parent 2764fefb3d
commit bdeac94d99
4 changed files with 52 additions and 48 deletions
+3 -3
View File
@@ -13,7 +13,7 @@ from ecdsa import SECP256k1, SigningKey
from fastapi import Query from fastapi import Query
from pydantic import BaseModel, validator from pydantic import BaseModel, validator
from lnbits.db import FilterModel, FromRowModel from lnbits.db import FilterModel
from lnbits.helpers import url_for from lnbits.helpers import url_for
from lnbits.lnurl import encode as lnurl_encode from lnbits.lnurl import encode as lnurl_encode
from lnbits.settings import settings from lnbits.settings import settings
@@ -103,7 +103,7 @@ class UserConfig(BaseModel):
provider: Optional[str] = "lnbits" # auth provider provider: Optional[str] = "lnbits" # auth provider
class Account(FromRowModel): class Account(BaseModel):
id: str id: str
is_super_user: Optional[bool] = False is_super_user: Optional[bool] = False
is_admin: Optional[bool] = False is_admin: Optional[bool] = False
@@ -244,7 +244,7 @@ class CreatePayment(BaseModel):
fee: int = 0 fee: int = 0
class Payment(FromRowModel): class Payment(BaseModel):
status: str status: str
# TODO should be removed in the future, backward compatibility # TODO should be removed in the future, backward compatibility
pending: bool pending: bool
+28 -20
View File
@@ -146,7 +146,10 @@ class Connection(Compat):
return clean_values return clean_values
async def fetchall( async def fetchall(
self, query: str, values: Optional[dict] = None, model: Optional[TModel] = None self,
query: str,
values: Optional[dict] = None,
model: Optional[type[TModel]] = None,
) -> list[TModel]: ) -> list[TModel]:
params = self.rewrite_values(values) if values else {} params = self.rewrite_values(values) if values else {}
result = await self.conn.execute(text(self.rewrite_query(query)), params) result = await self.conn.execute(text(self.rewrite_query(query)), params)
@@ -159,7 +162,10 @@ class Connection(Compat):
return row return row
async def fetchone( async def fetchone(
self, query: str, values: Optional[dict] = None, model: Optional[TModel] = None self,
query: str,
values: Optional[dict] = None,
model: Optional[type[TModel]] = None,
) -> TModel: ) -> TModel:
params = self.rewrite_values(values) if values else {} params = self.rewrite_values(values) if values else {}
result = await self.conn.execute(text(self.rewrite_query(query)), params) result = await self.conn.execute(text(self.rewrite_query(query)), params)
@@ -187,9 +193,9 @@ class Connection(Compat):
where: Optional[list[str]] = None, where: Optional[list[str]] = None,
values: Optional[dict] = None, values: Optional[dict] = None,
filters: Optional[Filters] = None, filters: Optional[Filters] = None,
model: Optional[type[TRowModel]] = None, model: Optional[type[TModel]] = None,
group_by: Optional[list[str]] = None, group_by: Optional[list[str]] = None,
) -> Page[TRowModel]: ) -> Page[TModel]:
if not filters: if not filters:
filters = Filters() filters = Filters()
clause = filters.where(where) clause = filters.where(where)
@@ -213,11 +219,12 @@ class Connection(Compat):
{filters.pagination()} {filters.pagination()}
""", """,
self.rewrite_values(parsed_values), self.rewrite_values(parsed_values),
model,
) )
if rows: if rows:
# no need for extra query if no pagination is specified # no need for extra query if no pagination is specified
if filters.offset or filters.limit: if filters.offset or filters.limit:
result = await self.fetchone( result = await self.execute(
f""" f"""
SELECT COUNT(*) as count FROM ( SELECT COUNT(*) as count FROM (
{query} {query}
@@ -234,7 +241,7 @@ class Connection(Compat):
count = 0 count = 0
return Page( return Page(
data=[model.from_row(row) for row in rows] if model else [], data=rows,
total=count, total=count,
) )
@@ -343,9 +350,9 @@ class Database(Compat):
where: Optional[list[str]] = None, where: Optional[list[str]] = None,
values: Optional[dict] = None, values: Optional[dict] = None,
filters: Optional[Filters] = None, filters: Optional[Filters] = None,
model: Optional[type[TRowModel]] = None, model: Optional[type[TModel]] = None,
group_by: Optional[list[str]] = None, group_by: Optional[list[str]] = None,
) -> Page[TRowModel]: ) -> Page[TModel]:
async with self.connect() as conn: async with self.connect() as conn:
return await conn.fetch_page(query, where, values, filters, model, group_by) return await conn.fetch_page(query, where, values, filters, model, group_by)
@@ -405,12 +412,6 @@ class Operator(Enum):
raise ValueError("Unknown SQL Operator") raise ValueError("Unknown SQL Operator")
class FromRowModel(BaseModel):
@classmethod
def from_row(cls, row: dict):
return cls(**row)
class FilterModel(BaseModel): class FilterModel(BaseModel):
__search_fields__: list[str] = [] __search_fields__: list[str] = []
__sort_fields__: Optional[list[str]] = None __sort_fields__: Optional[list[str]] = None
@@ -418,7 +419,6 @@ class FilterModel(BaseModel):
T = TypeVar("T") T = TypeVar("T")
TModel = TypeVar("TModel", bound=BaseModel) TModel = TypeVar("TModel", bound=BaseModel)
TRowModel = TypeVar("TRowModel", bound=FromRowModel)
TFilterModel = TypeVar("TFilterModel", bound=FilterModel) TFilterModel = TypeVar("TFilterModel", bound=FilterModel)
@@ -585,19 +585,27 @@ def update_query(
def model_to_dict(model: BaseModel) -> dict: def model_to_dict(model: BaseModel) -> dict:
"""
Convert a Pydantic model to a dictionary with JSON-encoded nested models
TODO: no recursion, maybe make them recursive?
"""
_dict = model.dict() _dict = model.dict()
for key, value in _dict.items(): for key, value in _dict.items():
if key.startswith("_"): if key.startswith("_"):
continue continue
type_ = model.__fields__[key].type_ type_ = model.__fields__[key].type_
if type_ == BaseModel: if type(type_) is type(BaseModel):
_dict[key] = json.dumps(value.dict()) _dict[key] = json.dumps(value)
return _dict return _dict
def dict_to_model(_dict: dict, model: TModel) -> TModel: def dict_to_model(_dict: dict, model: type[TModel]) -> TModel:
"""
Convert a dictionary with JSON-encoded nested models to a Pydantic model
TODO: no recursion, maybe make them recursive?
"""
for key, value in _dict.items(): for key, value in _dict.items():
type_ = model.__fields__[key].type_ type_ = model.__fields__[key].type_
if type_ is BaseModel: if issubclass(type_, BaseModel):
_dict[key] = json.loads(value) _dict[key] = type_.construct(**json.loads(value))
return model.construct(**_dict) return model.construct(**_dict)
+4 -11
View File
@@ -4,7 +4,6 @@ from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
from lnbits.db import FromRowModel
from lnbits.wallets import get_funding_source, set_funding_source from lnbits.wallets import get_funding_source, set_funding_source
@@ -12,23 +11,17 @@ class FakeError(Exception):
pass pass
class DbTestModel(FromRowModel): class DbTestModel(BaseModel):
id: int id: int
name: str name: str
value: Optional[str] = None value: Optional[str] = None
class DbTestModelInner(BaseModel):
id: int
label: str
description: Optional[str] = None
class DbTestModel2(BaseModel): class DbTestModel2(BaseModel):
id: int id: int
name: str label: str
value: Optional[str] = None description: Optional[str] = None
child: DbTestModelInner child: DbTestModel
def get_random_string(iterations: int = 10): def get_random_string(iterations: int = 10):
+17 -14
View File
@@ -1,3 +1,5 @@
import json
import pytest import pytest
from lnbits.db import ( from lnbits.db import (
@@ -6,13 +8,13 @@ from lnbits.db import (
model_to_dict, model_to_dict,
update_query, update_query,
) )
from tests.helpers import DbTestModel2, DbTestModelInner from tests.helpers import DbTestModel, DbTestModel2
test_data = DbTestModel2( test_data = DbTestModel2(
id=1, id=1,
name="test", label="test",
value="myvalue", description="mydesc",
child=DbTestModelInner(id=2, label="mylabel", description="mydesc"), child=DbTestModel(id=2, name="myname", value="myvalue"),
) )
@@ -20,8 +22,8 @@ test_data = DbTestModel2(
async def test_helpers_insert_query(): async def test_helpers_insert_query():
q = insert_query("test_helpers_query", test_data) q = insert_query("test_helpers_query", test_data)
assert ( assert (
q == "INSERT INTO test_helpers_query (id, name, value, child) " q == "INSERT INTO test_helpers_query (id, label, description, child) "
"VALUES (:id, :name, :value, :child)" "VALUES (:id, :label, :description, :child)"
) )
@@ -30,23 +32,24 @@ async def test_helpers_update_query():
q = update_query("test_helpers_query", test_data) q = update_query("test_helpers_query", test_data)
assert ( assert (
q == "UPDATE test_helpers_query " q == "UPDATE test_helpers_query "
"SET id = :id, name = :name, value = :value, child = :child " "SET id = :id, label = :label, description = :description, child = :child "
"WHERE id = :id" "WHERE id = :id"
) )
child_dict = json.dumps({"id": 2, "name": "myname", "value": "myvalue"})
test_dict = {"id": 1, "label": "test", "description": "mydesc", "child": child_dict}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_helpers_model_to_dict(): async def test_helpers_model_to_dict():
d = model_to_dict(test_data) d = model_to_dict(test_data)
assert d == { assert d == test_dict
"id": 1,
"name": "test",
"value": "myvalue",
"child": {"id": 2, "label": "mylabel", "description": "mydesc"},
}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_helpers_dict_to_model(): async def test_helpers_dict_to_model():
m = dict_to_model(model_to_dict(test_data), DbTestModel2) m = dict_to_model(test_dict, DbTestModel2)
assert m == test_data assert m == test_data
assert type(m) is DbTestModel2
assert type(m.child) is DbTestModel