diff --git a/README.md b/README.md index 7ecb08f..4e004c5 100755 --- a/README.md +++ b/README.md @@ -69,11 +69,14 @@ lnurl.bech32 # "LNURL1DP68GURN8GHJ7UM9WFMXJCM99E5K7TELWY7NXENRXVMRGDTZXSENJCM98 lnurl.bech32.hrp # "lnurl" lnurl.url # "https://service.io/?q=3fc3645b439ce8e7" lnurl.url.host # "service.io" -lnurl.url.base # "https://service.io/" lnurl.url.query # "q=3fc3645b439ce8e7" -lnurl.url.query_params # {"q": "3fc3645b439ce8e7"} +lnurl.url.query_params() # [("q", "3fc3645b439ce8e7")] +dict(lnurl.url.query_params()) # {"q": "3fc3645b439ce8e7"} ``` +`query_params()` returns a list of `(key, value)` tuples so query parameters stay lossless and ordered. +Use `dict(...)` when you want convenient mapping-style access and duplicate keys do not matter. + Parsing LNURL responses ----------------------- @@ -94,8 +97,9 @@ try: res.ok # bool res.maxSendable # int res.max_sats # int - res.callback.base # str - res.callback.query_params # dict + res.callback.host # str + res.callback.query_params() # list[tuple] [("amount", "1000")] + dict(res.callback.query_params()) # dict res.metadata # str res.metadata.list() # list res.metadata.text # str @@ -125,10 +129,10 @@ For LNURL services, the `lnurl` package can be used to build **valid** responses ```python from lnurl import CallbackUrl, LnurlWithdrawResponse, MilliSatoshi -from pydantic import parse_obj_as, ValidationError +from pydantic import TypeAdapter, ValidationError try: res = LnurlWithdrawResponse( - callback=parse_obj_as(CallbackUrl, "https://lnurl.bigsun.xyz/lnurl-withdraw/callback/9702808"), + callback=TypeAdapter(CallbackUrl).validate_python("https://lnurl.bigsun.xyz/lnurl-withdraw/callback/9702808"), k1="38d304051c1b76dcd8c5ee17ee15ff0ebc02090c0afbc6c98100adfa3f920874", minWithdrawable=MilliSatoshi(1000), maxWithdrawable=MilliSatoshi(1000000), @@ -144,8 +148,7 @@ All responses are `pydantic` models, so the information you provide will be vali access to `.json()` and `.dict()` methods to export the data. **Data is exported using :camel: camelCase keys by default, as per spec.** -You can also use camelCases when you parse the data, and it will be converted to snake_case to make your -Python code nicer. +Use the LNURL spec field names when parsing and exporting response models. Will throw and ValidationError if the data is not valid, so you can catch it and return an error response. diff --git a/lnurl/__init__.py b/lnurl/__init__.py index 4ad23e3..545f0d4 100644 --- a/lnurl/__init__.py +++ b/lnurl/__init__.py @@ -1,6 +1,3 @@ -# backward compatibility, MilliSatoshi is now imported from bolt11 -from bolt11 import MilliSatoshi - from .core import decode, encode, execute, execute_login, execute_pay_request, execute_withdraw, get, handle from .exceptions import ( InvalidLnurl, @@ -63,6 +60,7 @@ LnurlResponseTag, LnurlStatus, Max144Str, + MilliSatoshi, Url, ) diff --git a/lnurl/core.py b/lnurl/core.py index 815e4f3..5e15af5 100644 --- a/lnurl/core.py +++ b/lnurl/core.py @@ -4,7 +4,7 @@ import httpx from bolt11 import Bolt11Exception, MilliSatoshi from bolt11 import decode as bolt11_decode -from pydantic import ValidationError, parse_obj_as +from pydantic import TypeAdapter, ValidationError from .exceptions import InvalidLnurl, InvalidUrl, LnurlResponseException from .helpers import ( @@ -23,7 +23,7 @@ LnurlSuccessResponse, LnurlWithdrawResponse, ) -from .types import CallbackUrl, LnAddress, Lnurl +from .types import CallbackUrl, LnAddress, Lnurl, Url USER_AGENT = "lnbits/lnurl" TIMEOUT = 5 @@ -44,16 +44,17 @@ def encode(url: str) -> Lnurl: async def get( - url: str, + url: str | Url | CallbackUrl, *, response_class: Optional[Any] = None, user_agent: Optional[str] = None, timeout: Optional[int] = None, ) -> LnurlResponseModel: + request_url = str(url) headers = {"User-Agent": user_agent or USER_AGENT} async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client: try: - res = await client.get(url, timeout=timeout or TIMEOUT) + res = await client.get(request_url, timeout=timeout or TIMEOUT) res.raise_for_status() except Exception as exc: raise LnurlResponseException(str(exc)) from exc @@ -61,7 +62,7 @@ async def get( try: _json = res.json() except JSONDecodeError as exc: - raise LnurlResponseException(f"Invalid JSON response from {url}") from exc + raise LnurlResponseException(f"Invalid JSON response from {request_url}") from exc if response_class: if not issubclass(response_class, LnurlResponseModel): @@ -86,8 +87,15 @@ async def handle( raise InvalidLnurl if lnurl.is_login: - callback_url = parse_obj_as(CallbackUrl, lnurl.url) - return LnurlAuthResponse(callback=callback_url, k1=lnurl.url.query_params["k1"]) + callback_url = TypeAdapter(CallbackUrl).validate_python(lnurl.url) + k1 = None + for param in lnurl.url.query_params(): + if param[0] == "k1": + k1 = param[1] + break + if not k1: + raise LnurlResponseException("k1 parameter not found in LNURLauth URL") + return LnurlAuthResponse(callback=callback_url, k1=k1) return await get(lnurl.url, response_class=response_class, user_agent=user_agent, timeout=timeout) @@ -134,7 +142,7 @@ async def execute_pay_request( headers = {"User-Agent": user_agent or USER_AGENT} async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client: res2 = await client.get( - url=res.callback, + url=str(res.callback), params=params, timeout=timeout or TIMEOUT, ) @@ -178,7 +186,7 @@ async def execute_login( headers = {"User-Agent": user_agent or USER_AGENT} async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client: res2 = await client.get( - url=res.callback, + url=str(res.callback), params={ "key": key, "sig": sig, @@ -209,7 +217,7 @@ async def execute_withdraw( headers = {"User-Agent": user_agent or USER_AGENT} async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client: res2 = await client.get( - url=res.callback, + url=str(res.callback), params={ "k1": res.k1, "pr": pr, diff --git a/lnurl/models.py b/lnurl/models.py index 6e009ec..91eda84 100644 --- a/lnurl/models.py +++ b/lnurl/models.py @@ -4,8 +4,7 @@ from abc import ABC from typing import Optional, Union -from bolt11 import MilliSatoshi -from pydantic import BaseModel, Field, ValidationError, validator +from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator from .exceptions import LnurlResponseException from .types import ( @@ -14,22 +13,40 @@ InitializationVectorBase64, LightningInvoice, LightningNodeUri, - Lnurl, + # Lnurl, LnurlPayMetadata, LnurlPaySuccessActionTag, LnurlResponseTag, LnurlStatus, + Lud17PayLink, Max144Str, + MilliSatoshi, Url, ) -class LnurlPayRouteHop(BaseModel): +class LnurlBaseModel(BaseModel): + model_config = ConfigDict( + use_enum_values=True, + extra="forbid", + ) + + def dict(self, *args, **kwargs): + kwargs.setdefault("mode", "json") + kwargs.setdefault("exclude_none", True) + return self.model_dump(*args, **kwargs) + + def json(self, *args, **kwargs): + kwargs.setdefault("exclude_none", True) + return self.model_dump_json(*args, **kwargs) + + +class LnurlPayRouteHop(LnurlBaseModel): nodeId: str channelUpdate: str -class LnurlPaySuccessAction(BaseModel, ABC): +class LnurlPaySuccessAction(LnurlBaseModel, ABC): tag: LnurlPaySuccessActionTag @@ -52,20 +69,7 @@ class AesAction(LnurlPaySuccessAction): iv: InitializationVectorBase64 -class LnurlResponseModel(BaseModel): - - class Config: - use_enum_values = True - extra = "forbid" - - def dict(self, **kwargs): - kwargs["exclude_none"] = True - return super().dict(**kwargs) - - def json(self, **kwargs): - kwargs["exclude_none"] = True - return super().json(**kwargs) - +class LnurlResponseModel(LnurlBaseModel): @property def ok(self) -> bool: return True @@ -119,7 +123,7 @@ class LnurlHostedChannelResponse(LnurlResponseModel): # LUD-18: Payer identity in payRequest protocol. -class LnurlPayResponsePayerDataOption(BaseModel): +class LnurlPayResponsePayerDataOption(LnurlBaseModel): mandatory: bool @@ -127,12 +131,12 @@ class LnurlPayResponsePayerDataOptionAuth(LnurlPayResponsePayerDataOption): k1: str -class LnurlPayResponsePayerDataExtra(BaseModel): +class LnurlPayResponsePayerDataExtra(LnurlBaseModel): name: str field: LnurlPayResponsePayerDataOption -class LnurlPayResponsePayerData(BaseModel): +class LnurlPayResponsePayerData(LnurlBaseModel): name: Optional[LnurlPayResponsePayerDataOption] = None pubkey: Optional[LnurlPayResponsePayerDataOption] = None identifier: Optional[LnurlPayResponsePayerDataOption] = None @@ -141,13 +145,13 @@ class LnurlPayResponsePayerData(BaseModel): extras: Optional[list[LnurlPayResponsePayerDataExtra]] = None -class LnurlPayerDataAuth(BaseModel): +class LnurlPayerDataAuth(LnurlBaseModel): key: str k1: str sig: str -class LnurlPayerData(BaseModel): +class LnurlPayerData(LnurlBaseModel): name: Optional[str] = None pubkey: Optional[str] = None identifier: Optional[str] = None @@ -172,11 +176,11 @@ class LnurlPayResponse(LnurlResponseModel): allowsNostr: Optional[bool] = None nostrPubkey: Optional[str] = None - @validator("maxSendable") - def max_less_than_min(cls, value, values): # noqa - if "minSendable" in values and value < values["minSendable"]: + @model_validator(mode="after") + def max_less_than_min(self): + if self.maxSendable < self.minSendable: raise ValueError("`maxSendable` cannot be less than `minSendable`.") - return value + return self @property def min_sats(self) -> int: @@ -214,22 +218,13 @@ class LnurlWithdrawResponse(LnurlResponseModel): balanceCheck: Optional[CallbackUrl] = None currentBalance: Optional[MilliSatoshi] = None # LUD-19: Pay link discoverable from withdraw link. - payLink: Optional[str] = None - - @validator("payLink", pre=True) - def paylink_must_be_lud17(cls, value: Optional[str] = None) -> str | None: - if not value: - return None - lnurl = Lnurl(value) - if lnurl.is_lud17 and lnurl.lud17_prefix == "lnurlp": - return value - raise ValueError("`payLink` must be a valid LUD17 URL (lnurlp://).") - - @validator("maxWithdrawable") - def max_less_than_min(cls, value, values): - if "minWithdrawable" in values and value < values["minWithdrawable"]: + payLink: Lud17PayLink | None = None + + @model_validator(mode="after") + def max_less_than_min(self): + if self.maxWithdrawable < self.minWithdrawable: raise ValueError("`maxWithdrawable` cannot be less than `minWithdrawable`.") - return value + return self # LUD-08: Fast withdrawRequest. @property diff --git a/lnurl/types.py b/lnurl/types.py index cfe56e8..17561f3 100644 --- a/lnurl/types.py +++ b/lnurl/types.py @@ -3,39 +3,33 @@ import json import os import re +from decimal import Decimal from enum import Enum from hashlib import sha256 -from typing import Generator, Optional -from urllib.parse import parse_qs +from typing import Annotated, Any, Optional from pydantic import ( + AfterValidator, AnyUrl, - ConstrainedStr, + BeforeValidator, + GetCoreSchemaHandler, + HttpUrl, Json, + StringConstraints, + TypeAdapter, + UrlConstraints, ValidationError, - parse_obj_as, - validator, ) -from pydantic.validators import str_validator +from pydantic_core import core_schema from .exceptions import InvalidLnurlPayMetadata, InvalidUrl, LnAddressError from .helpers import _bech32_decode, _lnurl_clean, url_decode, url_encode INSECURE_HOSTS = ["127.0.0.1", "0.0.0.0", "localhost"] +LUD17_SCHEMES = ["lnurlc", "lnurlw", "lnurlp", "keyauth"] -class ReprMixin: - def __repr__(self) -> str: - attrs = [ # type: ignore - outer_slot # type: ignore - for outer_slot in [slot for slot in self.__slots__ if not slot.startswith("_")] # type: ignore - if getattr(self, outer_slot) # type: ignore - ] # type: ignore - extra = ", " + ", ".join(f"{n}={getattr(self, n).__repr__()}" for n in attrs) if attrs else "" - return f"{self.__class__.__name__}({super().__repr__()}{extra})" - - -class Bech32(ReprMixin, str): +class Bech32(str): """Bech32 string.""" __slots__ = ("hrp", "data") @@ -43,7 +37,7 @@ class Bech32(ReprMixin, str): def __new__(cls, bech32: str, **_) -> "Bech32": return str.__new__(cls, bech32) - def __init__(self, bech32: str, *, hrp: Optional[str] = None, data: Optional[list[int]] = None): + def __init__(self, bech32: str, hrp: Optional[str] = None, data: Optional[list[int]] = None): str.__init__(bech32) self.hrp, self.data = (hrp, data) if hrp and data else self.__get_data__(bech32) @@ -52,164 +46,209 @@ def __get_data__(cls, bech32: str) -> tuple[str, list[int]]: return _bech32_decode(bech32) @classmethod - def __get_validators__(cls): - yield str_validator - yield cls.validate - - @classmethod - def validate(cls, value: str) -> "Bech32": - hrp, data = cls.__get_data__(value) - return cls(value, hrp=hrp, data=data) + def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + _ = (source_type, handler) + + def validate(value: Any): + hrp, data = cls.__get_data__(str(value)) + value = cls(str(value), hrp=hrp, data=data) + return value + + def serialize(value: Any) -> str: + return str(value) + + return core_schema.no_info_after_validator_function( + validate, + core_schema.union_schema( + [ + core_schema.is_instance_schema(cls), + core_schema.str_schema(), + ] + ), + serialization=core_schema.plain_serializer_function_ser_schema(serialize, when_used="json"), + ) -def ctrl_characters_validator(value: str) -> str: +def ctrl_characters_validator(value: AnyUrl) -> AnyUrl: """Checks for control characters (unicode blocks C0 and C1, plus DEL).""" - if re.compile(r"[\u0000-\u001f\u007f-\u009f]").search(value): + if re.compile(r"[\u0000-\u001f\u007f-\u009f]").search(str(value)): raise InvalidUrl("URL contains control characters.") return value -def strict_rfc3986_validator(value: str) -> str: +def strict_rfc3986_validator(value: AnyUrl) -> AnyUrl: """Checks for RFC3986 compliance.""" if os.environ.get("LNURL_STRICT_RFC3986", "0") == "1": - if re.compile(r"[^]a-zA-Z0-9._~:/?#[@!$&'()*+,;=-]").search(value): + if re.compile(r"[^]a-zA-Z0-9._~:/?#[@!$&'()*+,;=-]").search(str(value)): raise InvalidUrl("URL is not RFC3986 compliant.") return value -def valid_lnurl_host(url: str) -> AnyUrl: - """Validates the host part of a URL.""" - _url = parse_obj_as(AnyUrl, url) - if not _url.host: - raise InvalidUrl("URL host is required.") - if _url.scheme == "http": - if _url.host not in INSECURE_HOSTS and not _url.host.endswith(".onion"): - raise InvalidUrl("HTTP scheme is only allowed for localhost or onion addresses.") - return _url - - -class Url(AnyUrl): - max_length = 2047 # https://stackoverflow.com/questions/417142/ - - # LUD-17: Protocol schemes and raw (non bech32-encoded) URLs. - allowed_schemes = {"https", "http", "lnurlc", "lnurlw", "lnurlp", "keyauth"} - - @property - def is_lud17(self) -> bool: - uris = ["lnurlc", "lnurlw", "lnurlp", "keyauth"] - return any(self.scheme == uri for uri in uris) - - @classmethod - def __get_validators__(cls) -> Generator: - yield ctrl_characters_validator - yield strict_rfc3986_validator - yield valid_lnurl_host - yield cls.validate - - @property - def query_params(self) -> dict: - return {k: v[0] for k, v in parse_qs(self.query).items()} - - @property - def insecure(self) -> bool: - if not self.host: - return True - return self.scheme == "http" or self.host in INSECURE_HOSTS or self.host.endswith(".onion") - +def validate_http_host(url: AnyUrl) -> AnyUrl: + """Ensure only localhost or .onion addresses can use http://""" + if not url.host: + raise InvalidUrl("URL must have a valid host.") + if not url.scheme == "http" or url.host.endswith(".onion"): + return url + if url.host not in INSECURE_HOSTS: + raise InvalidUrl("HTTP scheme is only allowed for localhost or onion addresses.") + return url -class CallbackUrl(Url): - """URL for callbacks. exclude lud17 schemes.""" - allowed_schemes = {"https", "http"} +Url = Annotated[ + AnyUrl, + UrlConstraints( + max_length=2047, # https://stackoverflow.com/questions/417142/ + allowed_schemes=[ # LUD-17: Protocol schemes and raw (non bech32-encoded) URLs. + "https", + "http", + "lnurlc", + "lnurlw", + "lnurlp", + "keyauth", + ], + ), + BeforeValidator(ctrl_characters_validator), + BeforeValidator(strict_rfc3986_validator), + AfterValidator(validate_http_host), +] + +# URL for callbacks. exclude lud17 schemes. +CallbackUrl = Annotated[ + HttpUrl, + BeforeValidator(ctrl_characters_validator), + BeforeValidator(strict_rfc3986_validator), + AfterValidator(validate_http_host), +] class LightningInvoice(Bech32): """Bech32 Lightning invoice.""" -class LightningNodeUri(ReprMixin, str): +class LightningNodeUri(str): """Remote node address of form `node_key@ip_address:port_number`.""" - __slots__ = ("key", "ip", "port") + __slots__ = ("username", "host", "port") def __new__(cls, uri: str, **_) -> "LightningNodeUri": return str.__new__(cls, uri) - def __init__(self, uri: str, *, key: Optional[str] = None, ip: Optional[str] = None, port: Optional[str] = None): + def __init__( + self, + uri: str, + username: Optional[str] = None, + host: Optional[str] = None, + port: Optional[str] = None, + ): str.__init__(uri) - self.key = key - self.ip = ip - self.port = port + self.username, self.host, self.port = ( + (username, host, port) if username and host and port else self.__get_parts__(uri) + ) @classmethod - def __get_validators__(cls): - yield str_validator - yield cls.validate + def __get_parts__(cls, uri: str) -> tuple[str, str, str]: + match = re.fullmatch(r"(?P[^@:\s]+)@(?P[^@:\s]+):(?P[^@:\s]+)", uri) + if not match: + raise ValueError("Invalid Lightning node URI.") + return match.group("username"), match.group("host"), match.group("port") @classmethod - def validate(cls, value: str) -> "LightningNodeUri": - try: - key, netloc = value.split("@") - ip, port = netloc.split(":") - except Exception: - raise ValueError - - return cls(value, key=key, ip=ip, port=port) + def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + _ = (source_type, handler) + + def validate(value: Any): + username, host, port = cls.__get_parts__(str(value)) + return cls(str(value), username=username, host=host, port=port) + + def serialize(value: Any) -> str: + return str(value) + + return core_schema.no_info_after_validator_function( + validate, + core_schema.union_schema( + [ + core_schema.is_instance_schema(cls), + core_schema.str_schema(), + ] + ), + serialization=core_schema.plain_serializer_function_ser_schema(serialize, when_used="json"), + ) -class Lnurl(ReprMixin, str): +class Lnurl(str): url: Url lud17_prefix: Optional[str] = None def __new__(cls, lightning: str) -> Lnurl: url = cls.clean(lightning) - _url = url.replace(url.scheme, "http" if url.insecure else "https", 1) - return str.__new__(cls, _url) + return str.__new__(cls, str(url)) def __init__(self, lightning: str): url = self.clean(lightning) - if not url.is_lud17: - self.url = url + if not url.host: + raise InvalidUrl("URL must have a valid host.") + is_lud17 = any(url.scheme == uri for uri in LUD17_SCHEMES) + if not is_lud17: + self.url = TypeAdapter(Url).validate_python(str(url)) self.lud17_prefix = None - return str.__init__(url) + return str.__init__(str(url)) self.lud17_prefix = url.scheme - _url = parse_obj_as(Url, url.replace(url.scheme, "http" if url.insecure else "https", 1)) - self.url = _url + insecure = url.host in INSECURE_HOSTS or url.host.endswith(".onion") + _replace = "http" if insecure else "https" + _url = str(url) + _url = _url.replace(url.scheme, _replace, 1) + self.url = TypeAdapter(Url).validate_python(_url) return str.__init__(_url) - @classmethod - def __get_validators__(cls): - yield str_validator - yield cls.validate - @classmethod def clean(cls, lightning: str) -> Url: lightning = _lnurl_clean(lightning) if lightning.lower().startswith("lnurl1"): - url = parse_obj_as(Url, url_decode(lightning)) + url = TypeAdapter(Url).validate_python(url_decode(lightning)) return url - url = parse_obj_as(Url, lightning) + url = TypeAdapter(Url).validate_python(lightning) return url @classmethod - def validate(cls, lightning: str) -> Lnurl: - _ = cls.clean(lightning) - return cls(lightning) + def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + _ = (source_type, handler) + + def validate(value: Any): + _ = cls.clean(value) + return cls(value) + + def serialize(value: Any) -> str: + return str(value) + + return core_schema.no_info_after_validator_function( + validate, + core_schema.union_schema( + [ + core_schema.is_instance_schema(cls), + core_schema.str_schema(), + ] + ), + serialization=core_schema.plain_serializer_function_ser_schema(serialize, when_used="json"), + ) @property def bech32(self) -> Bech32: """Returns Bech32 representation of the Lnurl if it is a Bech32 encoded URL.""" - return parse_obj_as(Bech32, url_encode(self.url)) + url = url_encode(str(self.url)) + return TypeAdapter(Bech32).validate_python(url) # LUD-04: auth base spec. @property def is_login(self) -> bool: - return self.url.query_params.get("tag") == "login" + return any(k == "tag" and v == "login" for k, v in self.url.query_params()) # LUD-08: Fast withdrawRequest. @property def is_fast_withdraw(self) -> bool: - q = self.url.query_params + q: dict[str, str] = {} + for k, v in self.url.query_params(): + q[k] = v return ( q.get("tag") == "withdrawRequest" and q.get("k1") is not None @@ -228,11 +267,12 @@ def is_lud17(self) -> bool: def lud17(self) -> Optional[str]: if not self.lud17_prefix: return None - url = self.url.replace(self.url.scheme, self.lud17_prefix, 1) - return url + _url = str(self.url) + return _url.replace(self.url.scheme, self.lud17_prefix, 1) -class LnAddress(ReprMixin, str): +# LUD-16: Paying to static internet identifiers. +class LnAddress(str): """Lightning address of form `user+tag@host`""" slots = ("address", "url", "tag") @@ -252,9 +292,7 @@ def __init__(self, address: str): self.tag = None self.address = address - # LUD-16: Paying to static internet identifiers. - @validator("address") - def is_valid_lnaddress(cls, address: str) -> bool: + def is_valid_lnaddress(self, address: str) -> bool: # A user can then type these on a WALLET. The is limited # to a-z-1-9-_.. Please note that this is way more strict than common # email addresses as it allows fewer symbols and only lowercase characters. @@ -265,10 +303,10 @@ def is_valid_lnaddress(cls, address: str) -> bool: def __get_url__(cls, address: str) -> CallbackUrl: name, domain = address.split("@") url = ("http://" if domain.endswith(".onion") else "https://") + domain + "/.well-known/lnurlp/" + name - return parse_obj_as(CallbackUrl, url) + return TypeAdapter(CallbackUrl).validate_python(url) -class LnurlPayMetadata(ReprMixin, str): +class LnurlPayMetadata(str): # LUD-16: Paying to static internet identifiers. "text/identifier", "text/email", "text/tag" # LUD-20: Long payment description for pay protocol. "text/long-desc" valid_metadata_mime_types = { @@ -293,7 +331,7 @@ def __init__(self, json_str: str, *, json_obj: Optional[list] = None): @classmethod def __validate_metadata__(cls, json_str: str) -> list[tuple[str, str]]: try: - parse_obj_as(Json[list[tuple[str, str]]], json_str) + TypeAdapter(Json[list[tuple[str, str]]]).validate_python(json_str) data = [(str(item[0]), str(item[1])) for item in json.loads(json_str)] except ValidationError: raise InvalidLnurlPayMetadata @@ -312,13 +350,25 @@ def __validate_metadata__(cls, json_str: str) -> list[tuple[str, str]]: return clean_data @classmethod - def __get_validators__(cls): - yield str_validator - yield cls.validate - - @classmethod - def validate(cls, value: str) -> LnurlPayMetadata: - return cls(value, json_obj=cls.__validate_metadata__(value)) + def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + _ = (source_type, handler) + + def validate(value: Any): + return cls(value, json_obj=cls.__validate_metadata__(str(value))) + + def serialize(value: Any) -> str: + return str(value) + + return core_schema.no_info_after_validator_function( + validate, + core_schema.union_schema( + [ + core_schema.is_instance_schema(cls), + core_schema.str_schema(), + ] + ), + serialization=core_schema.plain_serializer_function_ser_schema(serialize, when_used="json"), + ) @property def h(self) -> str: @@ -343,18 +393,33 @@ def list(self) -> list[tuple[str, str]]: return self._list -class InitializationVectorBase64(ConstrainedStr): - min_length = 24 - max_length = 24 +InitializationVectorBase64 = Annotated[str, StringConstraints(min_length=24, max_length=24)] +CiphertextBase64 = Annotated[str, StringConstraints(min_length=24, max_length=4096)] +Max144Str = Annotated[str, StringConstraints(max_length=144)] -class CiphertextBase64(ConstrainedStr): - min_length = 24 - max_length = 4096 +class MilliSatoshi(int): + """A thousandth of a satoshi.""" + @classmethod + def from_btc(cls, btc: Decimal) -> MilliSatoshi: + return cls(btc * 100_000_000_000) -class Max144Str(ConstrainedStr): - max_length = 144 + @property + def btc(self) -> Decimal: + return Decimal(self) / 100_000_000_000 + + @property + def sat(self) -> int: + return self // 1000 + + @classmethod + def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + _ = (source_type, handler) + return core_schema.no_info_after_validator_function( + cls, + core_schema.int_schema(), + ) # LUD-04: auth base spec. @@ -390,3 +455,15 @@ class LnurlResponseTag(Enum): hostedChannelRequest = "hostedChannelRequest" payRequest = "payRequest" withdrawRequest = "withdrawRequest" + + +def validate_paylink_is_lud17(value: Optional[str] = None) -> str | None: + if not value: + return None + lnurl = Lnurl(value) + if lnurl.is_lud17 and lnurl.lud17_prefix == "lnurlp": + return value + raise ValueError("`payLink` must be a valid LUD17 URL (lnurlp://).") + + +Lud17PayLink = Annotated[str, AfterValidator(validate_paylink_is_lud17)] diff --git a/pyproject.toml b/pyproject.toml index 44966d9..87592ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lnurl" -version = "0.10.0" +version = "2.0.0" requires-python = ">=3.10,<3.13" description = "LNURL implementation for Python." authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }] @@ -8,7 +8,7 @@ urls = { Repository = "https://github.com/lnbits/lnurl" } readme = "README.md" license = "MIT" dependencies = [ - "pydantic>=1.10.0,<2.0.0", + "pydantic>=2.10.0,<3.0.0", "pycryptodomex>=3.21.0", "bip32>=5.0.0", "bech32", diff --git a/tests/test_core.py b/tests/test_core.py index 6faf12f..64953d0 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -11,7 +11,7 @@ LnurlSuccessResponse, LnurlWithdrawResponse, ) -from lnurl.types import Lnurl, Url +from lnurl.types import Lnurl class TestDecode: @@ -31,8 +31,7 @@ def test_decode(self, bech32, url): decoded = decode(bech32) assert isinstance(decoded, Lnurl) decoded_url = decoded.url - assert isinstance(decoded_url, Url) - assert decoded_url == str(decoded_url) == url + assert str(decoded_url) == url assert decoded_url.host == "service.io" @pytest.mark.parametrize( diff --git a/tests/test_fast_withdraw.py b/tests/test_fast_withdraw.py index afcca4c..3671f39 100644 --- a/tests/test_fast_withdraw.py +++ b/tests/test_fast_withdraw.py @@ -1,5 +1,5 @@ import pytest -from pydantic import parse_obj_as +from pydantic import TypeAdapter from lnurl import encode from lnurl.models import LnurlWithdrawResponse @@ -27,8 +27,7 @@ def test_is_lnurl_fast_withdraw(self, url: str, expected: bool): assert lnurl.is_fast_withdraw == expected def test_set_lnurl_fast_withdraw(self): - response = parse_obj_as( - LnurlWithdrawResponse, + response = TypeAdapter(LnurlWithdrawResponse).validate_python( { "tag": "withdrawRequest", "k1": "0" * 16, diff --git a/tests/test_models.py b/tests/test_models.py index 0d434e4..92d406b 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,7 @@ import json import pytest -from pydantic import ValidationError, parse_obj_as +from pydantic import TypeAdapter, ValidationError from lnurl import CallbackUrl, Lnurl, LnurlPayMetadata, MilliSatoshi, encode from lnurl.models import ( @@ -9,6 +9,7 @@ LnurlErrorResponse, LnurlHostedChannelResponse, LnurlPayResponse, + LnurlPayResponsePayerDataOption, LnurlSuccessResponse, LnurlWithdrawResponse, ) @@ -19,7 +20,7 @@ def test_response(self): res = LnurlErrorResponse(reason="blah blah blah") assert res.ok is False assert res.error_msg == "blah blah blah" - assert res.json() == '{"status": "ERROR", "reason": "blah blah blah"}' + assert res.json() == '{"status":"ERROR","reason":"blah blah blah"}' assert res.dict() == {"status": "ERROR", "reason": "blah blah blah"} @@ -27,7 +28,7 @@ class TestLnurlSuccessResponse: def test_success_response(self): res = LnurlSuccessResponse() assert res.ok - assert res.json() == '{"status": "OK"}' + assert res.json() == '{"status":"OK"}' assert res.dict() == {"status": "OK"} @@ -84,8 +85,8 @@ class TestLnurlPayResponse: ], ) def test_success_response(self, callback: str, min_sendable: int, max_sendable: int, metadata: str): - callback_url = parse_obj_as(CallbackUrl, callback) - data = parse_obj_as(LnurlPayMetadata, metadata) + callback_url = TypeAdapter(CallbackUrl).validate_python(callback) + data = TypeAdapter(LnurlPayMetadata).validate_python(metadata) res = LnurlPayResponse( callback=callback_url, minSendable=MilliSatoshi(min_sendable), @@ -94,20 +95,16 @@ def test_success_response(self, callback: str, min_sendable: int, max_sendable: ) assert res.ok assert ( - res.json() == res.json() == '{"tag": "payRequest", "callback": "https://service.io/pay", ' - f'"minSendable": 1000, "maxSendable": 2000, "metadata": {json.dumps(metadata)}}}' - ) - assert ( - res.dict() - == res.dict() - == { - "tag": "payRequest", - "callback": "https://service.io/pay", - "minSendable": 1000, - "maxSendable": 2000, - "metadata": metadata, - } + res.json() == '{"tag":"payRequest","callback":"https://service.io/pay",' + f'"minSendable":1000,"maxSendable":2000,"metadata":{json.dumps(metadata)}}}' ) + assert res.dict() == { + "tag": "payRequest", + "callback": "https://service.io/pay", + "minSendable": 1000, + "maxSendable": 2000, + "metadata": metadata, + } @pytest.mark.parametrize( "d", @@ -141,30 +138,25 @@ def test_success_response( self, callback: str, min_sendable: int, max_sendable: int, metadata: str, comment_allowed: int ): res = LnurlPayResponse( - callback=parse_obj_as(CallbackUrl, callback), + callback=TypeAdapter(CallbackUrl).validate_python(callback), minSendable=MilliSatoshi(min_sendable), maxSendable=MilliSatoshi(max_sendable), - metadata=parse_obj_as(LnurlPayMetadata, metadata), + metadata=TypeAdapter(LnurlPayMetadata).validate_python(metadata), commentAllowed=comment_allowed, ) assert res.ok assert ( - res.json() == res.json() == '{"tag": "payRequest", "callback": "https://service.io/pay", ' - f'"minSendable": 1000, "maxSendable": 2000, "metadata": {json.dumps(metadata)}, ' - '"commentAllowed": 555}' - ) - assert ( - res.dict() - == res.dict() - == { - "tag": "payRequest", - "callback": "https://service.io/pay", - "minSendable": 1000, - "maxSendable": 2000, - "metadata": metadata, - "commentAllowed": 555, - } + res.json() == '{"tag":"payRequest","callback":"https://service.io/pay",' + f'"minSendable":1000,"maxSendable":2000,"metadata":{json.dumps(metadata)},"commentAllowed":555}}' ) + assert res.dict() == { + "tag": "payRequest", + "callback": "https://service.io/pay", + "minSendable": 1000, + "maxSendable": 2000, + "metadata": metadata, + "commentAllowed": 555, + } @pytest.mark.parametrize( "d", @@ -202,30 +194,24 @@ class TestLnurlWithdrawResponse: ) def test_success_response(self, callback: str, k1: str, min_withdrawable: int, max_withdrawable: int): res = LnurlWithdrawResponse( - callback=parse_obj_as(CallbackUrl, callback), + callback=TypeAdapter(CallbackUrl).validate_python(callback), k1=k1, minWithdrawable=MilliSatoshi(min_withdrawable), maxWithdrawable=MilliSatoshi(max_withdrawable), ) assert res.ok assert ( - res.json() - == res.json() - == '{"tag": "withdrawRequest", "callback": "https://service.io/withdraw", "k1": "c3RyaW5n", ' - '"minWithdrawable": 100, "maxWithdrawable": 200, "defaultDescription": ""}' - ) - assert ( - res.dict() - == res.dict() - == { - "tag": "withdrawRequest", - "callback": "https://service.io/withdraw", - "k1": "c3RyaW5n", - "minWithdrawable": 100, - "maxWithdrawable": 200, - "defaultDescription": "", - } + res.json() == '{"tag":"withdrawRequest","callback":"https://service.io/withdraw","k1":"c3RyaW5n",' + '"minWithdrawable":100,"maxWithdrawable":200,"defaultDescription":""}' ) + assert res.dict() == { + "tag": "withdrawRequest", + "callback": "https://service.io/withdraw", + "k1": "c3RyaW5n", + "minWithdrawable": 100, + "maxWithdrawable": 200, + "defaultDescription": "", + } @pytest.mark.parametrize( "d", @@ -256,7 +242,7 @@ def test_invalid_data(self, d): def test_invalid_pay_link(self, payLink: str): with pytest.raises(ValidationError): _ = LnurlWithdrawResponse( - callback=parse_obj_as(CallbackUrl, "https://service.io/withdraw/cb"), + callback=TypeAdapter(CallbackUrl).validate_python("https://service.io/withdraw/cb"), k1="c3RyaW5n", minWithdrawable=MilliSatoshi(100), maxWithdrawable=MilliSatoshi(200), @@ -264,13 +250,20 @@ def test_invalid_pay_link(self, payLink: str): ) def test_valid_pay_link(self): - payLink = parse_obj_as(Lnurl, "lnurlp://service.io/pay") + payLink = TypeAdapter(Lnurl).validate_python("lnurlp://service.io/pay") assert payLink.is_lud17 assert payLink.lud17_prefix == "lnurlp" _ = LnurlWithdrawResponse( - callback=parse_obj_as(CallbackUrl, "https://service.io/withdraw/cb"), + callback=TypeAdapter(CallbackUrl).validate_python("https://service.io/withdraw/cb"), k1="c3RyaW5n", minWithdrawable=MilliSatoshi(100), maxWithdrawable=MilliSatoshi(200), payLink=payLink.lud17, ) + + +class TestLnurlBaseModelCompatibility: + def test_nested_models_support_dict_and_json(self): + option = LnurlPayResponsePayerDataOption(mandatory=True) + assert option.dict() == option.model_dump() + assert option.json() == option.model_dump_json() diff --git a/tests/test_models_from_dict.py b/tests/test_models_from_dict.py index 0b71904..af086fb 100644 --- a/tests/test_models_from_dict.py +++ b/tests/test_models_from_dict.py @@ -84,7 +84,7 @@ def test_pay_action_aes(self): assert isinstance(res.successAction, LnurlPaySuccessAction) assert res.ok assert res.successAction - assert res.successAction.tag == LnurlPaySuccessActionTag.aes + assert LnurlPaySuccessActionTag.aes.value == res.successAction.tag assert res.successAction.description == "your will receive a secret message" assert len(res.successAction.iv) == 24 assert len(res.successAction.ciphertext) == 44 diff --git a/tests/test_types.py b/tests/test_types.py index 46a955d..c737fad 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,5 +1,5 @@ import pytest -from pydantic import ValidationError, parse_obj_as +from pydantic import HttpUrl, TypeAdapter, ValidationError from lnurl import ( CallbackUrl, @@ -22,20 +22,24 @@ class TestUrl: # https://github.com/pydantic/pydantic/discussions/2450 @pytest.mark.parametrize( - "hostport", - ["service.io:443", "service.io:9000"], + "hostport, expected", + [ + ("service.io:443", "https://service.io/?q=3fc3645b439ce8e7&test=ok"), + ("service.io:9000", "https://service.io:9000/?q=3fc3645b439ce8e7&test=ok"), + ], ) - def test_parameters(self, hostport): - url = parse_obj_as(CallbackUrl, f"https://{hostport}/?q=3fc3645b439ce8e7&test=ok") + def test_parameters(self, hostport, expected): + url = TypeAdapter(CallbackUrl).validate_python(f"https://{hostport}/?q=3fc3645b439ce8e7&test=ok") + assert isinstance(url, HttpUrl) assert url.host == "service.io" - assert url == f"https://{hostport}/?q=3fc3645b439ce8e7&test=ok" - assert url.query_params == {"q": "3fc3645b439ce8e7", "test": "ok"} + assert str(url) == expected + assert dict(url.query_params()) == {"q": "3fc3645b439ce8e7", "test": "ok"} @pytest.mark.parametrize( "url", [ "https://service.io/?q=3fc3645b439ce8e7&test=ok", - "https://[2001:db8:0:1]:80", + "https://[2001:db8::1]:80", "https://protonirockerxow.onion/", "http://protonirockerxow.onion/", "https://📙.la/⚡", # https://emojipedia.org/high-voltage-sign/ @@ -46,8 +50,8 @@ def test_parameters(self, hostport): ], ) def test_valid_callback(self, url): - url = parse_obj_as(CallbackUrl, url) - assert isinstance(url, CallbackUrl) + url = TypeAdapter(CallbackUrl).validate_python(url) + assert isinstance(url, HttpUrl) @pytest.mark.parametrize( "url", @@ -63,7 +67,7 @@ def test_valid_callback(self, url): ) def test_invalid_data_callback(self, url): with pytest.raises(ValidationError): - parse_obj_as(CallbackUrl, url) + TypeAdapter(CallbackUrl).validate_python(url) @pytest.mark.parametrize( "url", @@ -75,7 +79,7 @@ def test_invalid_data_callback(self, url): def test_strict_rfc3986(self, monkeypatch, url): monkeypatch.setenv("LNURL_STRICT_RFC3986", "1") with pytest.raises(ValidationError): - parse_obj_as(CallbackUrl, url) + TypeAdapter(CallbackUrl).validate_python(url) class TestLightningInvoice: @@ -97,7 +101,7 @@ class TestLightningInvoice: ) def test_valid_invoice(self, bech32, hrp, prefix, amount, h): invoice = LightningInvoice(bech32) - assert invoice == parse_obj_as(LightningInvoice, bech32) + assert invoice == TypeAdapter(LightningInvoice).validate_python(bech32) assert invoice.hrp == hrp # TODO: implement these properties # assert invoice.prefix == prefix @@ -106,15 +110,15 @@ def test_valid_invoice(self, bech32, hrp, prefix, amount, h): class TestLightningNode: def test_valid_node(self): - node = parse_obj_as(LightningNodeUri, "node_key@ip_address:port_number") - assert node.key == "node_key" - assert node.ip == "ip_address" - assert node.port == "port_number" + node = TypeAdapter(LightningNodeUri).validate_python("node_key@0.0.0.0:5000") + assert node.username == "node_key" + assert node.host == "0.0.0.0" + assert node.port == "5000" @pytest.mark.parametrize("uri", ["https://service.io/node", "node_key@ip_address", "ip_address:port_number"]) def test_invalid_node(self, uri): with pytest.raises(ValidationError): - parse_obj_as(LightningNodeUri, uri) + TypeAdapter(LightningNodeUri).validate_python(uri) class TestLnurl: @@ -129,24 +133,24 @@ class TestLnurl: "lightning:LNURL1DP68GURN8GHJ7UM9WFMXJCM99E5K7TELWY7NXENRXVMRGDTZXSENJCM98PJNWE3JX56NXCFK89JN2V3K" "XUCRSVTY8YMXGCMYXV6RQD3EXDSKVCTZV5CRGCN9XA3RQCMRVSCNWWRYVCYAE0UU" ), - "https://service.io?a=1&b=2", - "lnurlp://service.io?a=1&b=2", + "https://service.io/?a=1&b=2", + "lnurlp://service.io/?a=1&b=2", "lnurlp://service.io/lnurlp?a=1&b=2", ], ) def test_valid_lnurl_and_bech32(self, lightning): lnurl = Lnurl(lightning) - assert lnurl == parse_obj_as(Lnurl, lightning) + assert lnurl == TypeAdapter(Lnurl).validate_python(lightning) if lnurl.is_lud17: assert lnurl.lud17 == lightning assert lnurl.lud17_prefix == lightning.split("://")[0] assert lnurl.is_lud17 is True - assert lnurl.url == lightning.replace("lnurlp://", "https://") - assert lnurl == lightning.replace("lnurlp://", "https://") + assert str(lnurl.url) == lightning.replace("lnurlp://", "https://") + assert str(lnurl) == lightning else: assert lnurl.bech32 is not None assert lnurl.bech32.hrp == "lnurl" - assert lnurl.bech32 == lnurl or lnurl.url == lnurl + assert lnurl.bech32 == lnurl or str(lnurl.url) == lnurl assert lnurl.lud17 is None assert lnurl.lud17_prefix is None assert lnurl.is_lud17 is False @@ -156,14 +160,14 @@ def test_valid_lnurl_and_bech32(self, lightning): @pytest.mark.parametrize( "url", [ - "lnurlp://service.io?a=1&b=2", - "lnurlc://service.io", - "lnurlw://service.io", - "keyauth://service.io", + "lnurlp://service.io/?a=1&b=2", + "lnurlc://service.io/", + "lnurlw://service.io/", + "keyauth://service.io/", ], ) def test_valid_lnurl_lud17(self, url: str): - _lnurl = parse_obj_as(Lnurl, url) + _lnurl = TypeAdapter(Lnurl).validate_python(url) _prefix = url.split("://")[0] assert _lnurl.lud17 == url @@ -173,8 +177,8 @@ def test_valid_lnurl_lud17(self, url: str): _url = _url.replace("lnurlc://", "https://") _url = _url.replace("lnurlw://", "https://") _url = _url.replace("keyauth://", "https://") + assert str(_lnurl) == url assert str(_lnurl.url) == _url - assert str(_lnurl) == _url @pytest.mark.parametrize( "url", @@ -185,7 +189,8 @@ def test_valid_lnurl_lud17(self, url: str): ) def test_invalid_lnurl(self, url: str): with pytest.raises(ValidationError): - parse_obj_as(Lnurl, url) + TypeAdapter(Lnurl).validate_python(url) + raise ValidationError @pytest.mark.parametrize( "bech32", @@ -197,7 +202,7 @@ def test_invalid_lnurl(self, url: str): ) def test_decode_nolnurl(self, bech32): with pytest.raises(ValidationError): - parse_obj_as(Lnurl, bech32) + TypeAdapter(Lnurl).validate_python(bech32) @pytest.mark.parametrize( "url", @@ -207,10 +212,9 @@ def test_decode_nolnurl(self, bech32): ], ) def test_insecure_lnurl(self, url: str): - lnurl = parse_obj_as(Lnurl, url) - assert lnurl.url.insecure is True + lnurl = TypeAdapter(Lnurl).validate_python(url) + assert lnurl.url.scheme == "http" assert lnurl.url.host == "localhost" - assert lnurl.url.startswith("http://") class TestLnurlPayMetadata: @@ -224,7 +228,7 @@ class TestLnurlPayMetadata: ], ) def test_valid(self, metadata, image_type): - m = parse_obj_as(LnurlPayMetadata, metadata) + m = TypeAdapter(LnurlPayMetadata).validate_python(metadata) assert m.text == "main text" if m.images: @@ -244,7 +248,7 @@ def test_valid(self, metadata, image_type): ) def test_invalid_data(self, metadata): with pytest.raises(ValidationError): - parse_obj_as(LnurlPayMetadata, metadata) + TypeAdapter(LnurlPayMetadata).validate_python(metadata) @pytest.mark.parametrize( "lnaddress", @@ -254,7 +258,7 @@ def test_invalid_data(self, metadata): ) def test_valid_lnaddress(self, lnaddress): lnaddress = LnAddress(lnaddress) - assert isinstance(lnaddress.url, CallbackUrl) + assert isinstance(lnaddress.url, HttpUrl) assert lnaddress.tag is None @pytest.mark.parametrize( @@ -265,7 +269,7 @@ def test_valid_lnaddress(self, lnaddress): ) def test_valid_lnaddress_with_tag(self, lnaddress): lnaddress = LnAddress(lnaddress) - assert isinstance(lnaddress.url, CallbackUrl) + assert isinstance(lnaddress.url, HttpUrl) assert lnaddress.tag == "lud16tag" @pytest.mark.parametrize( @@ -295,7 +299,7 @@ def test_valid_pay_response_payer_data(self): }, ], } - payer_data = parse_obj_as(LnurlPayResponsePayerData, data) + payer_data = TypeAdapter(LnurlPayResponsePayerData).validate_python(data) assert payer_data.name is not None assert payer_data.name.mandatory is True assert payer_data.pubkey is not None @@ -350,7 +354,7 @@ def test_valid_payer_data(self): "sig": "0" * 64, }, } - payer_data = parse_obj_as(LnurlPayerData, data) + payer_data = TypeAdapter(LnurlPayerData).validate_python(data) assert payer_data.name == "John Doe" assert payer_data.pubkey == "03a3xxxxxxxxxxxx" assert payer_data.auth is not None @@ -389,4 +393,4 @@ def test_error_res_details(self): assert _dict["status"] == "ERROR" assert "reason" in _dict assert _dict["reason"] == "detail" - assert res.json() == '{"status": "ERROR", "reason": "detail"}' + assert res.json() == '{"status":"ERROR","reason":"detail"}' diff --git a/uv.lock b/uv.lock index f3dc7f2..424dd51 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 3 requires-python = ">=3.10, <3.13" +[[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" @@ -490,7 +499,7 @@ wheels = [ [[package]] name = "lnurl" -version = "0.10.0" +version = "2.0.0" source = { editable = "." } dependencies = [ { name = "bech32" }, @@ -522,7 +531,7 @@ requires-dist = [ { name = "coincurve", specifier = ">=20.0.0" }, { name = "httpx" }, { name = "pycryptodomex", specifier = ">=3.21.0" }, - { name = "pydantic", specifier = ">=1.10.0,<2.0.0" }, + { name = "pydantic", specifier = ">=2.10.0,<3.0.0" }, ] [package.metadata.requires-dev] @@ -676,29 +685,93 @@ wheels = [ [[package]] name = "pydantic" -version = "1.10.26" +version = "2.12.5" 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/7b/da/fd89f987a376c807cd81ea0eff4589aade783bbb702637b4734ef2c743a2/pydantic-1.10.26.tar.gz", hash = "sha256:8c6aa39b494c5af092e690127c283d84f363ac36017106a9e66cb33a22ac412e", size = 357906, upload-time = "2025-12-18T15:47:46.557Z" } +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" } wheels = [ - { 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" }, + { 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" }, +] + +[[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]] @@ -890,6 +963,18 @@ 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"