Skip to content

Commit

Permalink
feat: make requests async (#32)
Browse files Browse the repository at this point in the history
* feat: make requests async
use `httpx` instead of `requests` for and make functions async
  • Loading branch information
dni committed Apr 24, 2024
1 parent f92b14a commit 6ad564d
Show file tree
Hide file tree
Showing 6 changed files with 316 additions and 310 deletions.
36 changes: 22 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,26 +53,28 @@ The different types of responses defined in the [LNURL spec][lnurl-spec] have a
with different properties (see `models.py`):

```python
import requests
import httpx

from lnurl import Lnurl, LnurlResponse

lnurl = Lnurl('LNURL1DP68GURN8GHJ7MRWW4EXCTNZD9NHXATW9EU8J730D3H82UNV94MKJARGV3EXZAELWDJHXUMFDAHR6WFHXQERSVPCA649RV')
try:
async with httpx.AsyncClient() as client:
r = await client.get(lnurl.url)
res = LnurlResponse.from_dict(r.json()) # LnurlPayResponse
res.ok # bool
res.max_sendable # int
res.max_sats # int
res.callback.base # str
res.callback.query_params # dict
res.metadata # str
res.metadata.list() # list
res.metadata.text # str
res.metadata.images # list
r = requests.get(lnurl.url)

res = LnurlResponse.from_dict(r.json()) # LnurlPayResponse
res.ok # bool
res.max_sendable # int
res.max_sats # int
res.callback.base # str
res.callback.query_params # dict
res.metadata # str
res.metadata.list() # list
res.metadata.text # str
res.metadata.images # list
```

If you have already `requests` installed, you can also use the `.handle()` function directly.
If you have already `httpx` installed, you can also use the `.handle()` function directly.
It will return the appropriate response for a LNURL.

```python
Expand All @@ -81,6 +83,12 @@ It will return the appropriate response for a LNURL.
LnurlPayResponse(tag='payRequest', callback=WebUrl('https://lnurl.bigsun.xyz/lnurl-pay/callback/2169831', scheme='https', host='lnurl.bigsun.xyz', tld='xyz', host_type='domain', path='/lnurl-pay/callback/2169831'), min_sendable=10000, max_sendable=10000, metadata=LnurlPayMetadata('[["text/plain","NgHaEyaZNDnW iI DsFYdkI"],["image/png;base64","iVBOR...uQmCC"]]'))
```

You can execute and LNURL with either payRequest, withdrawRequest or login tag using the `execute` function.
```python
>>> import lnurl
>>> lnurl.execute('lightning:LNURL1DP68GURN8GHJ7MRWW4EXCTNZD9NHXATW9EU8J730D3H82UNV94CXZ7FLWDJHXUMFDAHR6V33XCUNSVE38QV6UF', 100000)
```

Building your own LNURL responses
---------------------------------

Expand Down Expand Up @@ -142,5 +150,5 @@ Commands:
decode decode a LNURL
encode encode a URL
handle handle a LNURL
payment-request make a payment_request
execute execute a LNURL
```
5 changes: 3 additions & 2 deletions lnurl/cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""lnurl CLI"""

import asyncio
import sys

import click
Expand Down Expand Up @@ -46,7 +47,7 @@ def handle(lnurl):
"""
handle a LNURL
"""
decoded = handle_lnurl(lnurl)
decoded = asyncio.run(handle_lnurl(lnurl))
click.echo(decoded.json())


Expand All @@ -59,7 +60,7 @@ def execute(lnurl, msat_or_login):
"""
if not msat_or_login:
raise ValueError("You must provide either an amount_msat or a login_id.")
res = execute_lnurl(lnurl, msat_or_login)
res = asyncio.run(execute_lnurl(lnurl, msat_or_login))
click.echo(res.json())


Expand Down
99 changes: 51 additions & 48 deletions lnurl/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any, Optional, Union

import requests
import httpx
from bolt11 import Bolt11Exception, MilliSatoshi
from bolt11 import decode as bolt11_decode
from pydantic import ValidationError
Expand All @@ -25,89 +25,91 @@ def encode(url: str) -> Lnurl:
raise InvalidUrl


def get(url: str, *, response_class: Optional[Any] = None, verify: Union[str, bool] = True) -> LnurlResponseModel:
try:
req = requests.get(url, verify=verify)
req.raise_for_status()
except Exception as e:
raise LnurlResponseException(str(e))
async def get(url: str, *, response_class: Optional[Any] = None) -> LnurlResponseModel:
async with httpx.AsyncClient() as client:
try:
res = await client.get(url)
res.raise_for_status()
except Exception as e:
raise LnurlResponseException(str(e))

if response_class:
assert issubclass(response_class, LnurlResponseModel), "Use a valid `LnurlResponseModel` subclass."
return response_class(**req.json())
if response_class:
assert issubclass(response_class, LnurlResponseModel), "Use a valid `LnurlResponseModel` subclass."
return response_class(**res.json())

return LnurlResponse.from_dict(req.json())
return LnurlResponse.from_dict(res.json())


def handle(
async def handle(
bech32_lnurl: str,
response_class: Optional[LnurlResponseModel] = None,
verify: Union[str, bool] = True,
) -> LnurlResponseModel:
try:
if "@" in bech32_lnurl:
lnaddress = LnAddress(bech32_lnurl)
return get(lnaddress.url, response_class=response_class, verify=verify)
return await get(lnaddress.url, response_class=response_class)
lnurl = Lnurl(bech32_lnurl)
except (ValidationError, ValueError):
raise InvalidLnurl

if lnurl.is_login:
return LnurlAuthResponse(callback=lnurl.url, k1=lnurl.url.query_params["k1"])

return get(lnurl.url, response_class=response_class, verify=verify)
return await get(lnurl.url, response_class=response_class)


def execute(bech32_or_address: str, value: str) -> LnurlResponseModel:
async def execute(bech32_or_address: str, value: str) -> LnurlResponseModel:
try:
res = handle(bech32_or_address)
except Exception as exc:
raise LnurlResponseException(str(exc))

if isinstance(res, LnurlPayResponse) and res.tag == "payRequest":
return execute_pay_request(res, value)
return await execute_pay_request(res, value)
elif isinstance(res, LnurlAuthResponse) and res.tag == "login":
return execute_login(res, value)
return await execute_login(res, value)
elif isinstance(res, LnurlWithdrawResponse) and res.tag == "withdrawRequest":
return execute_withdraw(res, value)
return await execute_withdraw(res, value)

raise LnurlResponseException(f"{res.tag} not implemented") # type: ignore


def execute_pay_request(res: LnurlPayResponse, msat: str) -> LnurlResponseModel:
async def execute_pay_request(res: LnurlPayResponse, msat: str) -> LnurlResponseModel:
if not res.min_sendable <= MilliSatoshi(msat) <= res.max_sendable:
raise LnurlResponseException(f"Amount {msat} not in range {res.min_sendable} - {res.max_sendable}")
try:
req = requests.get(
res.callback,
params={
"amount": msat,
},
)
req.raise_for_status()
return LnurlResponse.from_dict(req.json())
async with httpx.AsyncClient() as client:
res2 = await client.get(
url=res.callback,
params={
"amount": msat,
},
)
res2.raise_for_status()
return LnurlResponse.from_dict(res2.json())
except Exception as exc:
raise LnurlResponseException(str(exc))


def execute_login(res: LnurlAuthResponse, secret: str) -> LnurlResponseModel:
async def execute_login(res: LnurlAuthResponse, secret: str) -> LnurlResponseModel:
try:
assert res.callback.host, "LNURLauth host does not exist"
key, sig = lnurlauth_signature(res.callback.host, secret, res.k1)
req = requests.get(
res.callback,
params={
"key": key,
"sig": sig,
},
)
req.raise_for_status()
return LnurlResponse.from_dict(req.json())
async with httpx.AsyncClient() as client:
res2 = await client.get(
url=res.callback,
params={
"key": key,
"sig": sig,
},
)
res2.raise_for_status()
return LnurlResponse.from_dict(res2.json())
except Exception as e:
raise LnurlResponseException(str(e))


def execute_withdraw(res: LnurlWithdrawResponse, pr: str) -> LnurlResponseModel:
async def execute_withdraw(res: LnurlWithdrawResponse, pr: str) -> LnurlResponseModel:
try:
invoice = bolt11_decode(pr)
except Bolt11Exception as exc:
Expand All @@ -117,14 +119,15 @@ def execute_withdraw(res: LnurlWithdrawResponse, pr: str) -> LnurlResponseModel:
if not res.min_withdrawable <= MilliSatoshi(amount) <= res.max_withdrawable:
raise LnurlResponseException(f"Amount {amount} not in range {res.min_withdrawable} - {res.max_withdrawable}")
try:
req = requests.get(
res.callback,
params={
"k1": res.k1,
"pr": pr,
},
)
req.raise_for_status()
return LnurlResponse.from_dict(req.json())
async with httpx.AsyncClient() as client:
res2 = await client.get(
url=res.callback,
params={
"k1": res.k1,
"pr": pr,
},
)
res2.raise_for_status()
return LnurlResponse.from_dict(res2.json())
except Exception as exc:
raise LnurlResponseException(str(exc))
Loading

0 comments on commit 6ad564d

Please sign in to comment.