Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5388e23a9 | ||
|
|
d01893f577 | ||
|
|
1fc15cde81 | ||
|
|
c711be4d60 | ||
|
|
ccb9dda164 | ||
|
|
100bc45185 | ||
|
|
222d1c8990 | ||
|
|
69c414fabf | ||
|
|
b33aa41b9a | ||
|
|
68a36dfd18 | ||
|
|
84a958af6e | ||
|
|
7db645d388 | ||
|
|
30e61eacf3 | ||
|
|
de496ed9e8 | ||
|
|
8cb35d9aac | ||
|
|
d662b03370 | ||
|
|
ef78ca7db3 | ||
|
|
c97c5842ad | ||
|
|
abce6196de | ||
|
|
e7d3be9854 | ||
|
|
e9dc8c775d | ||
|
|
31a2d68285 | ||
|
|
bf1ad185b9 | ||
|
|
62de35541c | ||
|
|
1a61435868 | ||
|
|
03ba28d0cb | ||
|
|
2b260bfdaf | ||
|
|
30ab42c0b2 | ||
|
|
f603026d5e | ||
|
|
84e6b3a9a8 | ||
|
|
3aafc0724f | ||
|
|
b978ce11fc |
+40
-7
@@ -12,6 +12,7 @@ from typing import Any, Generic, Literal, TypeVar, get_origin
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, ValidationError, root_validator
|
from pydantic import BaseModel, ValidationError, root_validator
|
||||||
|
from sqlalchemy import event
|
||||||
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
|
||||||
from sqlalchemy.sql import text
|
from sqlalchemy.sql import text
|
||||||
|
|
||||||
@@ -55,6 +56,14 @@ def compat_timestamp_placeholder(key: str):
|
|||||||
return f":{key}"
|
return f":{key}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_placeholder(model: Any, field: str) -> str:
|
||||||
|
type_ = model.__fields__[field].type_
|
||||||
|
if type_ == datetime:
|
||||||
|
return compat_timestamp_placeholder(field)
|
||||||
|
else:
|
||||||
|
return f":{field}"
|
||||||
|
|
||||||
|
|
||||||
class Compat:
|
class Compat:
|
||||||
type: str | None = "<inherited>"
|
type: str | None = "<inherited>"
|
||||||
schema: str | None = "<inherited>"
|
schema: str | None = "<inherited>"
|
||||||
@@ -317,7 +326,31 @@ class Database(Compat):
|
|||||||
self.engine: AsyncEngine = create_async_engine(
|
self.engine: AsyncEngine = create_async_engine(
|
||||||
database_uri, echo=settings.debug_database
|
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_date(value) -> datetime:
|
||||||
|
if value is None:
|
||||||
|
value = "1970-01-01 00:00:00"
|
||||||
|
f = "%Y-%m-%d %H:%M:%S.%f"
|
||||||
|
if "." not in value:
|
||||||
|
f = "%Y-%m-%d %H:%M:%S"
|
||||||
|
# Parse and add UTC timezone info
|
||||||
|
return datetime.strptime(value, f).replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
dbapi_connection.run_async(
|
||||||
|
lambda connection: connection.set_type_codec(
|
||||||
|
"TIMESTAMP",
|
||||||
|
encoder=datetime,
|
||||||
|
decoder=_parse_date,
|
||||||
|
schema="pg_catalog",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self.lock = asyncio.Lock()
|
self.lock = asyncio.Lock()
|
||||||
|
|
||||||
logger.trace(f"database {self.type} added for {self.name}")
|
logger.trace(f"database {self.type} added for {self.name}")
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -630,7 +663,7 @@ def insert_query(table_name: str, model: BaseModel) -> str:
|
|||||||
placeholders = []
|
placeholders = []
|
||||||
keys = model_to_dict(model).keys()
|
keys = model_to_dict(model).keys()
|
||||||
for field in keys:
|
for field in keys:
|
||||||
placeholders.append(f":{field}")
|
placeholders.append(get_placeholder(model, field))
|
||||||
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
||||||
fields = ", ".join([f'"{key}"' for key in keys])
|
fields = ", ".join([f'"{key}"' for key in keys])
|
||||||
values = ", ".join(placeholders)
|
values = ", ".join(placeholders)
|
||||||
@@ -648,8 +681,9 @@ def update_query(
|
|||||||
"""
|
"""
|
||||||
fields = []
|
fields = []
|
||||||
for field in model_to_dict(model).keys():
|
for field in model_to_dict(model).keys():
|
||||||
|
placeholder = get_placeholder(model, field)
|
||||||
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
||||||
fields.append(f'"{field}" = :{field}')
|
fields.append(f'"{field}" = {placeholder}')
|
||||||
query = ", ".join(fields)
|
query = ", ".join(fields)
|
||||||
return f"UPDATE {table_name} SET {query} {where}" # noqa: S608
|
return f"UPDATE {table_name} SET {query} {where}" # noqa: S608
|
||||||
|
|
||||||
@@ -667,12 +701,7 @@ def model_to_dict(model: BaseModel) -> dict:
|
|||||||
if model.__fields__[key].field_info.extra.get("no_database", False):
|
if model.__fields__[key].field_info.extra.get("no_database", False):
|
||||||
continue
|
continue
|
||||||
if isinstance(value, datetime):
|
if isinstance(value, datetime):
|
||||||
if DB_TYPE == SQLITE:
|
|
||||||
_dict[key] = value.timestamp()
|
_dict[key] = value.timestamp()
|
||||||
else:
|
|
||||||
# remove tz. postgres and cockroach TIMESTAMP is not tz aware
|
|
||||||
# so it will throw if we dont remove the UTC.
|
|
||||||
_dict[key] = value.replace(tzinfo=None)
|
|
||||||
continue
|
continue
|
||||||
if (
|
if (
|
||||||
type(type_) is type(BaseModel)
|
type(type_) is type(BaseModel)
|
||||||
@@ -728,7 +757,11 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel: # noqa: C901
|
|||||||
if DB_TYPE == SQLITE:
|
if DB_TYPE == SQLITE:
|
||||||
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
|
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
|
||||||
else:
|
else:
|
||||||
|
# Ensure PostgreSQL datetime values have timezone info
|
||||||
|
if isinstance(value, datetime) and value.tzinfo is None:
|
||||||
_dict[key] = value.replace(tzinfo=timezone.utc)
|
_dict[key] = value.replace(tzinfo=timezone.utc)
|
||||||
|
else:
|
||||||
|
_dict[key] = value
|
||||||
continue
|
continue
|
||||||
if issubclass(type_, BaseModel):
|
if issubclass(type_, BaseModel):
|
||||||
_dict[key] = dict_to_submodel(type_, value)
|
_dict[key] = dict_to_submodel(type_, value)
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -71,10 +71,6 @@ window.app.component('lnbits-wallet-extra', {
|
|||||||
'lnbits.exchangeRate.' + this.g.wallet.currency,
|
'lnbits.exchangeRate.' + this.g.wallet.currency,
|
||||||
this.g.exchangeRate
|
this.g.exchangeRate
|
||||||
)
|
)
|
||||||
if (this.g.exchangeRate <= 0) {
|
|
||||||
this.g.fiatTracking = false
|
|
||||||
this.g.isFiatPriority = false
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(e => console.error(e))
|
.catch(e => console.error(e))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ window._lnbitsUtils = {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async notifyApiError(error) {
|
notifyApiError(error) {
|
||||||
if (!error.response) {
|
if (!error.response) {
|
||||||
return console.error(error)
|
return console.error(error)
|
||||||
}
|
}
|
||||||
@@ -154,10 +154,6 @@ window._lnbitsUtils = {
|
|||||||
500: 'negative'
|
500: 'negative'
|
||||||
}
|
}
|
||||||
let messages = error.response.data.detail
|
let messages = error.response.data.detail
|
||||||
if (!messages) {
|
|
||||||
const text = await error.response.data?.text()
|
|
||||||
messages = this.parseJSONSafe(text)?.detail
|
|
||||||
}
|
|
||||||
if (messages) {
|
if (messages) {
|
||||||
messages = Array.isArray(messages)
|
messages = Array.isArray(messages)
|
||||||
? messages.map(e => e.msg + ` (${e.loc?.join('/')})`)
|
? messages.map(e => e.msg + ` (${e.loc?.join('/')})`)
|
||||||
|
|||||||
@@ -178,7 +178,7 @@
|
|||||||
"
|
"
|
||||||
:clickable="!!reviewsUrl"
|
:clickable="!!reviewsUrl"
|
||||||
@click="openReviews(extension)"
|
@click="openReviews(extension)"
|
||||||
></lnbits-extension-rating>
|
/>
|
||||||
<q-btn-group size="xs" style="margin: 5px 0">
|
<q-btn-group size="xs" style="margin: 5px 0">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="extension.hasFreeRelease"
|
v-if="extension.hasFreeRelease"
|
||||||
|
|||||||
@@ -292,8 +292,7 @@ async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
|||||||
async def btc_price(currency: str) -> float:
|
async def btc_price(currency: str) -> float:
|
||||||
rates = await btc_rates(currency)
|
rates = await btc_rates(currency)
|
||||||
if not rates:
|
if not rates:
|
||||||
logger.warning("Could not fetch any Bitcoin price.")
|
raise ValueError("Could not fetch any Bitcoin price.")
|
||||||
return 0.0
|
|
||||||
elif len(rates) == 1:
|
elif len(rates) == 1:
|
||||||
logger.warning("Could only fetch one Bitcoin price.")
|
logger.warning("Could only fetch one Bitcoin price.")
|
||||||
|
|
||||||
@@ -307,8 +306,7 @@ async def get_fiat_rate_and_price_satoshis(currency: str) -> tuple[float, float]
|
|||||||
f"btc-price-{currency}",
|
f"btc-price-{currency}",
|
||||||
settings.lnbits_exchange_rate_cache_seconds,
|
settings.lnbits_exchange_rate_cache_seconds,
|
||||||
)
|
)
|
||||||
rate = float(100_000_000 / price) if price > 0 else 0.0
|
return float(100_000_000 / price), price
|
||||||
return rate, price
|
|
||||||
|
|
||||||
|
|
||||||
async def get_fiat_rate_satoshis(currency: str) -> float:
|
async def get_fiat_rate_satoshis(currency: str) -> float:
|
||||||
@@ -318,13 +316,9 @@ async def get_fiat_rate_satoshis(currency: str) -> float:
|
|||||||
|
|
||||||
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
|
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
|
||||||
rate = await get_fiat_rate_satoshis(currency)
|
rate = await get_fiat_rate_satoshis(currency)
|
||||||
if rate > 0:
|
return int(amount * (rate))
|
||||||
return int(amount * rate)
|
|
||||||
raise ValueError(f"Could not get exchange rate for {currency}.")
|
|
||||||
|
|
||||||
|
|
||||||
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
|
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
|
||||||
rate = await get_fiat_rate_satoshis(currency)
|
rate = await get_fiat_rate_satoshis(currency)
|
||||||
if rate > 0:
|
|
||||||
return float(amount / rate)
|
return float(amount / rate)
|
||||||
raise ValueError(f"Could not get exchange rate for {currency}.")
|
|
||||||
|
|||||||
Reference in New Issue
Block a user