From e2a748e94e80bd39582ab79e2d418c40ddac26cb Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 20 Oct 2026 11:21:19 +0300 Subject: [PATCH] Migrate to Pydantic 2 for LNbits 1.7.0 --- config.json | 2 +- crud.py | 2 +- models.py | 37 +++++++++++++++++++------------------ tests/test_api.py | 16 +++++++++------- tests/test_tabs.py | 4 ++-- views.py | 6 +++--- views_api.py | 4 ++-- views_atm.py | 4 ++-- views_lnurl.py | 6 +++--- views_payments.py | 12 +++++++----- views_tabs.py | 6 +++++- views_wrapper.py | 2 +- 12 files changed, 55 insertions(+), 46 deletions(-) diff --git a/config.json b/config.json index 0d2a7864..c4b158d0 100644 --- a/config.json +++ b/config.json @@ -6,7 +6,7 @@ "short_description": "A shareable PoS terminal!", "description": "", "tile": "/tpos/static/image/tpos.png", - "min_lnbits_version": "1.5.0", + "min_lnbits_version": "1.7.0", "contributors": [ { "name": "Ben Arc", diff --git a/crud.py b/crud.py index 3f3d69ea..97740880 100644 --- a/crud.py +++ b/crud.py @@ -60,7 +60,7 @@ async def set_wrapper_assetlinks_cache( async def create_tpos(data: CreateTposData) -> Tpos: tpos_id = urlsafe_short_hash() - data_dict = data.dict() + data_dict = data.model_dump() data_dict["inventory_tags"] = serialize_inventory_tags(data.inventory_tags) data_dict["inventory_omit_tags"] = serialize_inventory_tags( data.inventory_omit_tags diff --git a/models.py b/models.py index 59f2fed2..3c2b6253 100644 --- a/models.py +++ b/models.py @@ -6,7 +6,7 @@ from typing import Any, Literal from fastapi import Query -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, ConfigDict, Field, field_validator class PayLnurlWData(BaseModel): @@ -20,8 +20,7 @@ class CreateWithdrawPay(BaseModel): class AuthorizeAtm(BaseModel): password: str = Field(..., min_length=1, max_length=256) - class Config: - extra = "forbid" + model_config = ConfigDict(extra="forbid") class CreateTposInvoiceTabSettlement(BaseModel): @@ -62,15 +61,15 @@ class InventorySale(BaseModel): class CreateTposData(BaseModel): - wallet: str | None - name: str | None - currency: str | None + wallet: str | None = None + name: str | None = None + currency: str | None = None use_inventory: bool = Field(False) inventory_id: str | None = None inventory_tags: list[str] | None = None inventory_omit_tags: list[str] | None = None tax_inclusive: bool = Field(True) - tax_default: float | None = Field(0.0) + tax_default: float | None = Field(0.0, validate_default=True) tip_options: str = Field("[]") tip_wallet: str = Field("") withdraw_time: int = Field(0) @@ -82,9 +81,9 @@ class CreateTposData(BaseModel): lnaddress_cut: int | None = Field(0) enable_receipt_print: bool = Query(False) enable_remote: bool = Query(False) - business_name: str | None - business_address: str | None - business_vat_id: str | None + business_name: str | None = None + business_address: str | None = None + business_vat_id: str | None = None only_show_sats_on_bitcoin: bool = Query(True) allow_price_adjustment: bool = Field(True) fiat_provider: str | None = Field(None) @@ -97,7 +96,8 @@ class CreateTposData(BaseModel): tabs_enabled: bool = Field(False) tabs_allow_create: bool = Field(False) - @validator("tax_default", pre=True, always=True) + @field_validator("tax_default", mode="before") + @classmethod def default_tax_when_none(cls, v): return 0.0 if v is None else v @@ -236,7 +236,7 @@ class CreateTposTabData(BaseModel): class CreateTposTabCharge(BaseModel): amount: float = Field(..., gt=0) description: str | None = Field(None, max_length=512) - items: list[dict[str, Any]] = Field(default_factory=list, max_items=200) + items: list[dict[str, Any]] = Field(default_factory=list, max_length=200) notes: dict[str, Any] | None = None internal_memo: str | None = Field(None, max_length=512) idempotency_key: str = Field(..., min_length=8, max_length=128) @@ -250,15 +250,16 @@ class LnurlCharge(BaseModel): class Item(BaseModel): - image: str | None + image: str | None = None price: float title: str - description: str | None - tax: float | None = Field(0, ge=0.0) + description: str | None = None + tax: float | None = Field(0, ge=0.0, validate_default=True) disabled: bool = False categories: list[str] | None = [] - @validator("tax", pre=True, always=True) + @field_validator("tax", mode="before") + @classmethod def set_default_tax(cls, v): return v or 0 @@ -405,7 +406,7 @@ def render_text( return "\n".join(lines) def to_api_dict(self) -> dict[str, Any]: - data = self.dict() + data = self.model_dump() details = data.get("extra", {}).get("details", {}) details["exchangeRate"] = details.pop("exchange_rate", 1.0) details["taxValue"] = details.pop("tax_value", 0.0) @@ -425,4 +426,4 @@ class ReceiptPrint(BaseModel): receipt: dict[str, Any] = Field(default_factory=dict) -CreateTposInvoice.update_forward_refs(InventorySale=InventorySale) +CreateTposInvoice.model_rebuild() diff --git a/tests/test_api.py b/tests/test_api.py index 1a9795f9..d217c20e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -18,7 +18,7 @@ ) from lnbits.core.services.users import create_user_account_no_ckeck from lnbits.settings import settings -from lnbits.tasks import internal_invoice_queue +from lnbits.task_manager import task_manager from tabs.crud import ( # type: ignore[import] get_tab_by_id, get_tab_entries, @@ -76,7 +76,7 @@ async def _user_with_tabs(username: str = "tposuser"): async def _drain_internal_invoice_queue() -> None: while True: try: - internal_invoice_queue.get_nowait() + task_manager.internal_invoice_queue.get_nowait() except asyncio.QueueEmpty: return @@ -1273,11 +1273,13 @@ async def test_atm_pay_endpoint(client: AsyncClient, monkeypatch): charge = charge_response.json() async def fake_lnurl_handle(pay_link, user_agent=None): - return views_atm.LnurlPayResponse( - callback="https://example.com/cb", - minSendable=1000, - maxSendable=1000, - metadata='[["text/plain","test"]]', + return views_atm.LnurlPayResponse.model_validate( + { + "callback": "https://example.com/cb", + "minSendable": 1000, + "maxSendable": 1000, + "metadata": '[["text/plain","test"]]', + } ) class FakePayResponse: diff --git a/tests/test_tabs.py b/tests/test_tabs.py index f5b83edb..fdcfe0a0 100644 --- a/tests/test_tabs.py +++ b/tests/test_tabs.py @@ -5,7 +5,7 @@ from lnbits.core.crud import get_standalone_payment from lnbits.core.services import pay_invoice, update_wallet_balance from lnbits.core.services.users import create_user_account_no_ckeck -from lnbits.tasks import internal_invoice_queue +from lnbits.task_manager import task_manager from tpos.crud import get_tpos_payment_by_hash # type: ignore[import] from tpos.tasks import on_invoice_paid # type: ignore[import] @@ -14,7 +14,7 @@ async def _drain_internal_invoice_queue() -> None: while True: try: - internal_invoice_queue.get_nowait() + task_manager.internal_invoice_queue.get_nowait() except asyncio.QueueEmpty: return diff --git a/views.py b/views.py index ff5c755b..0aae8d6d 100644 --- a/views.py +++ b/views.py @@ -20,7 +20,7 @@ def tpos_renderer(): @tpos_generic_router.get("/", response_class=HTMLResponse) async def index(request: Request, user: User = Depends(check_user_exists)): return tpos_renderer().TemplateResponse( - "tpos/index.html", {"request": request, "user": user.json()} + "tpos/index.html", {"request": request, "user": user.model_dump_json()} ) @@ -32,12 +32,12 @@ async def tpos(request: Request, tpos_id, lnaddress: str | None = ""): status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist." ) - tpos_clean = TposClean(**tpos.dict()) + tpos_clean = TposClean(**tpos.model_dump()) response = tpos_renderer().TemplateResponse( "tpos/tpos.html", { "request": request, - "tpos": tpos_clean.json(), + "tpos": tpos_clean.model_dump_json(), "lnaddressparam": lnaddress, "withdraw_maximum": tpos.withdraw_maximum, "web_manifest": f"/tpos/manifest/{tpos_id}.webmanifest", diff --git a/views_api.py b/views_api.py index c8334d48..f3b1ba09 100644 --- a/views_api.py +++ b/views_api.py @@ -106,7 +106,7 @@ async def api_tpos_update( if wallet.wallet.id != tpos.wallet: raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your TPoS.") user = await get_user(wallet.wallet.user) - update_payload = data.dict(exclude_unset=True) + update_payload = data.model_dump(exclude_unset=True) update_payload.pop("wallet", None) desired_onchain_enabled = update_payload.get( "onchain_enabled", tpos.onchain_enabled @@ -191,7 +191,7 @@ async def api_tpos_create_items( if wallet.wallet.id != tpos.wallet: raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your TPoS.") - tpos.items = json.dumps(data.dict()["items"]) + tpos.items = json.dumps(data.model_dump()["items"]) tpos = await update_tpos(tpos) return tpos diff --git a/views_atm.py b/views_atm.py index 2c3cfa60..bdd25ad6 100644 --- a/views_atm.py +++ b/views_atm.py @@ -20,7 +20,7 @@ ) from lnurl import handle as lnurl_handle from loguru import logger -from pydantic import parse_obj_as +from pydantic import TypeAdapter from .crud import ( create_lnurlcharge, @@ -167,7 +167,7 @@ async def api_tpos_atm_pay( callback_url = str(request.url_for("tpos.tposlnurlcharge.callback")) withdraw_res = LnurlWithdrawResponse( k1=charge_id, - callback=parse_obj_as(CallbackUrl, callback_url), + callback=TypeAdapter(CallbackUrl).validate_python(callback_url), maxWithdrawable=MilliSatoshi(amount * 1000), minWithdrawable=MilliSatoshi(amount * 1000), ) diff --git a/views_lnurl.py b/views_lnurl.py index 4cfc3003..19de5d5c 100644 --- a/views_lnurl.py +++ b/views_lnurl.py @@ -10,7 +10,7 @@ MilliSatoshi, ) from loguru import logger -from pydantic import parse_obj_as +from pydantic import TypeAdapter from .crud import get_lnurlcharge, get_tpos, update_lnurlcharge, update_tpos @@ -70,8 +70,8 @@ async def lnurl_params( ) logger.debug(f"Amount to withdraw: {amount}") - callback = parse_obj_as( - CallbackUrl, str(request.url_for("tpos.tposlnurlcharge.callback")) + callback = TypeAdapter(CallbackUrl).validate_python( + str(request.url_for("tpos.tposlnurlcharge.callback")) ) return LnurlWithdrawResponse( callback=callback, diff --git a/views_payments.py b/views_payments.py index ef7afd91..0ef3fc9b 100644 --- a/views_payments.py +++ b/views_payments.py @@ -149,7 +149,7 @@ async def api_tpos_create_invoice( "base_url": str(request.base_url), } if tab_settlement: - extra["tab_settlement"] = tab_settlement.dict() + extra["tab_settlement"] = tab_settlement.model_dump() if cash_method or onchain_method: wallet = await get_wallet(tpos.wallet) if wallet: @@ -176,7 +176,7 @@ async def api_tpos_create_invoice( ) await update_account(account) if inventory_payload: - extra["inventory"] = inventory_payload.dict() + extra["inventory"] = inventory_payload.model_dump() if data.pay_in_fiat: extra["fiat_method"] = data.fiat_method if data.fiat_method else "checkout" if data.fiat_method == "terminal" and tpos.stripe_reader_id: @@ -272,8 +272,10 @@ async def api_tpos_create_invoice( tpos_id=tpos_id, payment_hash=payment.payment_hash, ) - await websocket_updater(tpos_id, json.dumps(tap_to_pay_payload.dict())) - return response_payload.dict() + await websocket_updater( + tpos_id, json.dumps(tap_to_pay_payload.model_dump()) + ) + return response_payload.model_dump() except Exception as exc: raise HTTPException( @@ -451,7 +453,7 @@ async def api_tpos_print_invoice( print_text=receipt.render_text(receipt_type), receipt=receipt.to_api_dict(), ) - await websocket_updater(tpos_id, json.dumps(payload.dict())) + await websocket_updater(tpos_id, json.dumps(payload.model_dump())) return {"success": True} diff --git a/views_tabs.py b/views_tabs.py index afe61f5a..3dd2747d 100644 --- a/views_tabs.py +++ b/views_tabs.py @@ -118,4 +118,8 @@ async def api_tpos_add_tab_charge( payload=payload, ) updated_tab = await get_tab_for_tpos(user_id, tpos, tab_id) - return {"tab_id": tab_id, "entry": entry, "tab": TposTab(**updated_tab).dict()} + return { + "tab_id": tab_id, + "entry": entry, + "tab": TposTab(**updated_tab).model_dump(), + } diff --git a/views_wrapper.py b/views_wrapper.py index f345fb44..6bcc1250 100644 --- a/views_wrapper.py +++ b/views_wrapper.py @@ -115,7 +115,7 @@ async def api_tpos_create_wrapper_token( sub=account.username, api_token_id=api_token_id, auth_time=int(time()) ) api_token = create_access_token( - data=payload.dict(), token_expire_minutes=token_expire_minutes + data=payload.model_dump(), token_expire_minutes=token_expire_minutes ) acl.token_id_list.append(