Compare commits

..
Author SHA1 Message Date
Vlad Stan 613f88faf2 test: codex testing 2026-03-30 09:56:42 +03:00
Vlad Stan 6a2ade1695 fix: ui 2026-03-27 16:57:33 +02:00
Vlad Stan 8211f95bf1 feat: ui 2026-03-27 16:54:52 +02:00
Vlad Stan 3857402471 feat: ui 2026-03-27 16:35:57 +02:00
Vlad Stan 0f0e756c37 feat: ui 2026-03-27 16:32:44 +02:00
Vlad Stan 2e82b255e7 feat: ui 2026-03-27 15:22:57 +02:00
Vlad Stan dda3685a81 feat: ui 2026-03-27 15:17:25 +02:00
Vlad Stan 301c588112 feat: better fields 2026-03-27 14:14:26 +02:00
Vlad Stan 3b47db1b35 feat: UI 2026-03-27 14:07:08 +02:00
Vlad Stan a2e5d1507c feat: better ui 2026-03-27 13:54:44 +02:00
Vlad Stan 9f9057cca6 feat: ui 2026-03-27 12:52:49 +02:00
Vlad Stan 3d919aa1a8 feat: UI changs 2026-03-27 12:33:19 +02:00
Vlad Stan 1bedc428a5 feat: ui 2026-03-27 12:17:19 +02:00
Vlad Stan 43abd4ed82 feat: ui 2026-03-27 12:14:02 +02:00
Vlad Stan 742efd7dd9 fat: ui 2026-03-27 12:10:51 +02:00
Vlad Stan 7446dfc29c feat: ui settings 2026-03-27 11:57:40 +02:00
Vlad Stan 396fe72a3f feat: ui 2026-03-27 11:48:15 +02:00
Vlad Stan 77f112cd5e feat: ui 2026-03-27 11:43:31 +02:00
Vlad Stan 0003574d21 feat: nicer mocks 2026-03-26 19:43:51 +02:00
Vlad Stan 22e3ec3672 feat: some changes 2026-03-26 19:30:04 +02:00
Vlad Stan 2e77dcf393 feat: ui 2026-03-26 19:22:15 +02:00
Vlad Stan 0b8b530ca6 feat: nicer UI 2026-03-26 19:13:06 +02:00
Vlad Stan 193217d157 feat: ok 2026-03-26 15:29:03 +02:00
Vlad Stan 33fef98d9e feat: ok 2026-03-26 15:09:40 +02:00
Vlad Stan 8649129156 feat: something 2026-03-26 14:51:35 +02:00
Vlad Stan 427894d106 feat: nicer dropdown 2026-03-26 14:39:00 +02:00
Vlad Stan d5cde46ed6 feat: blaaa 2026-03-26 14:31:30 +02:00
Vlad Stan cc0ea50be0 feat: better 2026-03-26 14:10:10 +02:00
Vlad Stan 633a0d2d2d feat: better UI 2026-03-26 13:29:15 +02:00
Vlad Stan 21c007d2b3 feat: initial commit 2026-03-26 12:48:28 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b2d6243697 chore(deps): bump requests from 2.32.5 to 2.33.0 (#3903)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-26 11:07:50 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6b5a77fb3b chore(deps): bump picomatch from 2.3.1 to 2.3.2 (#3901)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-26 09:58:34 +02:00
dni ⚡andGitHub cc8fb68b02 ci: only run ci on pull request (#3902) 2026-03-26 09:37:36 +02:00
Vlad StanandGitHub 658da6b28e feat: optimize QR code value for bach32 (#3900) 2026-03-25 16:22:03 +02:00
Vlad StanandGitHub 7d734ecb74 fix: use --offline for uv (#3896) 2026-03-25 13:10:47 +02:00
dni ⚡andGitHub 9177dd195b chore: update to version 1.5.3 (#3899) 2026-03-25 12:08:39 +01:00
15faab4f38 [feat] reset the first install token (#3894) (#3898)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-03-25 11:57:37 +01:00
44 changed files with 6803 additions and 918 deletions
-4
View File
@@ -1,9 +1,5 @@
name: LNbits CI
on:
push:
branches:
- main
- dev
pull_request:
+1 -1
View File
@@ -43,4 +43,4 @@ ENV LNBITS_HOST="0.0.0.0"
EXPOSE 5000
CMD ["sh", "-c", "uv run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
CMD ["sh", "-c", "uv --offline run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
+1 -3
View File
@@ -711,9 +711,7 @@ async def _call_install_extension(
user_id = user_id or get_super_user()
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{url}/api/v1/extension?usr={user_id}",
json=data.model_dump(),
timeout=40,
f"{url}/api/v1/extension?usr={user_id}", json=data.dict(), timeout=40
)
resp.raise_for_status()
else:
+3 -2
View File
@@ -44,7 +44,7 @@ async def update_admin_settings(
data: EditableSettings, tag: str | None = "core"
) -> None:
editable_settings = await get_settings_by_tag("core") or {}
editable_settings.update(data.model_dump(exclude_unset=True))
editable_settings.update(data.dict(exclude_unset=True))
for key, value in editable_settings.items():
try:
await set_settings_field(key, value, tag)
@@ -105,7 +105,8 @@ async def get_settings_field(
)
if not row:
return None
return SettingsField(id=row["id"], value=json.loads(row["value"]), tag=row["tag"])
value = json.loads(row["value"]) if row["value"] else None
return SettingsField(id=row["id"], value=value, tag=row["tag"])
async def set_settings_field(id_: str, value: Any | None, tag: str | None = "core"):
+2 -2
View File
@@ -1,7 +1,6 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from typing import Any
from pydantic import BaseModel, Field
@@ -22,7 +21,8 @@ class AuditEntry(BaseModel):
delete_at: datetime | None = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
def model_post_init(self, __context: Any) -> None:
def __init__(self, **data):
super().__init__(**data)
retention_days = max(0, settings.lnbits_audit_retention_days) or 365
self.delete_at = self.created_at + timedelta(days=retention_days)
+6 -6
View File
@@ -96,7 +96,7 @@ class ExtensionConfig(BaseModel):
)
error_msg = "Cannot fetch GitHub extension config"
config = await github_api_get(config_url, error_msg)
return ExtensionConfig.model_validate(config)
return ExtensionConfig.parse_obj(config)
class ReleasePaymentInfo(BaseModel):
@@ -304,7 +304,7 @@ class ExtensionRelease(BaseModel):
releases_url = f"https://api.github.com/repos/{org}/{repo}/releases"
error_msg = "Cannot fetch extension releases"
releases = await github_api_get(releases_url, error_msg)
return [GitHubRepoRelease.model_validate(r) for r in releases]
return [GitHubRepoRelease.parse_obj(r) for r in releases]
@classmethod
async def fetch_release_details(cls, details_link: str) -> dict | None:
@@ -757,7 +757,7 @@ class InstallableExtension(BaseModel):
repo_url = f"https://api.github.com/repos/{org}/{repository}"
error_msg = "Cannot fetch extension repo"
repo = await github_api_get(repo_url, error_msg)
github_repo = GitHubRepo.model_validate(repo)
github_repo = GitHubRepo.parse_obj(repo)
lates_release_url = (
f"https://api.github.com/repos/{org}/{repository}/releases/latest"
@@ -771,15 +771,15 @@ class InstallableExtension(BaseModel):
return (
github_repo,
GitHubRepoRelease.model_validate(latest_release),
ExtensionConfig.model_validate(config),
GitHubRepoRelease.parse_obj(latest_release),
ExtensionConfig.parse_obj(config),
)
@classmethod
async def fetch_manifest(cls, url) -> Manifest:
error_msg = "Cannot fetch extensions manifest"
manifest = await github_api_get(url, error_msg)
return Manifest.model_validate(manifest)
return Manifest.parse_obj(manifest)
class CreateExtension(BaseModel):
+21 -31
View File
@@ -6,7 +6,7 @@ import uuid
from datetime import datetime, timedelta, timezone
from typing import Any, Literal
from pydantic import BaseModel, field_validator
from pydantic import BaseModel, validator
from lnbits.helpers import (
camel_to_snake,
@@ -141,8 +141,7 @@ class DataField(BaseModel):
else:
return f"{self.name} {index}"
@field_validator("name")
@classmethod
@validator("name")
def validate_name(cls, v: str) -> str:
if v.strip() == "":
raise ValueError("Field name is required.")
@@ -150,8 +149,7 @@ class DataField(BaseModel):
raise ValueError(f"Field Name must be snake_case. Found: {v}")
return v
@field_validator("type")
@classmethod
@validator("type")
def validate_type(cls, v: str) -> str:
if v.strip() == "":
raise ValueError("Owner Data type is required")
@@ -173,8 +171,7 @@ class DataField(BaseModel):
)
return v
@field_validator("label")
@classmethod
@validator("label")
def validate_label(cls, v: str | None) -> str | None:
if v and '"' in v:
raise ValueError(
@@ -182,8 +179,7 @@ class DataField(BaseModel):
)
return v
@field_validator("hint")
@classmethod
@validator("hint")
def validate_hint(cls, v: str | None) -> str | None:
if v and '"' in v:
raise ValueError(f'Field hint cannot contain double quotes ("). Value: {v}')
@@ -195,7 +191,8 @@ class DataFields(BaseModel):
editable: bool = True
fields: list[DataField] = []
def model_post_init(self, __context: Any) -> None:
def __init__(self, **data):
super().__init__(**data)
self.normalize()
def normalize(self) -> None:
@@ -213,8 +210,7 @@ class DataFields(BaseModel):
return field
return None
@field_validator("name")
@classmethod
@validator("name")
def validate_name(cls, v: str) -> str:
if v.strip() == "":
raise ValueError("Data fields name is required")
@@ -227,13 +223,12 @@ class SettingsFields(DataFields):
enabled: bool = False
type: str = "user"
@field_validator("type")
@classmethod
@validator("type")
def validate_type(cls, v: str) -> str:
if v.strip() == "":
raise ValueError("Settings type is required")
if v not in ["user", "admin"]:
raise ValueError(f"Field Type must be one of: user, admin. Found: {v}")
raise ValueError("Field Type must be one of: user, admin." f" Found: {v}")
return v
@@ -270,7 +265,8 @@ class PreviewAction(BaseModel):
is_client_data_preview: bool = False
is_public_page_preview: bool = False
def model_post_init(self, __context: Any) -> None:
def __init__(self, **data):
super().__init__(**data)
if not self.is_preview_mode:
self.is_settings_preview = False
self.is_owner_data_preview = False
@@ -290,7 +286,8 @@ class ExtensionData(BaseModel):
public_page: PublicPageFields
preview_action: PreviewAction = PreviewAction()
def model_post_init(self, __context: Any) -> None:
def __init__(self, **data):
super().__init__(**data)
self.validate_data()
self.normalize()
@@ -437,8 +434,7 @@ class ExtensionData(BaseModel):
f" Received: {paid_flag_field.type}."
)
@field_validator("id")
@classmethod
@validator("id")
def validate_id(cls, v: str) -> str:
if v.strip() == "":
raise ValueError("Extension ID is required")
@@ -446,15 +442,13 @@ class ExtensionData(BaseModel):
raise ValueError(f"Extension Id must be snake_case. Found: {v}")
return v
@field_validator("name")
@classmethod
@validator("name")
def validate_name(cls, v: str) -> str:
if v.strip() == "":
raise ValueError("Extension name is required")
return v
@field_validator("stub_version")
@classmethod
@validator("stub_version")
def validate_stub_version(cls, v: str | None) -> str | None:
if v and '"' in v:
raise ValueError(
@@ -462,8 +456,7 @@ class ExtensionData(BaseModel):
)
return v
@field_validator("short_description")
@classmethod
@validator("short_description")
def validate_short_description(cls, v: str | None) -> str | None:
if v and '"' in v:
raise ValueError(
@@ -471,8 +464,7 @@ class ExtensionData(BaseModel):
)
return v
@field_validator("description")
@classmethod
@validator("description")
def validate_description(cls, v: str | None) -> str | None:
if v and '"' in v:
raise ValueError(
@@ -480,15 +472,13 @@ class ExtensionData(BaseModel):
)
return v
@field_validator("owner_data")
@classmethod
@validator("owner_data")
def validate_owner_data(cls, v: DataFields) -> DataFields:
if len(v.fields) == 0:
raise ValueError("At least one owner data field is required")
return v
@field_validator("client_data")
@classmethod
@validator("client_data")
def validate_client_data(cls, v: DataFields) -> DataFields:
if len(v.fields) == 0:
raise ValueError("At least one client data field is required")
+2 -2
View File
@@ -6,7 +6,7 @@ from pydantic import BaseModel, Field
class CreateLnurlPayment(BaseModel):
res: LnurlPayResponse | None = None
lnurl: Lnurl | None = None
lnurl: Lnurl | LnAddress | None = None
amount: int
comment: str | None = None
unit: str | None = None
@@ -14,7 +14,7 @@ class CreateLnurlPayment(BaseModel):
class CreateLnurlWithdraw(BaseModel):
lnurl_w: Lnurl | LnAddress
lnurl_w: Lnurl
class LnurlScan(BaseModel):
+9 -14
View File
@@ -2,12 +2,12 @@ from __future__ import annotations
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Literal
from typing import Literal
from fastapi import Query
from lnurl import LnurlWithdrawResponse
from loguru import logger
from pydantic import BaseModel, Field, field_validator
from pydantic import BaseModel, Field, validator
from lnbits.db import FilterModel
from lnbits.fiat.base import (
@@ -62,10 +62,7 @@ class Payment(BaseModel):
amount: int
fee: int
bolt11: str
payment_request: str | None = Field(
default=None,
json_schema_extra={"no_database": True},
)
payment_request: str | None = Field(default=None, no_database=True)
fiat_provider: str | None = None
status: str = PaymentState.PENDING
memo: str | None = None
@@ -81,7 +78,8 @@ class Payment(BaseModel):
labels: list[str] = []
extra: dict = {}
def model_post_init(self, __context: Any) -> None:
def __init__(self, **data):
super().__init__(**data)
if "fiat_payment_request" in self.extra:
self.payment_request = self.extra["fiat_payment_request"]
else:
@@ -252,14 +250,13 @@ class CreateInvoice(BaseModel):
fiat_provider: str | None = None
labels: list[str] = []
@field_validator("payment_hash")
@classmethod
@validator("payment_hash")
def check_hex(cls, v):
if v:
_ = bytes.fromhex(v)
return v
@field_validator("unit")
@validator("unit")
@classmethod
def unit_is_from_allowed_currencies(cls, v):
if v != "sat" and v not in allowed_currencies():
@@ -282,8 +279,7 @@ class SettleInvoice(BaseModel):
max_length=64,
)
@field_validator("preimage")
@classmethod
@validator("preimage")
def check_hex(cls, v):
_ = bytes.fromhex(v)
return v
@@ -297,8 +293,7 @@ class CancelInvoice(BaseModel):
max_length=64,
)
@field_validator("payment_hash")
@classmethod
@validator("payment_hash")
def check_hex(cls, v):
_ = bytes.fromhex(v)
return v
+9 -16
View File
@@ -1,7 +1,6 @@
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
from uuid import UUID
from bcrypt import checkpw, gensalt, hashpw
@@ -39,9 +38,11 @@ class WalletInviteRequest(BaseModel):
class UserLabel(BaseModel):
name: str = Field(pattern=r"([A-Za-z0-9 ._-]{1,100}$)")
name: str = Field(regex=r"([A-Za-z0-9 ._-]{1,100}$)")
description: str | None = Field(default=None, max_length=250)
color: str | None = Field(default=None, pattern=r"^#[0-9A-Fa-f]{6}$")
color: str | None = Field(
default=None, regex=r"^#[0-9A-Fa-f]{6}$"
) # e.g., "#RRGGBB"
class UserExtra(BaseModel):
@@ -192,20 +193,12 @@ class Account(AccountId):
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
is_super_user: bool = Field(
default=False,
json_schema_extra={"no_database": True},
)
is_admin: bool = Field(
default=False,
json_schema_extra={"no_database": True},
)
fiat_providers: list[str] = Field(
default=[],
json_schema_extra={"no_database": True},
)
is_super_user: bool = Field(default=False, no_database=True)
is_admin: bool = Field(default=False, no_database=True)
fiat_providers: list[str] = Field(default=[], no_database=True)
def model_post_init(self, __context: Any) -> None:
def __init__(self, **data):
super().__init__(**data)
self.is_super_user = settings.is_super_user(self.id)
self.is_admin = settings.is_admin_user(self.id)
self.fiat_providers = settings.get_fiat_providers_for_user(self.id)
+5 -8
View File
@@ -3,7 +3,6 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum
from typing import Any
from pydantic import BaseModel, Field
@@ -127,16 +126,14 @@ class Wallet(BaseWallet):
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
currency: str | None = None
balance_msat: int = Field(default=0, json_schema_extra={"no_database": True})
balance_msat: int = Field(default=0, no_database=True)
extra: WalletExtra = WalletExtra()
stored_paylinks: StoredPayLinks = Field(default_factory=StoredPayLinks)
stored_paylinks: StoredPayLinks = StoredPayLinks()
# What permission this wallet has when it's a shared wallet
share_permissions: list[WalletPermission] = Field(
default=[],
json_schema_extra={"no_database": True},
)
share_permissions: list[WalletPermission] = Field(default=[], no_database=True)
def model_post_init(self, __context: Any) -> None:
def __init__(self, **data):
super().__init__(**data)
self._validate_data()
def mirror_shared_wallet(
+3 -3
View File
@@ -97,7 +97,7 @@ def _transform_extension_builder_stub(data: ExtensionData, extension_dir: Path)
def _export_extension_data_json(data: ExtensionData, build_dir: Path):
json.dump(
data.model_dump(),
data.dict(),
open(Path(build_dir, "builder.json"), "w", encoding="utf-8"),
indent=4,
)
@@ -133,7 +133,7 @@ async def _get_extension_stub_release(
logger.debug(f"Save release cache {stub_ext_id} ({stub_version}).")
with open(release_cache_file, "w", encoding="utf-8") as f:
f.write(json.dumps(release.model_dump(), indent=4))
f.write(json.dumps(release.dict(), indent=4))
return release
@@ -290,7 +290,7 @@ def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None
{
"extension_builder_stub_public_client_inputs": public_client_inputs,
"preview": data.preview_action,
**data.public_page.action_fields.model_dump(),
**data.public_page.action_fields.dict(),
"cancel_comment": remove_line_marker,
},
)
+1 -2
View File
@@ -45,11 +45,10 @@ def dict_to_settings(sets_dict: dict) -> UpdateSettings:
def update_cached_settings(sets_dict: dict):
editable_settings = dict_to_settings(sets_dict)
settings_keys = settings.model_dump().keys()
for key in sets_dict.keys():
if key in readonly_variables:
continue
if key not in settings_keys:
if key not in settings.dict().keys():
continue
try:
value = getattr(editable_settings, key)
+9 -3
View File
@@ -163,7 +163,7 @@ async def check_admin_settings():
# .env super_user overwrites DB super_user
settings_db = await update_super_user(settings.super_user)
update_cached_settings(settings_db.model_dump())
update_cached_settings(settings_db.dict())
# saving superuser to {data_dir}/.super_user file
with open(Path(settings.lnbits_data_folder) / ".super_user", "w") as file:
@@ -173,6 +173,12 @@ async def check_admin_settings():
if account and account.extra and account.extra.provider == "env":
settings.first_install = True
if settings.has_first_install_token_changed():
logger.warning("First install token is changed. Resetting admin settings.")
new_settings = await init_admin_settings()
settings.super_user = new_settings.super_user
settings.first_install = True
logger.success(
"✔️ Admin UI is enabled. run `uv run lnbits-cli superuser` "
"to get the superuser."
@@ -192,8 +198,8 @@ async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
await create_account(account)
await create_wallet(user_id=account.id)
editable_settings = EditableSettings.from_dict(settings.model_dump())
return await create_admin_settings(account.id, editable_settings.model_dump())
editable_settings = EditableSettings.from_dict(settings.dict())
return await create_admin_settings(account.id, editable_settings.dict())
async def check_register_activation_settings(data: RegisterUser):
+2 -2
View File
@@ -87,7 +87,7 @@ async def api_update_settings(
admin_settings = await get_admin_settings(account.is_super_user)
if not admin_settings:
raise ValueError("Updated admin settings not found.")
update_cached_settings(admin_settings.model_dump())
update_cached_settings(admin_settings.dict())
core_app_extra.register_new_ratelimiter()
return {"status": "Success"}
@@ -99,7 +99,7 @@ async def api_update_settings(
async def api_update_settings_partial(
data: dict, account: Account = Depends(check_admin)
):
updatable_settings = dict_to_settings({**settings.model_dump(), **data})
updatable_settings = dict_to_settings({**settings.dict(), **data})
return await api_update_settings(updatable_settings, account)
+11 -3
View File
@@ -12,6 +12,7 @@ from fastapi.responses import JSONResponse, RedirectResponse
from fastapi_sso.sso.base import OpenID, SSOBase
from loguru import logger
from lnbits.core.crud.settings import set_settings_field
from lnbits.core.crud.users import (
get_user_access_control_lists,
update_user_access_control_list,
@@ -548,6 +549,13 @@ async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
account.hash_password(data.password)
await update_account(account)
settings.first_install = False
# only confrm it after the super user has been successfully updated
if settings.first_install_token:
settings.first_install_token_confirmed = data.first_install_token
await set_settings_field(
"first_install_token_confirmed", data.first_install_token
)
return _auth_success_response(account.username, account.id, account.email)
@@ -587,7 +595,7 @@ def _auth_success_response(
payload = AccessTokenPayload(
sub=username or "", usr=user_id, email=email, auth_time=int(time())
)
access_token = create_access_token(data=payload.model_dump())
access_token = create_access_token(data=payload.dict())
max_age = settings.auth_token_expire_minutes * 60
response = JSONResponse({"access_token": access_token, "token_type": "bearer"})
response.set_cookie(
@@ -617,7 +625,7 @@ def _auth_api_token_response(
sub=username, api_token_id=api_token_id, auth_time=int(time())
)
return create_access_token(
data=payload.model_dump(), token_expire_minutes=token_expire_minutes
data=payload.dict(), token_expire_minutes=token_expire_minutes
)
@@ -625,7 +633,7 @@ def _auth_redirect_response(path: str, user_id: str, email: str) -> RedirectResp
payload = AccessTokenPayload(
usr=user_id, sub="", email=email, auth_time=int(time())
)
access_token = create_access_token(data=payload.model_dump())
access_token = create_access_token(data=payload.dict())
max_age = settings.auth_token_expire_minutes * 60
response = RedirectResponse(path)
response.set_cookie(
+1 -2
View File
@@ -637,8 +637,7 @@ async def create_extension_review(
) -> ExtensionReviewPaymentRequest:
async with httpx.AsyncClient() as client:
resp = await client.post(
settings.lnbits_extensions_reviews_url + "/reviews",
json=data.model_dump(),
settings.lnbits_extensions_reviews_url + "/reviews", json=data.dict()
)
resp.raise_for_status()
payment_request = resp.json()
+37 -125
View File
@@ -8,20 +8,10 @@ import time
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from enum import Enum
from types import UnionType
from typing import (
Any,
ClassVar,
Generic,
Literal,
TypeVar,
Union,
get_args,
get_origin,
)
from typing import Any, Generic, Literal, TypeVar, get_origin
from loguru import logger
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, model_validator
from pydantic import BaseModel, ValidationError, root_validator
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
from sqlalchemy.sql import text
@@ -30,7 +20,6 @@ from lnbits.settings import settings
POSTGRES = "POSTGRES"
COCKROACH = "COCKROACH"
SQLITE = "SQLITE"
PYDANTIC_MODEL_TYPES = (BaseModel,)
DateTrunc = Literal["hour", "day", "month"]
sqlite_formats = {
@@ -39,72 +28,6 @@ sqlite_formats = {
"month": "%Y-%m-01 00:00:00",
}
def _is_pydantic_model_class(model: Any) -> bool:
return isinstance(model, type) and any(
issubclass(model, model_type) for model_type in PYDANTIC_MODEL_TYPES
)
def _issubclass(candidate: Any, parent: Any) -> bool:
return isinstance(candidate, type) and issubclass(candidate, parent)
def _strip_optional(annotation: Any) -> Any:
origin = get_origin(annotation)
if origin not in {Union, UnionType}:
return annotation
args = [arg for arg in get_args(annotation) if arg is not type(None)]
if len(args) == 1:
return _strip_optional(args[0])
return annotation
def _model_fields(model: type[Any]) -> dict[str, Any]:
fields = getattr(model, "model_fields", None)
if fields is not None:
return fields
return getattr(model, "__fields__", {})
def _field_annotation(field: Any) -> Any:
annotation = getattr(field, "annotation", None)
if annotation is None:
annotation = getattr(field, "outer_type_", Any)
return _strip_optional(annotation)
def _field_inner_type(field: Any) -> Any:
type_ = getattr(field, "type_", None)
if type_ is not None:
return _strip_optional(type_)
annotation = _field_annotation(field)
origin = get_origin(annotation)
if origin in {list, set, tuple, dict}:
args = get_args(annotation)
return _strip_optional(args[0]) if args else Any
return annotation
def _field_extra(field: Any) -> dict[str, Any]:
field_info = getattr(field, "field_info", None)
if field_info is not None:
return getattr(field_info, "extra", {})
return getattr(field, "json_schema_extra", None) or {}
def _model_dump(model: BaseModel) -> dict[str, Any]:
return model.model_dump()
def _validate_model(model: type[Any], values: dict[str, Any]) -> Any:
return model.model_validate(values)
if settings.lnbits_database_url:
database_uri = settings.lnbits_database_url
if database_uri.startswith("cockroachdb://"):
@@ -112,7 +35,7 @@ if settings.lnbits_database_url:
else:
if not database_uri.startswith("postgres://"):
raise ValueError(
"Please use the 'postgres://...' format for the database URL."
"Please use the 'postgres://...' " "format for the database URL."
)
DB_TYPE = POSTGRES
@@ -524,8 +447,8 @@ class Operator(Enum):
class FilterModel(BaseModel):
__search_fields__: ClassVar[list[str]] = []
__sort_fields__: ClassVar[list[str] | None] = None
__search_fields__: list[str] = []
__sort_fields__: list[str] | None = None
T = TypeVar("T")
@@ -539,12 +462,10 @@ class Page(BaseModel, Generic[T]):
class Filter(BaseModel, Generic[TFilterModel]):
model_config = ConfigDict(arbitrary_types_allowed=True)
table_name: str | None = None
field: str
op: Operator = Operator.EQ
model: type[TFilterModel] | None = Field(default=None, exclude=True)
model: type[TFilterModel] | None
values: dict | None = None
@classmethod
@@ -566,17 +487,16 @@ class Filter(BaseModel, Generic[TFilterModel]):
field = key
op = Operator("eq")
model_fields = _model_fields(model)
if field in model_fields:
compare_field = model_fields[field]
if field in model.__fields__:
compare_field = model.__fields__[field]
values: dict = {}
if op in {Operator.EVERY, Operator.ANY, Operator.INCLUDE, Operator.EXCLUDE}:
raw_values = [v for rv in raw_values for v in rv.split(",")]
for index, raw_value in enumerate(raw_values):
validated = TypeAdapter(
_field_annotation(compare_field)
).validate_python(raw_value)
validated, errors = compare_field.validate(raw_value, {}, loc="none")
if errors:
raise ValidationError(errors=[errors], model=model)
values[f"{field}__{index}"] = validated
else:
raise ValueError("Unknown filter field")
@@ -588,10 +508,7 @@ class Filter(BaseModel, Generic[TFilterModel]):
prefix = f"{self.table_name}." if self.table_name else ""
stmt = []
for key in self.values.keys() if self.values else []:
if (
self.model
and _field_inner_type(_model_fields(self.model)[self.field]) == datetime
):
if self.model and self.model.__fields__[self.field].type_ == datetime:
placeholder = compat_timestamp_placeholder(key)
stmt.append(f"{prefix}{self.field} {self.op.as_sql} {placeholder}")
if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
@@ -617,9 +534,7 @@ class Filters(BaseModel, Generic[TFilterModel]):
the values can be validated. Otherwise, make sure to validate the inputs manually.
"""
model_config = ConfigDict(arbitrary_types_allowed=True)
filters: list[Filter[TFilterModel]] = Field(default_factory=list)
filters: list[Filter[TFilterModel]] = []
search: str | None = None
offset: int | None = None
@@ -627,20 +542,18 @@ class Filters(BaseModel, Generic[TFilterModel]):
sortby: str | None = None
direction: Literal["asc", "desc"] | None = None
model: type[TFilterModel] | None = Field(default=None, exclude=True)
model: type[TFilterModel] | None = None
table_name: str | None = Field(default=None, exclude=True)
table_name: str | None = None
@model_validator(mode="before")
@classmethod
def validate_sortby(cls, values: Any):
if not isinstance(values, dict):
return values
@root_validator(pre=True)
def validate_sortby(cls, values):
sortby = values.get("sortby")
model = values.get("model")
if sortby and model:
model = values["model"]
# if no sort fields are specified explicitly all fields are allowed
allowed = model.__sort_fields__ or _model_fields(model).keys()
allowed = model.__sort_fields__ or model.__fields__
if sortby not in allowed:
raise ValueError("Invalid sort field")
return values
@@ -754,12 +667,10 @@ def model_to_dict(model: BaseModel) -> dict:
:param model: Pydantic model
"""
_dict: dict = {}
model_fields = _model_fields(type(model))
for key, value in _model_dump(model).items():
field = model_fields[key]
type_ = _field_inner_type(field)
outertype_ = _field_annotation(field)
if _field_extra(field).get("no_database", False):
for key, value in model.dict().items():
type_ = model.__fields__[key].type_
outertype_ = model.__fields__[key].outer_type_
if model.__fields__[key].field_info.extra.get("no_database", False):
continue
if isinstance(value, datetime):
if DB_TYPE == SQLITE:
@@ -770,7 +681,7 @@ def model_to_dict(model: BaseModel) -> dict:
_dict[key] = value.replace(tzinfo=None)
continue
if (
_is_pydantic_model_class(type_)
type(type_) is type(BaseModel)
or type_ is dict
or get_origin(outertype_) is list
):
@@ -794,50 +705,51 @@ def dict_to_submodel(model: type[TModel], value: dict | str) -> TModel | None:
return dict_to_model(_subdict, model)
def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
def dict_to_model(_row: dict, model: type[TModel]) -> TModel: # noqa: C901
"""
Convert a dictionary with JSON-encoded nested models to a Pydantic model
:param _dict: Dictionary from database
:param model: Pydantic model
"""
_dict: dict = {}
model_fields = _model_fields(model)
for key, value in _row.items():
if value is None:
continue
if key not in model_fields:
if key not in model.__fields__:
# Somethimes an SQL JOIN will create additional column
continue
field = model_fields[key]
type_ = _field_inner_type(field)
outertype_ = _field_annotation(field)
type_ = model.__fields__[key].type_
outertype_ = model.__fields__[key].outer_type_
if get_origin(outertype_) is list:
_items = _safe_load_json(value) if isinstance(value, str) else value
_dict[key] = [
dict_to_submodel(type_, v) if _is_pydantic_model_class(type_) else v
dict_to_submodel(type_, v) if issubclass(type_, BaseModel) else v
for v in _items
]
continue
if _issubclass(type_, bool):
if issubclass(type_, bool):
_dict[key] = bool(value)
continue
if _issubclass(type_, datetime):
if issubclass(type_, datetime):
if DB_TYPE == SQLITE:
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
else:
_dict[key] = value.replace(tzinfo=timezone.utc)
continue
if _is_pydantic_model_class(type_):
if issubclass(type_, BaseModel):
_dict[key] = dict_to_submodel(type_, value)
continue
# TODO: remove this when all sub models are migrated to Pydantic
# NOTE: this is for type dict on BaseModel, (used in Payment class)
if (type_ is dict or get_origin(outertype_) is dict) and value:
if type_ is dict and value:
_dict[key] = _safe_load_json(value)
continue
_dict[key] = value
continue
return _validate_model(model, _dict)
_model = model.construct(**_dict)
if isinstance(_model, BaseModel):
_model.__init__(**_dict) # type: ignore
return _model
def _safe_load_json(value: str) -> dict:
+8 -5
View File
@@ -6,7 +6,7 @@ from typing import Any, Literal
import httpx
from loguru import logger
from pydantic import BaseModel, ConfigDict, Field, ValidationError
from pydantic import BaseModel, Field, ValidationError
from lnbits.helpers import normalize_endpoint, urlsafe_short_hash
from lnbits.settings import settings
@@ -28,7 +28,8 @@ FiatMethod = Literal["checkout", "subscription"]
class PayPalCheckoutOptions(BaseModel):
model_config = ConfigDict(extra="ignore")
class Config:
extra = "ignore"
success_url: str | None = None
cancel_url: str | None = None
@@ -36,14 +37,16 @@ class PayPalCheckoutOptions(BaseModel):
class PayPalSubscriptionOptions(BaseModel):
model_config = ConfigDict(extra="ignore")
class Config:
extra = "ignore"
checking_id: str | None = None
payment_request: str | None = None
class PayPalCreateInvoiceOptions(BaseModel):
model_config = ConfigDict(extra="ignore")
class Config:
extra = "ignore"
fiat_method: FiatMethod = "checkout"
checkout: PayPalCheckoutOptions | None = None
@@ -336,7 +339,7 @@ class PayPalWallet(FiatProvider):
self, raw_opts: dict[str, Any]
) -> PayPalCreateInvoiceOptions | None:
try:
return PayPalCreateInvoiceOptions.model_validate(raw_opts)
return PayPalCreateInvoiceOptions.parse_obj(raw_opts)
except ValidationError as e:
logger.warning(f"Invalid PayPal options: {e}")
return None
+11 -10
View File
@@ -8,7 +8,7 @@ from urllib.parse import urlencode
import httpx
from loguru import logger
from pydantic import BaseModel, ConfigDict, Field, ValidationError
from pydantic import BaseModel, Field, ValidationError
from lnbits.helpers import normalize_endpoint, urlsafe_short_hash
from lnbits.settings import settings
@@ -30,7 +30,8 @@ FiatMethod = Literal["checkout", "terminal", "subscription"]
class StripeTerminalOptions(BaseModel):
model_config = ConfigDict(extra="ignore")
class Config:
extra = "ignore"
capture_method: Literal["automatic", "manual"] = "automatic"
metadata: dict[str, str] = Field(default_factory=dict)
@@ -38,7 +39,8 @@ class StripeTerminalOptions(BaseModel):
class StripeCheckoutOptions(BaseModel):
model_config = ConfigDict(extra="ignore")
class Config:
extra = "ignore"
success_url: str | None = None
metadata: dict[str, str] = Field(default_factory=dict)
@@ -46,14 +48,16 @@ class StripeCheckoutOptions(BaseModel):
class StripeSubscriptionOptions(BaseModel):
model_config = ConfigDict(extra="ignore")
class Config:
extra = "ignore"
checking_id: str | None = None
payment_request: str | None = None
class StripeCreateInvoiceOptions(BaseModel):
model_config = ConfigDict(extra="ignore")
class Config:
extra = "ignore"
fiat_method: FiatMethod = "checkout"
terminal: StripeTerminalOptions | None = None
@@ -167,10 +171,7 @@ class StripeWallet(FiatProvider):
("line_items[0][price]", subscription_id),
("line_items[0][quantity]", f"{quantity}"),
]
subscription_data = {
**payment_options.model_dump(),
"alan_action": "subscription",
}
subscription_data = {**payment_options.dict(), "alan_action": "subscription"}
subscription_data["extra"] = json.dumps(subscription_data.get("extra") or {})
form_data += self._encode_metadata(
@@ -493,7 +494,7 @@ class StripeWallet(FiatProvider):
self, raw_opts: dict[str, Any]
) -> StripeCreateInvoiceOptions | None:
try:
return StripeCreateInvoiceOptions.model_validate(raw_opts)
return StripeCreateInvoiceOptions.parse_obj(raw_opts)
except ValidationError as e:
logger.warning(f"Invalid Stripe options: {e}")
return None
+9 -11
View File
@@ -12,6 +12,7 @@ import shortuuid
from fastapi.routing import APIRoute
from loguru import logger
from packaging import version
from pydantic.schema import field_schema
from starlette.templating import Jinja2Templates
from lnbits.settings import settings
@@ -70,7 +71,7 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
# used in base.html
t.env.globals["SITE_TITLE"] = settings.lnbits_site_title
t.env.globals["LNBITS_APPLE_TOUCH_ICON"] = settings.lnbits_apple_touch_icon
t.env.globals["SETTINGS"] = settings.to_public().model_dump(by_alias=True)
t.env.globals["SETTINGS"] = settings.to_public().dict(by_alias=True)
t.env.globals["CURRENCIES"] = list(currencies.keys())
if settings.bundle_assets:
@@ -126,26 +127,23 @@ def generate_filter_params_openapi(model: type[FilterModel], keep_optional=False
:param keep_optional: If false, all parameters will be optional,
otherwise inferred from model
"""
schema = model.model_json_schema()
properties = schema.get("properties", {})
required = set(schema.get("required", []))
fields = list(model.__fields__.values())
params = []
for field_name, field in model.model_fields.items():
field_key = field.alias or field_name
field_schema = properties.get(field_key, {})
for field in fields:
schema, _, _ = field_schema(field, model_name_map={})
description = "Supports Filtering"
if (
hasattr(model, "__search_fields__")
and field_name in model.__search_fields__
and field.name in model.__search_fields__
):
description += ". Supports Search"
parameter = {
"name": field_key,
"name": field.alias,
"in": "query",
"required": field_key in required if keep_optional else False,
"schema": field_schema,
"required": field.required if keep_optional else False,
"schema": schema,
"description": description,
}
params.append(parameter)
+1 -1
View File
@@ -364,7 +364,7 @@ class LndRestNode(Node):
fee_report = await self.get("/v1/fees")
balance = await self.get("/v1/balance/channels")
return NodeInfoResponse(
**public.model_dump(),
**public.dict(),
onchain_balance_sat=onchain["total_balance"],
onchain_confirmed_sat=onchain["confirmed_balance"],
balance_msat=balance["local_balance"]["msat"],
+38 -116
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import importlib
import importlib.metadata
import inspect
import json
import os
import re
@@ -10,12 +11,11 @@ from enum import Enum
from os import path
from pathlib import Path
from time import gmtime, strftime, time
from typing import Any, get_args, get_origin
from typing import Any
from uuid import uuid4
from dotenv import dotenv_values
from loguru import logger
from pydantic import BaseModel, ConfigDict, Field, field_validator
from pydantic import BaseModel, BaseSettings, Extra, Field, validator
def list_parse_fallback(v: str):
@@ -29,89 +29,6 @@ def list_parse_fallback(v: str):
return []
def _remove_env_names(schema: dict[str, Any]) -> None:
for prop in schema.get("properties", {}).values():
prop.pop("env_names", None)
def _iter_validation_aliases(validation_alias: Any) -> list[str]:
if isinstance(validation_alias, str):
return [validation_alias]
choices = getattr(validation_alias, "choices", None)
if not choices:
return []
return [choice for choice in choices if isinstance(choice, str)]
def _annotation_has_origin(annotation: Any, origins: tuple[type, ...]) -> bool:
origin = get_origin(annotation)
if origin in origins:
return True
return any(_annotation_has_origin(arg, origins) for arg in get_args(annotation))
def _parse_settings_value(value: Any, annotation: Any) -> Any:
if not isinstance(value, str):
return value
stripped_value = value.strip()
if stripped_value.startswith("[") or stripped_value.startswith("{"):
return json.loads(stripped_value)
if _annotation_has_origin(annotation, (list, set, tuple)):
return list_parse_fallback(value)
return value
class LNbitsBaseSettings(BaseModel):
def __init__(self, **data):
super().__init__(**{**self._settings_data(), **data})
@classmethod
def _settings_data(cls) -> dict[str, Any]:
env_file = cls.model_config.get("env_file")
env_file_encoding = cls.model_config.get("env_file_encoding")
case_sensitive = bool(cls.model_config.get("case_sensitive", False))
raw_values: dict[str, Any] = {}
if env_file:
dotenv_items = dotenv_values(env_file, encoding=env_file_encoding)
raw_values.update(
{key: value for key, value in dotenv_items.items() if value is not None}
)
raw_values.update(os.environ)
lookup_values = (
raw_values
if case_sensitive
else {str(key).lower(): value for key, value in raw_values.items()}
)
settings_values: dict[str, Any] = {}
for field_name, field in cls.model_fields.items():
lookup_keys = _iter_validation_aliases(field.validation_alias)
if field.alias and field.alias != field_name:
lookup_keys.append(field.alias)
lookup_keys.append(field_name)
for key in lookup_keys:
lookup_key = key if case_sensitive else key.lower()
if lookup_key not in lookup_values:
continue
settings_values[field_name] = _parse_settings_value(
lookup_values[lookup_key], field.annotation
)
break
return settings_values
class LNbitsSettings(BaseModel):
@classmethod
def validate_list(cls, val):
@@ -407,7 +324,7 @@ class AssetSettings(LNbitsSettings):
"heif",
"heics",
"text/plain",
"text/jsontext/xml",
"text/json" "text/xml",
"application/json",
"application/pdf",
]
@@ -530,6 +447,7 @@ class SecuritySettings(LNbitsSettings):
lnbits_max_outgoing_payment_amount_sats: int = Field(default=10_000_000, ge=0)
lnbits_max_incoming_payment_amount_sats: int = Field(default=10_000_000, ge=0)
first_install_token_confirmed: str | None = Field(default=None)
def is_wallet_max_balance_exceeded(self, amount):
return (
@@ -735,9 +653,9 @@ class BoltzFundingSource(LNbitsSettings):
class StrikeFundingSource(LNbitsSettings):
strike_api_endpoint: str | None = Field(
default="https://api.strike.me/v1", validation_alias="STRIKE_API_ENDPOINT"
default="https://api.strike.me/v1", env="STRIKE_API_ENDPOINT"
)
strike_api_key: str | None = Field(default=None, validation_alias="STRIKE_API_KEY")
strike_api_key: str | None = Field(default=None, env="STRIKE_API_KEY")
class FiatProviderLimits(BaseModel):
@@ -1054,12 +972,12 @@ class EditableSettings(
KeycloakAuthSettings,
OidcAuthSettings,
):
@field_validator(
@validator(
"lnbits_admin_users",
"lnbits_allowed_users",
"lnbits_theme_options",
"lnbits_admin_extensions",
mode="before",
pre=True,
)
@classmethod
def validate_editable_settings(cls, val):
@@ -1067,21 +985,21 @@ class EditableSettings(
@classmethod
def from_dict(cls, d: dict):
return cls(**{k: v for k, v in d.items() if k in cls.model_fields})
return cls(
**{k: v for k, v in d.items() if k in inspect.signature(cls).parameters}
)
# Fixes openapi.json validation by removing the v1-only env_names metadata.
model_config = ConfigDict(
populate_by_name=True,
json_schema_extra=_remove_env_names,
)
# fixes openapi.json validation, remove field env_names
class Config:
@staticmethod
def schema_extra(schema: dict[str, Any]) -> None:
for prop in schema.get("properties", {}).values():
prop.pop("env_names", None)
class UpdateSettings(EditableSettings):
model_config = ConfigDict(
populate_by_name=True,
extra="forbid",
json_schema_extra=_remove_env_names,
)
class Config:
extra = Extra.forbid
class EnvSettings(LNbitsSettings):
@@ -1113,6 +1031,13 @@ class EnvSettings(LNbitsSettings):
def has_default_extension_path(self) -> bool:
return self.lnbits_extensions_path == "lnbits"
def has_first_install_token_changed(self) -> bool:
if not self.first_install_token:
return False
if not settings.first_install_token_confirmed:
return False
return self.first_install_token != settings.first_install_token_confirmed
def check_auth_secret_key(self):
if self.auth_secret_key:
return
@@ -1193,7 +1118,7 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
@classmethod
def readonly_fields(cls):
return [f for f in cls.model_fields if not f.startswith("_")]
return [f for f in inspect.signature(cls).parameters if not f.startswith("_")]
class ReadOnlySettings(
@@ -1208,9 +1133,9 @@ class ReadOnlySettings(
def lnbits_extensions_upgrade_path(self) -> str:
return str(Path(self.lnbits_data_folder, "upgrades"))
@field_validator(
@validator(
"lnbits_allowed_funding_sources",
mode="before",
pre=True,
)
@classmethod
def validate_readonly_settings(cls, val):
@@ -1218,18 +1143,15 @@ class ReadOnlySettings(
@classmethod
def readonly_fields(cls):
return [f for f in cls.model_fields if not f.startswith("_")]
return [f for f in inspect.signature(cls).parameters if not f.startswith("_")]
class Settings(
EditableSettings, ReadOnlySettings, TransientSettings, LNbitsBaseSettings
):
model_config = ConfigDict(
populate_by_name=True,
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
)
class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettings):
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
case_sensitive = False
json_loads = list_parse_fallback
def is_user_allowed(self, user_id: str) -> bool:
return (
@@ -1434,7 +1356,7 @@ if not settings.user_agent:
# printing environment variable for debugging
if not settings.lnbits_admin_ui:
logger.debug("Environment Settings:")
for key, value in settings.model_dump(exclude_none=True).items():
for key, value in settings.dict(exclude_none=True).items():
logger.debug(f"{key}: {value}")
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -59,7 +59,7 @@ window.LNbits = {
newWallet.canSendPayments = perms.includes('send-payments')
}
newWallet.url = `/wallet?&wal=${data.id}`
newWallet.storedPaylinks = data.stored_paylinks?.links
newWallet.storedPaylinks = data.stored_paylinks.links
return newWallet
}
}
@@ -165,5 +165,26 @@ window.app.component('lnbits-qrcode', {
this.$refs.qrCode.$el.style.maxWidth = this.maxWidth + 'px'
this.$refs.qrCode.$el.setAttribute('width', '100%')
this.$refs.qrCode.$el.removeAttribute('height')
},
computed: {
optimizedValue() {
const separatorIndex = this.value.indexOf(':')
const type =
separatorIndex === -1 ? '' : this.value.substring(0, separatorIndex)
const value =
separatorIndex === -1
? this.value
: this.value.substring(separatorIndex + 1)
if (this.utils.isValidBech32(value)) {
const normalizedValue = value.toUpperCase()
if (type) {
return `${type.toUpperCase()}:${normalizedValue}`
}
return normalizedValue
}
return this.value
}
}
})
+40
View File
@@ -160,6 +160,46 @@ window._lnbitsUtils = {
return null
}
},
isValidBech32(value) {
if (typeof value !== 'string') {
return false
}
const candidate = value.trim()
if (
!candidate ||
(candidate !== candidate.toLowerCase() &&
candidate !== candidate.toUpperCase())
) {
return false
}
const normalized = candidate.toLowerCase()
const splitPosition = normalized.lastIndexOf('1')
if (splitPosition <= 0) {
return false
}
const humanReadablePart = normalized.substring(0, splitPosition)
const data = normalized.substring(splitPosition + 1)
if (data.length < 6) {
return false
}
if (
typeof bech32ToFiveBitArray !== 'function' ||
typeof verify_checksum !== 'function'
) {
return false
}
const words = bech32ToFiveBitArray(data)
if (words.some(word => word < 0)) {
return false
}
return verify_checksum(humanReadablePart, words)
},
async notifyApiError(error) {
if (!error.response) {
return console.error(error)
@@ -12,7 +12,7 @@
>
<qrcode-vue
ref="qrCode"
:value="value"
:value="optimizedValue"
:margin="margin"
:size="size"
level="Q"
+5 -1
View File
@@ -15,7 +15,11 @@ from lnbits.settings import settings
def log_server_info():
logger.info("LNbits Info")
if settings.first_install:
logger.success("This is a fresh install of LNbits.")
if settings.has_first_install_token_changed():
logger.success("This is a first install token reset.")
else:
logger.success("This is a fresh install of LNbits.")
if settings.first_install_token:
logger.success(
f"FIRST_INSTALL_TOKEN: `{settings.first_install_token}`. "
+122 -23
View File
@@ -80,22 +80,40 @@ class SparkL2Wallet(Wallet):
async def status(self) -> StatusResponse:
try:
res = await self._request("POST", "/v1/balance")
status = res.get("status")
r = await self.client.post("/v1/balance", timeout=30)
r.raise_for_status()
data = r.json()
if not isinstance(data, dict) or len(data) == 0:
return StatusResponse("no data", 0)
error_message = self._extract_error_message(data)
if error_message:
return StatusResponse(self._server_error_message(error_message), 0)
status = data.get("status")
if status == "missing_mnemonic":
await self._check_sidecar_mnemonic()
return StatusResponse("Spark sidecar mnemonic not set", 0)
balance_msat = res.get("balance_msat")
balance_msat = data.get("balance_msat")
if balance_msat is not None:
return StatusResponse(None, int(balance_msat))
balance_sats = res.get("balance_sats")
balance_sats = data.get("balance_sats")
if balance_sats is None:
return StatusResponse("Spark sidecar: missing balance.", 0)
return StatusResponse("no data", 0)
return StatusResponse(None, int(balance_sats) * 1000)
except json.JSONDecodeError as e:
logger.warning(e)
return StatusResponse("Server error: 'invalid json response'", 0)
except httpx.HTTPStatusError as e:
logger.warning(e)
error_message = self._extract_http_error_message(e.response)
if error_message:
return StatusResponse(self._server_error_message(error_message), 0)
return StatusResponse(self._connect_error_message(), 0)
except Exception as e:
logger.warning(e)
return StatusResponse(f"Spark sidecar status error: {e}", 0)
return StatusResponse(self._connect_error_message(), 0)
async def create_invoice(
self,
@@ -121,13 +139,28 @@ class SparkL2Wallet(Wallet):
"description_hash": description_hash_hex,
"expiry_seconds": expiry_secs,
}
res = await self._request("POST", "/v1/invoices", payload)
bolt11 = res.get("payment_request")
checking_id = res.get("checking_id")
r = await self.client.post("/v1/invoices", json=payload, timeout=30)
r.raise_for_status()
data = r.json()
if not isinstance(data, dict):
return InvoiceResponse(
ok=False, error_message=self._server_error_message(r.text)
)
error_message = self._extract_error_message(data)
if error_message:
return InvoiceResponse(
ok=False,
error_message=self._server_error_message(error_message),
)
bolt11 = data.get("payment_request")
checking_id = data.get("checking_id")
if not bolt11 or not checking_id:
return InvoiceResponse(
ok=False,
error_message="Spark sidecar invoice response missing fields.",
error_message="Server error: 'missing required fields'",
)
self.pending_invoices.append(checking_id)
@@ -135,10 +168,24 @@ class SparkL2Wallet(Wallet):
ok=True,
payment_request=bolt11,
checking_id=checking_id,
preimage=res.get("preimage", None),
preimage=data.get("preimage", None),
)
except json.JSONDecodeError:
return InvoiceResponse(
ok=False, error_message="Server error: 'invalid json response'"
)
except httpx.HTTPStatusError as e:
logger.warning(e)
error_message = self._extract_http_error_message(e.response)
if error_message:
return InvoiceResponse(
ok=False,
error_message=self._server_error_message(error_message),
)
return InvoiceResponse(ok=False, error_message=self._connect_error_message())
except Exception as e:
return InvoiceResponse(ok=False, error_message=str(e))
logger.warning(e)
return InvoiceResponse(ok=False, error_message=self._connect_error_message())
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
try:
@@ -158,27 +205,53 @@ class SparkL2Wallet(Wallet):
"max_fee_sats": max_fee_sats,
"payment_hash": payment_hash,
}
res = await self._request("POST", "/v1/payments", payload)
checking_id = payment_hash or res.get("checking_id")
r = await self.client.post("/v1/payments", json=payload, timeout=30)
r.raise_for_status()
data = r.json()
if not isinstance(data, dict):
return PaymentResponse(error_message=self._server_error_message(r.text))
error_message = self._extract_error_message(data)
if error_message:
return PaymentResponse(error_message=error_message)
if len(data) == 0:
return PaymentResponse(
error_message="Server error: 'missing required fields'"
)
status = data.get("status")
fee_msat = data.get("fee_msat")
preimage = data.get("preimage")
ok = self._map_payment_ok(status) if status else None
if ok is False:
return PaymentResponse(ok=False)
checking_id = payment_hash or data.get("checking_id")
if not checking_id:
return PaymentResponse(
ok=False,
error_message="Spark sidecar payment response missing checking_id.",
error_message="Server error: 'missing required fields'"
)
status = res.get("status")
fee_msat = res.get("fee_msat")
ok = None
if status:
ok = self._map_payment_ok(status)
return PaymentResponse(
ok=ok,
checking_id=checking_id,
fee_msat=int(fee_msat) if fee_msat is not None else None,
preimage=res.get("preimage"),
preimage=preimage,
)
except json.JSONDecodeError:
return PaymentResponse(error_message="Server error: 'invalid json response'")
except httpx.HTTPStatusError as e:
logger.warning(e)
error_message = self._extract_http_error_message(e.response)
if error_message:
return PaymentResponse(error_message=error_message)
return PaymentResponse(error_message=self._connect_error_message())
except Exception as e:
return PaymentResponse(ok=False, error_message=str(e))
logger.warning(e)
return PaymentResponse(error_message=self._connect_error_message())
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
try:
@@ -330,6 +403,32 @@ class SparkL2Wallet(Wallet):
return False
return None
def _connect_error_message(self) -> str:
return f"Unable to connect to {self.endpoint}."
@staticmethod
def _server_error_message(message: str) -> str:
return f"Server error: '{message}'"
@staticmethod
def _extract_error_message(data: Any) -> str | None:
if not isinstance(data, dict):
return None
for key in ("error", "message", "detail", "reason"):
value = data.get(key)
if value:
return str(value)
return None
def _extract_http_error_message(self, response: httpx.Response | None) -> str | None:
if response is None:
return None
try:
data = response.json()
except Exception:
return None
return self._extract_error_message(data)
async def _check_sidecar_mnemonic(self):
if settings.spark_l2_mnemonic:
valid = mnemonic_is_valid(settings.spark_l2_mnemonic)
+3 -3
View File
@@ -1498,9 +1498,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {
Generated
+49 -178
View File
@@ -199,18 +199,6 @@ files = [
{file = "annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"},
]
[[package]]
name = "annotated-types"
version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
[[package]]
name = "anyio"
version = "4.12.1"
@@ -2271,14 +2259,14 @@ valkey = ["valkey (>=6)"]
[[package]]
name = "lnurl"
version = "2.0.0"
version = "0.10.0"
description = "LNURL implementation for Python."
optional = false
python-versions = "<3.13,>=3.10"
groups = ["main"]
files = [
{file = "lnurl-2.0.0-py3-none-any.whl", hash = "sha256:e355f7433aa5ba31557ffbfd042978e8e6f9df90aaae907ebeb613f06ff4f9f4"},
{file = "lnurl-2.0.0.tar.gz", hash = "sha256:4ac17e289eb3818f217b84b41d455f27d0161b300560f6bddd345b923b5ba82d"},
{file = "lnurl-0.10.0-py3-none-any.whl", hash = "sha256:0d43561b4370cecec19c8f1044fc840f0199fc2698bc83cd8782e8264bfde8f4"},
{file = "lnurl-0.10.0.tar.gz", hash = "sha256:55ba0a7e810bcdfc7410c682f91c76df4446ff24e83d6bf931a7c759e2e0d5cc"},
]
[package.dependencies]
@@ -2288,7 +2276,7 @@ bolt11 = "*"
coincurve = ">=20.0.0"
httpx = "*"
pycryptodomex = ">=3.21.0"
pydantic = ">=2.10.0,<3.0.0"
pydantic = ">=1.10.0,<2.0.0"
[[package]]
name = "loguru"
@@ -3332,160 +3320,58 @@ files = [
[[package]]
name = "pydantic"
version = "2.12.5"
description = "Data validation using Python type hints"
version = "1.10.26"
description = "Data validation and settings management using python type hints"
optional = false
python-versions = ">=3.9"
python-versions = ">=3.7"
groups = ["main", "dev"]
files = [
{file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"},
{file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"},
{file = "pydantic-1.10.26-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f7ae36fa0ecef8d39884120f212e16c06bb096a38f523421278e2f39c1784546"},
{file = "pydantic-1.10.26-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d95a76cf503f0f72ed7812a91de948440b2bf564269975738a4751e4fadeb572"},
{file = "pydantic-1.10.26-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a943ce8e00ad708ed06a1d9df5b4fd28f5635a003b82a4908ece6f24c0b18464"},
{file = "pydantic-1.10.26-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:465ad8edb29b15c10b779b16431fe8e77c380098badf6db367b7a1d3e572cf53"},
{file = "pydantic-1.10.26-cp310-cp310-win_amd64.whl", hash = "sha256:80e6be6272839c8a7641d26ad569ab77772809dd78f91d0068dc0fc97f071945"},
{file = "pydantic-1.10.26-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:116233e53889bcc536f617e38c1b8337d7fa9c280f0fd7a4045947515a785637"},
{file = "pydantic-1.10.26-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3cfdd361addb6eb64ccd26ac356ad6514cee06a61ab26b27e16b5ed53108f77"},
{file = "pydantic-1.10.26-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e4451951a9a93bf9a90576f3e25240b47ee49ab5236adccb8eff6ac943adf0f"},
{file = "pydantic-1.10.26-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9858ed44c6bea5f29ffe95308db9e62060791c877766c67dd5f55d072c8612b5"},
{file = "pydantic-1.10.26-cp311-cp311-win_amd64.whl", hash = "sha256:ac1089f723e2106ebde434377d31239e00870a7563245072968e5af5cc4d33df"},
{file = "pydantic-1.10.26-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:468d5b9cacfcaadc76ed0a4645354ab6f263ec01a63fb6d05630ea1df6ae453f"},
{file = "pydantic-1.10.26-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2c1b0b914be31671000ca25cf7ea17fcaaa68cfeadf6924529c5c5aa24b7ab1f"},
{file = "pydantic-1.10.26-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15b13b9f8ba8867095769e1156e0d7fbafa1f65b898dd40fd1c02e34430973cb"},
{file = "pydantic-1.10.26-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad7025ca324ae263d4313998e25078dcaec5f9ed0392c06dedb57e053cc8086b"},
{file = "pydantic-1.10.26-cp312-cp312-win_amd64.whl", hash = "sha256:4482b299874dabb88a6c3759e3d85c6557c407c3b586891f7d808d8a38b66b9c"},
{file = "pydantic-1.10.26-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ae7913bb40a96c87e3d3f6fe4e918ef53bf181583de4e71824360a9b11aef1c"},
{file = "pydantic-1.10.26-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8154c13f58d4de5d3a856bb6c909c7370f41fb876a5952a503af6b975265f4ba"},
{file = "pydantic-1.10.26-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8af0507bf6118b054a9765fb2e402f18a8b70c964f420d95b525eb711122d62"},
{file = "pydantic-1.10.26-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dcb5a7318fb43189fde6af6f21ac7149c4bcbcfffc54bc87b5becddc46084847"},
{file = "pydantic-1.10.26-cp313-cp313-win_amd64.whl", hash = "sha256:71cde228bc0600cf8619f0ee62db050d1880dcc477eba0e90b23011b4ee0f314"},
{file = "pydantic-1.10.26-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6b40730cc81d53d515dc0b8bb5c9b43fadb9bed46de4a3c03bd95e8571616dba"},
{file = "pydantic-1.10.26-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c3bbb9c0eecdf599e4db9b372fa9cc55be12e80a0d9c6d307950a39050cb0e37"},
{file = "pydantic-1.10.26-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc2e3fe7bc4993626ef6b6fa855defafa1d6f8996aa1caef2deb83c5ac4d043a"},
{file = "pydantic-1.10.26-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:36d9e46b588aaeb1dcd2409fa4c467fe0b331f3cc9f227b03a7a00643704e962"},
{file = "pydantic-1.10.26-cp314-cp314-win_amd64.whl", hash = "sha256:81ce3c8616d12a7be31b4aadfd3434f78f6b44b75adbfaec2fe1ad4f7f999b8c"},
{file = "pydantic-1.10.26-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc5c91a3b3106caf07ac6735ec6efad8ba37b860b9eb569923386debe65039ad"},
{file = "pydantic-1.10.26-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dde599e0388e04778480d57f49355c9cc7916de818bf674de5d5429f2feebfb6"},
{file = "pydantic-1.10.26-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8be08b5cfe88e58198722861c7aab737c978423c3a27300911767931e5311d0d"},
{file = "pydantic-1.10.26-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0141f4bafe5eda539d98c9755128a9ea933654c6ca4306b5059fc87a01a38573"},
{file = "pydantic-1.10.26-cp38-cp38-win_amd64.whl", hash = "sha256:eb664305ffca8a9766a8629303bb596607d77eae35bb5f32ff9245984881b638"},
{file = "pydantic-1.10.26-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:502b9d30d18a2dfaf81b7302f6ba0e5853474b1c96212449eb4db912cb604b7d"},
{file = "pydantic-1.10.26-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0d8f6087bf697dec3bf7ffcd7fe8362674f16519f3151789f33cbe8f1d19fc15"},
{file = "pydantic-1.10.26-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dd40a99c358419910c85e6f5d22f9c56684c25b5e7abc40879b3b4a52f34ae90"},
{file = "pydantic-1.10.26-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ce3293b86ca9f4125df02ff0a70be91bc7946522467cbd98e7f1493f340616ba"},
{file = "pydantic-1.10.26-cp39-cp39-win_amd64.whl", hash = "sha256:1a4e3062b71ab1d5df339ba12c48f9ed5817c5de6cb92a961dd5c64bb32e7b96"},
{file = "pydantic-1.10.26-py3-none-any.whl", hash = "sha256:c43ad70dc3ce7787543d563792426a16fd7895e14be4b194b5665e36459dd917"},
{file = "pydantic-1.10.26.tar.gz", hash = "sha256:8c6aa39b494c5af092e690127c283d84f363ac36017106a9e66cb33a22ac412e"},
]
[package.dependencies]
annotated-types = ">=0.6.0"
email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""}
pydantic-core = "2.41.5"
typing-extensions = ">=4.14.1"
typing-inspection = ">=0.4.2"
email-validator = {version = ">=1.0.3", optional = true, markers = "extra == \"email\""}
typing-extensions = ">=4.2.0"
[package.extras]
email = ["email-validator (>=2.0.0)"]
timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
[[package]]
name = "pydantic-core"
version = "2.41.5"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
files = [
{file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"},
{file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"},
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"},
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"},
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"},
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"},
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"},
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"},
{file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"},
{file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"},
{file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"},
{file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"},
{file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"},
{file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"},
{file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"},
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"},
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"},
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"},
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"},
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"},
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"},
{file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"},
{file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"},
{file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"},
{file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"},
{file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"},
{file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"},
{file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"},
{file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"},
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"},
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"},
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"},
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"},
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"},
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"},
{file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"},
{file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"},
{file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"},
{file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"},
{file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"},
{file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"},
{file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"},
{file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"},
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"},
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"},
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"},
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"},
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"},
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"},
{file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"},
{file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"},
{file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"},
{file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"},
{file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"},
{file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"},
{file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"},
{file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"},
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"},
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"},
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"},
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"},
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"},
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"},
{file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"},
{file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"},
{file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"},
{file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"},
{file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"},
{file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"},
{file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"},
{file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"},
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"},
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"},
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"},
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"},
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"},
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"},
{file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"},
{file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"},
{file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"},
{file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"},
{file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"},
{file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"},
{file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"},
{file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"},
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"},
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"},
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"},
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"},
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"},
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"},
{file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"},
{file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"},
{file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"},
{file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"},
{file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"},
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"},
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"},
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"},
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"},
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"},
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"},
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"},
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"},
{file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"},
]
[package.dependencies]
typing-extensions = ">=4.14.1"
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
[[package]]
name = "pygments"
@@ -4590,21 +4476,6 @@ files = [
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
description = "Runtime typing introspection tools"
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
files = [
{file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"},
{file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"},
]
[package.dependencies]
typing-extensions = ">=4.12.0"
[[package]]
name = "urllib3"
version = "2.6.3"
@@ -5157,4 +5028,4 @@ migration = ["psycopg2-binary"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<3.13"
content-hash = "42433ecd056377e5a537ebf0d9aeb70fc2bc275276ba9721310e8efc7a7d933b"
content-hash = "42751c05e1fa8250ff90981def5e0ce8d1b284eeb2acc2c7ac9ae5847cf99e17"
+7 -8
View File
@@ -1,6 +1,6 @@
[project]
name = "lnbits"
version = "1.5.2"
version = "1.5.3"
requires-python = ">=3.10,<3.13"
description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
@@ -9,15 +9,15 @@ readme = "README.md"
dependencies = [
"bech32~=1.2.0",
"click~=8.3.1",
"fastapi~=0.135.0",
"starlette~=1.0.0",
"fastapi~=0.116.1",
"starlette~=0.47.1",
"httpx~=0.27.2",
"jinja2~=3.1.6",
"lnurl~=2.0.0",
"pydantic~=2.12.0",
"lnurl~=0.10.0",
"pydantic~=1.10.26",
"pyqrcode~=1.2.1",
"shortuuid~=1.0.13",
"sse-starlette~=3.3.3",
"sse-starlette~=2.3.6",
"typing-extensions~=4.15.0",
"uvicorn~=0.40.0",
"sqlalchemy~=1.4.54",
@@ -37,7 +37,7 @@ dependencies = [
"bolt11~=2.1.1",
"pyjwt~=2.12.0",
"itsdangerous~=2.2.0",
"fastapi-sso~=0.21.0",
"fastapi-sso~=0.19.0",
# needed for boltz, lnurldevice, watchonly extensions
"embit~=0.8.0",
# needed for scheduler extension
@@ -51,7 +51,6 @@ dependencies = [
"pillow~=12.1.0",
"python-dotenv~=1.2.1",
"greenlet~=3.3.0",
"pydantic-settings>=2.13.1",
]
[project.scripts]
+1
View File
@@ -72,6 +72,7 @@ async def app(settings: Settings):
username="superadmin",
password="secret1234",
password_repeat="secret1234",
first_install_token=settings.first_install_token,
)
)
+1 -1
View File
@@ -1,7 +1,7 @@
import random
import string
from pydantic.v1 import BaseModel
from pydantic import BaseModel
from lnbits.wallets import get_funding_source, set_funding_source
+6 -6
View File
@@ -4,7 +4,7 @@ from http import HTTPStatus
import pytest
from httpx import AsyncClient, Headers
from pydantic import TypeAdapter
from pydantic import parse_obj_as
from lnbits import bolt11
from lnbits.nodes.base import ChannelPoint, ChannelState, NodeChannel
@@ -111,7 +111,7 @@ async def test_get_channel(node_client):
await asyncio.sleep(3)
response = await node_client.get("/node/api/v1/channels")
assert response.status_code == 200
channels = TypeAdapter(list[NodeChannel]).validate_python(response.json())
channels = parse_obj_as(list[NodeChannel], response.json())
ch = random.choice(
[channel for channel in channels if channel.state == ChannelState.ACTIVE]
)
@@ -121,7 +121,7 @@ async def test_get_channel(node_client):
response = await node_client.get(f"/node/api/v1/channels/{ch.id}")
assert response.status_code == 200
channel = TypeAdapter(NodeChannel).validate_python(response.json())
channel = parse_obj_as(NodeChannel, response.json())
assert channel.id == ch.id
@@ -131,7 +131,7 @@ async def test_set_channel_fees(node_client):
await asyncio.sleep(3)
response = await node_client.get("/node/api/v1/channels")
assert response.status_code == 200
channels = TypeAdapter(list[NodeChannel]).validate_python(response.json())
channels = parse_obj_as(list[NodeChannel], response.json())
ch = random.choice(
[channel for channel in channels if channel.state == ChannelState.ACTIVE]
@@ -146,7 +146,7 @@ async def test_set_channel_fees(node_client):
response = await node_client.get(f"/node/api/v1/channels/{ch.id}")
assert response.status_code == 200
channel = TypeAdapter(NodeChannel).validate_python(response.json())
channel = parse_obj_as(NodeChannel, response.json())
assert channel.fee_ppm == 69
assert channel.fee_base_msat == 42
@@ -162,7 +162,7 @@ async def test_channel_management(node_client):
await asyncio.sleep(3)
response = await node_client.get("/node/api/v1/channels")
assert response.status_code == 200
return TypeAdapter(list[NodeChannel]).validate_python(response.json())
return parse_obj_as(list[NodeChannel], response.json())
data = await get_channels()
close = random.choice(
-111
View File
@@ -1,111 +0,0 @@
import pytest
from pydantic import TypeAdapter, ValidationError
from lnbits.core.models.extensions import InstallableExtension
from lnbits.core.models.lnurl import StoredPayLink
from lnbits.core.models.payments import PaymentFilters
from lnbits.core.models.users import UserLabel
from lnbits.core.models.wallets import (
Wallet,
WalletPermission,
WalletSharePermission,
WalletShareStatus,
)
from lnbits.db import Filter, Page, dict_to_model
from lnbits.nodes.base import NodePayment
def test_user_label_uses_pydantic_v2_pattern_validation():
label = UserLabel(name="label-1", color="#FF00AA")
assert label.name == "label-1"
assert label.color == "#FF00AA"
with pytest.raises(ValidationError):
UserLabel(name="label-1", color="bad-color")
def test_wallet_has_stored_paylinks_field_and_mirrors_it():
source_wallet = Wallet(
id="source-wallet-id",
user="source-user-id",
name="source",
adminkey="admin-key",
inkey="invoice-key",
)
source_wallet.stored_paylinks.links.append(
StoredPayLink(lnurl="lnurl1example", label="saved paylink")
)
shared_wallet = Wallet(
id="shared-wallet-id",
user="shared-user-id",
name="shared",
adminkey="shared-admin-key",
inkey="shared-invoice-key",
)
source_wallet.extra.shared_with.append(
WalletSharePermission(
request_id="share-request-id",
username="shared-user",
shared_with_wallet_id=shared_wallet.id,
permissions=[WalletPermission.VIEW_PAYMENTS],
status=WalletShareStatus.APPROVED,
)
)
shared_wallet.mirror_shared_wallet(source_wallet)
assert shared_wallet.stored_paylinks.links[0].lnurl == "lnurl1example"
assert shared_wallet.stored_paylinks.links[0].label == "saved paylink"
def test_db_dict_to_model_parses_optional_nested_pydantic_v2_models():
ext = dict_to_model(
{
"id": "ext-id",
"name": "Extension",
"version": "1.0.0",
"active": 1,
"meta": (
'{"installed_release": {"name": "Release", "version": "1.0.0", '
'"archive": "https://example.com/release.zip", '
'"source_repo": "lnbits/example"}, "payments": [], '
'"dependencies": [], "featured": false, '
'"has_paid_release": false, "has_free_release": false}'
),
},
InstallableExtension,
)
assert ext.meta is not None
assert ext.meta.installed_release is not None
assert ext.meta.installed_release.source_repo == "lnbits/example"
def test_page_generic_validates_through_pydantic_v2_type_adapter():
page = Page[NodePayment](
data=[
NodePayment(
pending=False,
amount=1,
time=1,
preimage="preimage",
payment_hash="payment-hash",
)
],
total=1,
)
validated = TypeAdapter(Page[NodePayment]).validate_python(
page, from_attributes=True
)
assert validated.total == 1
assert validated.data[0].payment_hash == "payment-hash"
def test_filter_parse_query_uses_pydantic_v2_field_validation():
parsed = Filter.parse_query("amount[eq]", ["42"], PaymentFilters)
assert parsed.field == "amount"
assert parsed.values == {"amount__0": 42}
+140
View File
@@ -0,0 +1,140 @@
from pathlib import Path
from uuid import uuid4
import pytest
from lnbits.core.crud import create_account, delete_account, get_account
from lnbits.core.crud.settings import get_settings_field, set_settings_field
from lnbits.core.db import db
from lnbits.core.models import Account, UpdateSuperuserPassword, UserExtra
from lnbits.core.services.users import check_admin_settings
from lnbits.core.views.auth_api import first_install
from lnbits.settings import settings
async def _restore_setting_field(field_name: str, original_row) -> None:
if original_row is None:
await db.execute(
"DELETE FROM system_settings WHERE id = :id AND tag = :tag",
{"id": field_name, "tag": "core"},
)
return
await set_settings_field(field_name, original_row.value, original_row.tag)
def test_has_first_install_token_changed_requires_a_confirmed_mismatch():
original_token = settings.first_install_token
original_confirmed = settings.first_install_token_confirmed
try:
settings.first_install_token = "new-token"
settings.first_install_token_confirmed = "old-token"
assert settings.has_first_install_token_changed() is True
settings.first_install_token_confirmed = "new-token"
assert settings.has_first_install_token_changed() is False
settings.first_install_token_confirmed = None
assert settings.has_first_install_token_changed() is False
settings.first_install_token = None
assert settings.has_first_install_token_changed() is False
finally:
settings.first_install_token = original_token
settings.first_install_token_confirmed = original_confirmed
@pytest.mark.anyio
async def test_first_install_confirms_first_install_token(app):
temp_super_user = uuid4().hex
username = f"super_{temp_super_user[:8]}"
original_super_user = settings.super_user
original_first_install = settings.first_install
original_first_install_token = settings.first_install_token
original_first_install_token_confirmed = settings.first_install_token_confirmed
original_confirmed_row = await get_settings_field("first_install_token_confirmed")
await create_account(Account(id=temp_super_user, extra=UserExtra(provider="env")))
try:
settings.super_user = temp_super_user
settings.first_install = True
settings.first_install_token = "expected-token"
settings.first_install_token_confirmed = None
response = await first_install(
UpdateSuperuserPassword(
username=username,
password="secret1234",
password_repeat="secret1234",
first_install_token="expected-token",
)
)
assert response.status_code == 200
assert settings.first_install is False
assert settings.first_install_token_confirmed == "expected-token"
confirmed_row = await get_settings_field("first_install_token_confirmed")
assert confirmed_row is not None
assert confirmed_row.value == "expected-token"
account = await get_account(temp_super_user)
assert account is not None
assert account.username == username
assert account.extra.provider == "lnbits"
assert account.verify_password("secret1234")
finally:
await _restore_setting_field(
"first_install_token_confirmed", original_confirmed_row
)
settings.super_user = original_super_user
settings.first_install = original_first_install
settings.first_install_token = original_first_install_token
settings.first_install_token_confirmed = original_first_install_token_confirmed
await delete_account(temp_super_user)
@pytest.mark.anyio
async def test_check_admin_settings_clears_persisted_super_user_when_token_changes(app):
temp_super_user = uuid4().hex
original_super_user = settings.super_user
original_first_install = settings.first_install
original_first_install_token = settings.first_install_token
original_first_install_token_confirmed = settings.first_install_token_confirmed
original_super_user_row = await get_settings_field("super_user")
original_confirmed_row = await get_settings_field("first_install_token_confirmed")
super_user_file = Path(settings.lnbits_data_folder) / ".super_user"
await create_account(
Account(id=temp_super_user, extra=UserExtra(provider="lnbits"))
)
try:
await set_settings_field("super_user", temp_super_user)
await set_settings_field("first_install_token_confirmed", "old-token")
settings.lnbits_admin_ui = True
settings.super_user = temp_super_user
settings.first_install = False
settings.first_install_token = "new-token"
settings.first_install_token_confirmed = "old-token"
await check_admin_settings()
super_user_row = await get_settings_field("super_user")
assert super_user_row is not None
assert super_user_row.value
assert settings.first_install is True
finally:
await _restore_setting_field("super_user", original_super_user_row)
await _restore_setting_field(
"first_install_token_confirmed", original_confirmed_row
)
settings.super_user = original_super_user
settings.first_install = original_first_install
settings.first_install_token = original_first_install_token
settings.first_install_token_confirmed = original_first_install_token_confirmed
super_user_file.write_text(original_super_user)
await delete_account(temp_super_user)
+1 -59
View File
@@ -1,7 +1,6 @@
import pytest
from pydantic import ValidationError
from lnbits.settings import EditableSettings, RedirectPath, Settings, UpdateSettings
from lnbits.settings import RedirectPath
lnurlp_redirect_path = {
"from_path": "/.well-known/lnurlp",
@@ -167,60 +166,3 @@ def test_redirect_path_new_path_from(lnurlp: RedirectPath):
lnurlp.new_path_from("/.well-known/lnurlp/path/more")
== "/lnurlp/api/v1/well-known/path/more"
)
def test_settings_loads_env_and_dotenv_values(tmp_path, monkeypatch):
env_file = tmp_path / ".env"
env_file.write_text(
"\n".join(
[
"lnbits_admin_users=alice,bob",
"STRIKE_API_ENDPOINT=https://dotenv.strike.example",
]
),
encoding="utf-8",
)
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("LNBITS_ALLOWED_USERS", ' ["carol", "dave"] ')
monkeypatch.setenv("STRIKE_API_KEY", "secret-key")
monkeypatch.setenv("STRIKE_API_ENDPOINT", "https://env.strike.example")
settings = Settings()
assert settings.lnbits_admin_users == ["alice", "bob"]
assert settings.lnbits_allowed_users == ["carol", "dave"]
assert settings.strike_api_endpoint == "https://env.strike.example"
assert settings.strike_api_key == "secret-key"
def test_editable_settings_from_dict_ignores_unknown_fields():
editable_settings = EditableSettings.from_dict(
{"lnbits_admin_users": ["alice"], "unknown_setting": "ignored"}
)
assert editable_settings.lnbits_admin_users == ["alice"]
assert "unknown_setting" not in editable_settings.model_dump()
def test_update_settings_splits_string_lists_and_forbids_extra_fields():
updated = UpdateSettings.model_validate(
{
"lnbits_admin_users": "alice,bob",
"strike_api_endpoint": "https://api.strike.me/v1",
"strike_api_key": "secret-key",
}
)
assert updated.lnbits_admin_users == ["alice", "bob"]
assert updated.strike_api_endpoint == "https://api.strike.me/v1"
assert updated.strike_api_key == "secret-key"
with pytest.raises(ValidationError):
UpdateSettings.model_validate({"unknown_setting": True})
def test_update_settings_schema_does_not_include_env_names():
schema = UpdateSettings.model_json_schema()
assert "properties" in schema
assert all("env_names" not in prop for prop in schema["properties"].values())
+404 -4
View File
@@ -49,6 +49,14 @@
"phoenixd_api_password": "f171ba022a764e679eef950b21fb1c04f171ba022a764e679eef950b21fb1c04",
"user_agent": "LNbits/Tests"
}
},
"sparkl2": {
"wallet_class": "SparkL2Wallet",
"settings": {
"spark_l2_external_endpoint": "http://127.0.0.1:8555",
"spark_l2_external_api_key": "mock-spark-l2-api-key",
"user_agent": "LNbits/Tests"
}
}
},
"functions": {
@@ -115,6 +123,16 @@
},
"method": "GET"
}
},
"sparkl2": {
"status_endpoint": {
"uri": "/v1/balance",
"headers": {
"X-Api-Key": "mock-spark-l2-api-key",
"User-Agent": "LNbits/Tests"
},
"method": "POST"
}
}
},
"tests": [
@@ -190,6 +208,16 @@
}
}
]
},
"sparkl2": {
"status_endpoint": [
{
"response_type": "json",
"response": {
"balance_sats": 55
}
}
]
}
}
},
@@ -255,6 +283,16 @@
"response": "test-error"
}
]
},
"sparkl2": {
"status_endpoint": [
{
"response_type": "json",
"response": {
"error": "\"test-error\""
}
}
]
}
}
},
@@ -313,6 +351,14 @@
"response": {}
}
]
},
"sparkl2": {
"status_endpoint": [
{
"response_type": "json",
"response": {}
}
]
}
}
},
@@ -371,6 +417,14 @@
"response": "data-not-json"
}
]
},
"sparkl2": {
"status_endpoint": [
{
"response_type": "data",
"response": "data-not-json"
}
]
}
}
},
@@ -447,6 +501,17 @@
}
}
]
},
"sparkl2": {
"status_endpoint": [
{
"response_type": "response",
"response": {
"response": "Not Found",
"status": 404
}
}
]
}
}
},
@@ -523,6 +588,16 @@
},
"method": "POST"
}
},
"sparkl2": {
"create_invoice_endpoint": {
"uri": "/v1/invoices",
"headers": {
"X-Api-Key": "mock-spark-l2-api-key",
"User-Agent": "LNbits/Tests"
},
"method": "POST"
}
}
},
"tests": [
@@ -638,6 +713,24 @@
}
}
]
},
"sparkl2": {
"create_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"amount_sats": 555,
"memo": "Test Invoice",
"description_hash": null,
"expiry_seconds": null
},
"response_type": "json",
"response": {
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"payment_request": "lnbc5550n1pnq9jg3sp52rvwstvjcypjsaenzdh0h30jazvzsf8aaye0julprtth9kysxtuspp5e5s3z7felv4t9zrcc6wpn7ehvjl5yzewanzl5crljdl3jgeffyhqdq2f38xy6t5wvxqzjccqpjrzjq0yzeq76ney45hmjlnlpvu0nakzy2g35hqh0dujq8ujdpr2e42pf2rrs6vqpgcsqqqqqqqqqqqqqqeqqyg9qxpqysgqwftcx89k5pp28435pgxfl2vx3ksemzxccppw2j9yjn0ngr6ed7wj8ztc0d5kmt2mvzdlcgrludhz7jncd5l5l9w820hc4clpwhtqj3gq62g66n"
}
}
]
}
}
},
@@ -735,6 +828,23 @@
}
}
]
},
"sparkl2": {
"create_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"amount_sats": 555,
"memo": "Test Invoice",
"description_hash": null,
"expiry_seconds": null
},
"response_type": "json",
"response": {
"error": "Test Error"
}
}
]
}
}
},
@@ -845,6 +955,21 @@
}
}
]
},
"sparkl2": {
"create_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"amount_sats": 555,
"memo": "Test Invoice",
"description_hash": null,
"expiry_seconds": null
},
"response_type": "json",
"response": {}
}
]
}
}
},
@@ -942,6 +1067,21 @@
"response": "data-not-json"
}
]
},
"sparkl2": {
"create_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"amount_sats": 555,
"memo": "Test Invoice",
"description_hash": null,
"expiry_seconds": null
},
"response_type": "data",
"response": "data-not-json"
}
]
}
}
},
@@ -1057,6 +1197,24 @@
}
}
]
},
"sparkl2": {
"create_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"amount_sats": 555,
"memo": "Test Invoice",
"description_hash": null,
"expiry_seconds": null
},
"response_type": "response",
"response": {
"response": "Not Found",
"status": 404
}
}
]
}
}
},
@@ -1155,6 +1313,16 @@
},
"method": "POST"
}
},
"sparkl2": {
"pay_invoice_endpoint": {
"uri": "/v1/payments",
"headers": {
"X-Api-Key": "mock-spark-l2-api-key",
"User-Agent": "LNbits/Tests"
},
"method": "POST"
}
}
},
"tests": [
@@ -1309,6 +1477,24 @@
}
}
]
},
"sparkl2": {
"pay_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"max_fee_sats": 25,
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"response_type": "json",
"response": {
"status": "LIGHTNING_PAYMENT_SUCCEEDED",
"fee_msat": 30000,
"preimage": "0000000000000000000000000000000000000000000000000000000000000000"
}
}
]
}
}
},
@@ -1361,7 +1547,23 @@
"alby": {},
"eclair": [],
"lnbits": [],
"phoenixd": []
"phoenixd": [],
"sparkl2": {
"pay_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"max_fee_sats": 25,
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"response_type": "json",
"response": {
"status": "LIGHTNING_PAYMENT_FAILED"
}
}
]
}
}
},
{
@@ -1505,7 +1707,24 @@
}
],
"lnbits": [],
"phoenixd": []
"phoenixd": [],
"sparkl2": {
"pay_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"max_fee_sats": 25,
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"response_type": "json",
"response": {
"status": "PAYMENT_PENDING",
"preimage": "0000000000000000000000000000000000000000000000000000000000000000"
}
}
]
}
}
},
{
@@ -1598,6 +1817,22 @@
}
}
]
},
"sparkl2": {
"pay_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"max_fee_sats": 25,
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"response_type": "json",
"response": {
"error": "Test Error"
}
}
]
}
}
},
@@ -1695,6 +1930,20 @@
}
}
]
},
"sparkl2": {
"pay_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"max_fee_sats": 25,
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"response_type": "json",
"response": {}
}
]
}
}
},
@@ -1813,6 +2062,20 @@
"response": "data-not-json"
}
]
},
"sparkl2": {
"pay_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"max_fee_sats": 25,
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"response_type": "data",
"response": "data-not-json"
}
]
}
}
},
@@ -1943,6 +2206,23 @@
}
}
]
},
"sparkl2": {
"pay_invoice_endpoint": [
{
"request_type": "json",
"request_body": {
"bolt11": "lnbc210n1pjlgal5sp5xr3uwlfm7ltumdjyukhys0z2rw6grgm8me9k4w9vn05zt9svzzjspp5ud2jdfpaqn5c2k2vphatsjypfafyk8rcvkvwexnrhmwm94ex4jtqdqu24hxjapq23jhxapqf9h8vmmfvdjscqpjrzjqta942048v7qxh5x7pxwplhmtwfl0f25cq23jh87rhx7lgrwwvv86r90guqqnwgqqqqqqqqqqqqqqpsqyg9qxpqysgqylngsyg960lltngzy90e8n22v4j2hvjs4l4ttuy79qqefjv8q87q9ft7uhwdjakvnsgk44qyhalv6ust54x98whl3q635hkwgsyw8xgqjl7jwu",
"max_fee_sats": 25,
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
},
"response_type": "response",
"response": {
"response": "Not Found",
"status": 404
}
}
]
}
}
},
@@ -2030,6 +2310,16 @@
},
"method": "GET"
}
},
"sparkl2": {
"get_invoice_status_endpoint": {
"uri": "/v1/invoices/e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"headers": {
"X-Api-Key": "mock-spark-l2-api-key",
"User-Agent": "LNbits/Tests"
},
"method": "GET"
}
}
},
"tests": [
@@ -2125,6 +2415,24 @@
}
}
]
},
"sparkl2": {
"get_invoice_status_endpoint": [
{
"description": "LIGHTNING_PAYMENT_RECEIVED",
"response_type": "json",
"response": {
"status": "LIGHTNING_PAYMENT_RECEIVED"
}
},
{
"description": "TRANSFER_COMPLETED",
"response_type": "json",
"response": {
"status": "TRANSFER_COMPLETED"
}
}
]
}
}
},
@@ -2193,7 +2501,8 @@
}
}
]
}
},
"sparkl2": {}
}
},
{
@@ -2400,6 +2709,35 @@
"response": "data-not-json"
}
]
},
"sparkl2": {
"get_invoice_status_endpoint": [
{
"description": "no data",
"response_type": "json",
"response": {}
},
{
"description": "pending status",
"response_type": "json",
"response": {
"status": "PAYMENT_PENDING"
}
},
{
"description": "bad json",
"response_type": "data",
"response": "data-not-json"
},
{
"description": "http 404",
"response_type": "response",
"response": {
"response": "Not Found",
"status": 404
}
}
]
}
}
},
@@ -2482,6 +2820,16 @@
},
"method": "GET"
}
},
"sparkl2": {
"get_payment_status_endpoint": {
"uri": "/v1/payments/e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"headers": {
"X-Api-Key": "mock-spark-l2-api-key",
"User-Agent": "LNbits/Tests"
},
"method": "GET"
}
}
},
"tests": [
@@ -2587,6 +2935,28 @@
}
}
]
},
"sparkl2": {
"get_payment_status_endpoint": [
{
"description": "LIGHTNING_PAYMENT_SUCCEEDED",
"response_type": "json",
"response": {
"status": "LIGHTNING_PAYMENT_SUCCEEDED",
"fee_msat": 1000,
"preimage": "0000000000000000000000000000000000000000000000000000000000000000"
}
},
{
"description": "TRANSFER_COMPLETED",
"response_type": "json",
"response": {
"status": "TRANSFER_COMPLETED",
"fee_msat": 1000,
"preimage": "0000000000000000000000000000000000000000000000000000000000000000"
}
}
]
}
}
},
@@ -2675,7 +3045,8 @@
}
}
]
}
},
"sparkl2": {}
}
},
{
@@ -2933,6 +3304,35 @@
"response": "data-not-json"
}
]
},
"sparkl2": {
"get_payment_status_endpoint": [
{
"description": "pending status",
"response_type": "json",
"response": {
"status": "PAYMENT_PENDING"
}
},
{
"description": "no data",
"response_type": "json",
"response": {}
},
{
"description": "bad json",
"response_type": "data",
"response": "data-not-json"
},
{
"description": "http 404",
"response_type": "response",
"response": {
"response": "Not Found",
"status": 404
}
}
]
}
}
},
+1 -1
View File
@@ -1,4 +1,4 @@
from pydantic.v1 import BaseModel
from pydantic import BaseModel
class FundingSourceConfig(BaseModel):
File diff suppressed because it is too large Load Diff
Generated
+43 -147
View File
@@ -111,15 +111,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
]
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
name = "anyio"
version = "4.12.1"
@@ -796,23 +787,21 @@ wheels = [
[[package]]
name = "fastapi"
version = "0.135.2"
version = "0.116.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c4/73/5903c4b13beae98618d64eb9870c3fac4f605523dd0312ca5c80dadbd5b9/fastapi-0.135.2.tar.gz", hash = "sha256:88a832095359755527b7f63bb4c6bc9edb8329a026189eed83d6c1afcf419d56", size = 395833, upload-time = "2026-03-23T14:12:41.697Z" }
sdist = { url = "https://files.pythonhosted.org/packages/01/64/1296f46d6b9e3b23fb22e5d01af3f104ef411425531376212f1eefa2794d/fastapi-0.116.2.tar.gz", hash = "sha256:231a6af2fe21cfa2c32730170ad8514985fc250bec16c9b242d3b94c835ef529", size = 298595, upload-time = "2025-09-16T18:29:23.058Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/ea/18f6d0457f9efb2fc6fa594857f92810cadb03024975726db6546b3d6fcf/fastapi-0.135.2-py3-none-any.whl", hash = "sha256:0af0447d541867e8db2a6a25c23a8c4bd80e2394ac5529bd87501bbb9e240ca5", size = 117407, upload-time = "2026-03-23T14:12:43.284Z" },
{ url = "https://files.pythonhosted.org/packages/32/e4/c543271a8018874b7f682bf6156863c416e1334b8ed3e51a69495c5d4360/fastapi-0.116.2-py3-none-any.whl", hash = "sha256:c3a7a8fb830b05f7e087d920e0d786ca1fc9892eb4e9a84b227be4c1bc7569db", size = 95670, upload-time = "2025-09-16T18:29:21.329Z" },
]
[[package]]
name = "fastapi-sso"
version = "0.21.0"
version = "0.19.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "fastapi" },
@@ -821,9 +810,9 @@ dependencies = [
{ name = "pydantic", extra = ["email"] },
{ name = "pyjwt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/96/0a/81e67a78dd6562732cde9e68c9a483516d9de5bd2d5454ac465b66b4e97c/fastapi_sso-0.21.0.tar.gz", hash = "sha256:5fa596e28abb3c9d14567df8a2757f13a0fe4be8a2701fbf6853cf0cd297470b", size = 18029, upload-time = "2026-02-24T18:46:47.041Z" }
sdist = { url = "https://files.pythonhosted.org/packages/74/fc/644bc8f82fc887fffcf9a3eab8eb3dea06ee9ea160ef20441455ed7a0001/fastapi_sso-0.19.0.tar.gz", hash = "sha256:629f00581f72ea7e57f7b8775f8d2c425629c428c194359a2b4ebaa6bcb8e12b", size = 17278, upload-time = "2025-12-17T15:18:06.721Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/4f/22d8cfe4ac207b4718dfdd7a1fea0acdcdb61be2d546bde905ca6ce065a4/fastapi_sso-0.21.0-py3-none-any.whl", hash = "sha256:30e631277050c16e0eb03eb228867b586631e2f5155cc5574e27e28df73356ff", size = 29219, upload-time = "2026-02-24T18:46:47.821Z" },
{ url = "https://files.pythonhosted.org/packages/d7/61/d2ac0800cb1f390ec2aad464ea4cc5944de285fabc8f987327da4633f17d/fastapi_sso-0.19.0-py3-none-any.whl", hash = "sha256:d958c46cd9996234c7b162e192168b4c0807a248224a55b0f877d3a82a16a930", size = 26419, upload-time = "2025-12-17T15:18:05.439Z" },
]
[[package]]
@@ -1274,7 +1263,7 @@ wheels = [
[[package]]
name = "lnbits"
version = "1.5.2"
version = "1.5.3"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },
@@ -1301,7 +1290,6 @@ dependencies = [
{ name = "protobuf" },
{ name = "pycryptodomex" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "pyjwt" },
{ name = "pyln-client" },
{ name = "pynostr" },
@@ -1368,8 +1356,8 @@ requires-dist = [
{ name = "breez-sdk-liquid", marker = "extra == 'breez'", specifier = "~=0.11.11" },
{ name = "click", specifier = "~=8.3.1" },
{ name = "embit", specifier = "~=0.8.0" },
{ name = "fastapi", specifier = "~=0.135.0" },
{ name = "fastapi-sso", specifier = "~=0.21.0" },
{ name = "fastapi", specifier = "~=0.116.1" },
{ name = "fastapi-sso", specifier = "~=0.19.0" },
{ name = "filetype", specifier = "~=1.2.0" },
{ name = "greenlet", specifier = "~=3.3.0" },
{ name = "grpcio", specifier = "~=1.76.0" },
@@ -1377,7 +1365,7 @@ requires-dist = [
{ name = "itsdangerous", specifier = "~=2.2.0" },
{ name = "jinja2", specifier = "~=3.1.6" },
{ name = "jsonpath-ng", specifier = "~=1.7.0" },
{ name = "lnurl", specifier = "~=2.0.0" },
{ name = "lnurl", specifier = "~=0.10.0" },
{ name = "loguru", specifier = "~=0.7.3" },
{ name = "nostr-sdk", specifier = "~=0.44.0" },
{ name = "packaging", specifier = "~=25.0.0" },
@@ -1385,8 +1373,7 @@ requires-dist = [
{ name = "protobuf", specifier = "~=6.33.5" },
{ name = "psycopg2-binary", marker = "extra == 'migration'", specifier = "~=2.9.11" },
{ name = "pycryptodomex", specifier = "~=3.23.0" },
{ name = "pydantic", specifier = "~=2.12.0" },
{ name = "pydantic-settings", specifier = ">=2.13.1" },
{ name = "pydantic", specifier = "~=1.10.26" },
{ name = "pyjwt", specifier = "~=2.12.0" },
{ name = "pyln-client", specifier = "~=25.12.0" },
{ name = "pynostr", specifier = "~=0.7.0" },
@@ -1398,8 +1385,8 @@ requires-dist = [
{ name = "shortuuid", specifier = "~=1.0.13" },
{ name = "slowapi", specifier = "~=0.1.9" },
{ name = "sqlalchemy", specifier = "~=1.4.54" },
{ name = "sse-starlette", specifier = "~=3.3.3" },
{ name = "starlette", specifier = "~=1.0.0" },
{ name = "sse-starlette", specifier = "~=2.3.6" },
{ name = "starlette", specifier = "~=0.47.1" },
{ name = "typing-extensions", specifier = "~=4.15.0" },
{ name = "uvicorn", specifier = "~=0.40.0" },
{ name = "uvloop", specifier = "~=0.22.1" },
@@ -1434,7 +1421,7 @@ dev = [
[[package]]
name = "lnurl"
version = "2.0.0"
version = "0.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bech32" },
@@ -1445,9 +1432,9 @@ dependencies = [
{ name = "pycryptodomex" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/75/1f/ba20c0754ae29bc6f53439649a5295640e09ca189741911e215338b7aae3/lnurl-2.0.0.tar.gz", hash = "sha256:4ac17e289eb3818f217b84b41d455f27d0161b300560f6bddd345b923b5ba82d", size = 75603, upload-time = "2026-03-25T07:18:31.473Z" }
sdist = { url = "https://files.pythonhosted.org/packages/25/cc/e4849752e88c6d346de52eb14f4bd2860e8276dcbc657536211d38e0a2e0/lnurl-0.10.0.tar.gz", hash = "sha256:55ba0a7e810bcdfc7410c682f91c76df4446ff24e83d6bf931a7c759e2e0d5cc", size = 67646, upload-time = "2026-03-20T12:42:10.879Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/23/86/fd25c848d71b0a18ab95b16e4c718364ac1f1b691fc7ba2334da4ff547c0/lnurl-2.0.0-py3-none-any.whl", hash = "sha256:e355f7433aa5ba31557ffbfd042978e8e6f9df90aaae907ebeb613f06ff4f9f4", size = 17979, upload-time = "2026-03-25T07:18:32.35Z" },
{ url = "https://files.pythonhosted.org/packages/99/88/b8abeed9844a71ad91c10b3046d79afc8de1da1b8e74002090b54c1e1d7d/lnurl-0.10.0-py3-none-any.whl", hash = "sha256:0d43561b4370cecec19c8f1044fc840f0199fc2698bc83cd8782e8264bfde8f4", size = 17375, upload-time = "2026-03-20T12:42:09.872Z" },
]
[[package]]
@@ -2003,17 +1990,29 @@ wheels = [
[[package]]
name = "pydantic"
version = "2.12.5"
version = "1.10.26"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7b/da/fd89f987a376c807cd81ea0eff4589aade783bbb702637b4734ef2c743a2/pydantic-1.10.26.tar.gz", hash = "sha256:8c6aa39b494c5af092e690127c283d84f363ac36017106a9e66cb33a22ac412e", size = 357906, upload-time = "2025-12-18T15:47:46.557Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
{ url = "https://files.pythonhosted.org/packages/71/08/2587a6d4314e7539eec84acd062cb7b037638edb57a0335d20e4c5b8878c/pydantic-1.10.26-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f7ae36fa0ecef8d39884120f212e16c06bb096a38f523421278e2f39c1784546", size = 2444588, upload-time = "2025-12-18T15:46:28.882Z" },
{ url = "https://files.pythonhosted.org/packages/47/e6/10df5f08c105bcbb4adbee7d1108ff4b347702b110fed058f6a03f1c6b73/pydantic-1.10.26-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d95a76cf503f0f72ed7812a91de948440b2bf564269975738a4751e4fadeb572", size = 2255972, upload-time = "2025-12-18T15:46:31.72Z" },
{ url = "https://files.pythonhosted.org/packages/ba/7d/fdb961e7adc2c31f394feba6f560ef2c74c446f0285e2c2eb87d2b7206c7/pydantic-1.10.26-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a943ce8e00ad708ed06a1d9df5b4fd28f5635a003b82a4908ece6f24c0b18464", size = 2857175, upload-time = "2025-12-18T15:46:34Z" },
{ url = "https://files.pythonhosted.org/packages/8f/6c/f21e27dda475d4c562bd01b5874284dd3180f336c1e669413b743ca8b278/pydantic-1.10.26-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:465ad8edb29b15c10b779b16431fe8e77c380098badf6db367b7a1d3e572cf53", size = 2947001, upload-time = "2025-12-18T15:46:35.922Z" },
{ url = "https://files.pythonhosted.org/packages/6d/f6/27ea206232cbb6ec24dc4e4e8888a9a734f96a1eaf13504be4b30ef26aa7/pydantic-1.10.26-cp310-cp310-win_amd64.whl", hash = "sha256:80e6be6272839c8a7641d26ad569ab77772809dd78f91d0068dc0fc97f071945", size = 2066217, upload-time = "2025-12-18T15:46:37.614Z" },
{ url = "https://files.pythonhosted.org/packages/1d/c1/d521e64c8130e1ad9d22c270bed3fabcc0940c9539b076b639c88fd32a8d/pydantic-1.10.26-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:116233e53889bcc536f617e38c1b8337d7fa9c280f0fd7a4045947515a785637", size = 2428347, upload-time = "2025-12-18T15:46:39.41Z" },
{ url = "https://files.pythonhosted.org/packages/2c/08/f4b804a00c16e3ea994cb640a7c25c579b4f1fa674cde6a19fa0dfb0ae4f/pydantic-1.10.26-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3cfdd361addb6eb64ccd26ac356ad6514cee06a61ab26b27e16b5ed53108f77", size = 2212605, upload-time = "2025-12-18T15:46:41.006Z" },
{ url = "https://files.pythonhosted.org/packages/5d/78/0df4b9efef29bbc5e39f247fcba99060d15946b4463d82a5589cf7923d71/pydantic-1.10.26-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e4451951a9a93bf9a90576f3e25240b47ee49ab5236adccb8eff6ac943adf0f", size = 2753560, upload-time = "2025-12-18T15:46:43.215Z" },
{ url = "https://files.pythonhosted.org/packages/68/66/6ab6c1d3a116d05d2508fce64f96e35242938fac07544d611e11d0d363a0/pydantic-1.10.26-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9858ed44c6bea5f29ffe95308db9e62060791c877766c67dd5f55d072c8612b5", size = 2859235, upload-time = "2025-12-18T15:46:45.112Z" },
{ url = "https://files.pythonhosted.org/packages/61/4e/f1676bb0fcdf6ed2ce4670d7d1fc1d6c3a06d84497644acfbe02649503f1/pydantic-1.10.26-cp311-cp311-win_amd64.whl", hash = "sha256:ac1089f723e2106ebde434377d31239e00870a7563245072968e5af5cc4d33df", size = 2066646, upload-time = "2025-12-18T15:46:46.816Z" },
{ url = "https://files.pythonhosted.org/packages/02/6c/cd97a5a776c4515e6ee2ae81c2f2c5be51376dda6c31f965d7746ce0019f/pydantic-1.10.26-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:468d5b9cacfcaadc76ed0a4645354ab6f263ec01a63fb6d05630ea1df6ae453f", size = 2433795, upload-time = "2025-12-18T15:46:49.321Z" },
{ url = "https://files.pythonhosted.org/packages/47/12/de20affa30dcef728fcf9cc98e13ff4438c7a630de8d2f90eb38eba0891c/pydantic-1.10.26-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2c1b0b914be31671000ca25cf7ea17fcaaa68cfeadf6924529c5c5aa24b7ab1f", size = 2227387, upload-time = "2025-12-18T15:46:50.877Z" },
{ url = "https://files.pythonhosted.org/packages/7b/1d/9d65dcc5b8c17ba590f1f9f486e9306346831902318b7ee93f63516f4003/pydantic-1.10.26-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15b13b9f8ba8867095769e1156e0d7fbafa1f65b898dd40fd1c02e34430973cb", size = 2629594, upload-time = "2025-12-18T15:46:53.42Z" },
{ url = "https://files.pythonhosted.org/packages/3f/76/acb41409356789e23e1a7ef58f93821410c96409183ce314ddb58d97f23e/pydantic-1.10.26-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad7025ca324ae263d4313998e25078dcaec5f9ed0392c06dedb57e053cc8086b", size = 2745305, upload-time = "2025-12-18T15:46:55.987Z" },
{ url = "https://files.pythonhosted.org/packages/22/72/a98c0c5e527a66057d969fedd61675223c7975ade61acebbca9f1abd6dc0/pydantic-1.10.26-cp312-cp312-win_amd64.whl", hash = "sha256:4482b299874dabb88a6c3759e3d85c6557c407c3b586891f7d808d8a38b66b9c", size = 1937647, upload-time = "2025-12-18T15:46:57.905Z" },
{ url = "https://files.pythonhosted.org/packages/1f/98/556e82f00b98486def0b8af85da95e69d2be7e367cf2431408e108bc3095/pydantic-1.10.26-py3-none-any.whl", hash = "sha256:c43ad70dc3ce7787543d563792426a16fd7895e14be4b194b5665e36459dd917", size = 166975, upload-time = "2025-12-18T15:47:44.927Z" },
]
[package.optional-dependencies]
@@ -2021,96 +2020,6 @@ email = [
{ name = "email-validator" },
]
[[package]]
name = "pydantic-core"
version = "2.41.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" },
{ url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" },
{ url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" },
{ url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" },
{ url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" },
{ url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" },
{ url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" },
{ url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" },
{ url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" },
{ url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" },
{ url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" },
{ url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" },
{ url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" },
{ url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
{ url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
{ url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
{ url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
{ url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
{ url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
{ url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
{ url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
{ url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
{ url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
{ url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
{ url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
{ url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
{ url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
{ url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
{ url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
{ url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
{ url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
{ url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
{ url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
{ url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
{ url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
{ url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
{ url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
{ url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
{ url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
{ url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
{ url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
{ url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
{ url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
{ url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
{ url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
{ url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
{ url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
{ url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
{ url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
{ url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" },
{ url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" },
{ url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" },
{ url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" },
{ url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" },
{ url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" },
{ url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" },
{ url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" },
{ url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
{ url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
{ url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
{ url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
{ url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
{ url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
{ url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
]
[[package]]
name = "pydantic-settings"
version = "2.13.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
@@ -2403,7 +2312,7 @@ wheels = [
[[package]]
name = "requests"
version = "2.32.5"
version = "2.33.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@@ -2411,9 +2320,9 @@ dependencies = [
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
{ url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" },
]
[[package]]
@@ -2618,28 +2527,27 @@ wheels = [
[[package]]
name = "sse-starlette"
version = "3.3.3"
version = "2.3.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "starlette" },
]
sdist = { url = "https://files.pythonhosted.org/packages/14/2f/9223c24f568bb7a0c03d751e609844dce0968f13b39a3f73fbb3a96cd27a/sse_starlette-3.3.3.tar.gz", hash = "sha256:72a95d7575fd5129bd0ae15275ac6432bb35ac542fdebb82889c24bb9f3f4049", size = 32420, upload-time = "2026-03-17T20:05:55.529Z" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/f4/989bc70cb8091eda43a9034ef969b25145291f3601703b82766e5172dfed/sse_starlette-2.3.6.tar.gz", hash = "sha256:0382336f7d4ec30160cf9ca0518962905e1b69b72d6c1c995131e0a703b436e3", size = 18284, upload-time = "2025-05-30T13:34:12.914Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/e2/b8cff57a67dddf9a464d7e943218e031617fb3ddc133aeeb0602ff5f6c85/sse_starlette-3.3.3-py3-none-any.whl", hash = "sha256:c5abb5082a1cc1c6294d89c5290c46b5f67808cfdb612b7ec27e8ba061c22e8d", size = 14329, upload-time = "2026-03-17T20:05:54.35Z" },
{ url = "https://files.pythonhosted.org/packages/81/05/78850ac6e79af5b9508f8841b0f26aa9fd329a1ba00bf65453c2d312bcc8/sse_starlette-2.3.6-py3-none-any.whl", hash = "sha256:d49a8285b182f6e2228e2609c350398b2ca2c36216c2675d875f81e93548f760", size = 10606, upload-time = "2025-05-30T13:34:11.703Z" },
]
[[package]]
name = "starlette"
version = "1.0.0"
version = "0.47.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" }
sdist = { url = "https://files.pythonhosted.org/packages/15/b9/cc3017f9a9c9b6e27c5106cc10cc7904653c3eec0729793aec10479dd669/starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9", size = 2584144, upload-time = "2025-08-24T13:36:42.122Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
{ url = "https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51", size = 72991, upload-time = "2025-08-24T13:36:40.887Z" },
]
[[package]]
@@ -2778,18 +2686,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
name = "urllib3"
version = "2.6.3"