commit
9e68a242e8
16
Pipfile
16
Pipfile
|
@ -14,28 +14,22 @@ environs = "*"
|
||||||
lnurl = "==0.3.6"
|
lnurl = "==0.3.6"
|
||||||
pyscss = "*"
|
pyscss = "*"
|
||||||
shortuuid = "*"
|
shortuuid = "*"
|
||||||
quart = "*"
|
|
||||||
quart-cors = "*"
|
|
||||||
quart-compress = "*"
|
|
||||||
typing-extensions = "*"
|
typing-extensions = "*"
|
||||||
httpx = "*"
|
httpx = "*"
|
||||||
quart-trio = "*"
|
|
||||||
trio = "==0.16.0"
|
|
||||||
sqlalchemy-aio = "*"
|
sqlalchemy-aio = "*"
|
||||||
embit = "*"
|
embit = "*"
|
||||||
pyqrcode = "*"
|
pyqrcode = "*"
|
||||||
pypng = "*"
|
pypng = "*"
|
||||||
sqlalchemy = "==1.3.23"
|
sqlalchemy = "==1.3.23"
|
||||||
psycopg2-binary = "*"
|
psycopg2-binary = "*"
|
||||||
|
aiofiles = "*"
|
||||||
|
asyncio = "*"
|
||||||
|
fastapi = "*"
|
||||||
|
uvicorn = {extras = ["standard"], version = "*"}
|
||||||
|
sse-starlette = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
black = "==20.8b1"
|
black = "==20.8b1"
|
||||||
pytest = "*"
|
pytest = "*"
|
||||||
pytest-cov = "*"
|
pytest-cov = "*"
|
||||||
mypy = "latest"
|
mypy = "latest"
|
||||||
pytest-trio = "*"
|
|
||||||
trio-typing = "*"
|
|
||||||
|
|
||||||
[packages.hypercorn]
|
|
||||||
extras = [ "trio",]
|
|
||||||
version = "*"
|
|
||||||
|
|
856
Pipfile.lock
generated
Normal file
856
Pipfile.lock
generated
Normal file
|
@ -0,0 +1,856 @@
|
||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "e26f678c4b89a86400e0a62396d06e360bfdf1e0f922d474ded200ee1ffde5c4"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {
|
||||||
|
"python_version": "3.7"
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"aiofiles": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4",
|
||||||
|
"sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.7.0"
|
||||||
|
},
|
||||||
|
"anyio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:929a6852074397afe1d989002aa96d457e3e1e5441357c60d03e7eea0e65e1b0",
|
||||||
|
"sha256:ae57a67583e5ff8b4af47666ff5651c3732d45fd26c929253748e796af860374"
|
||||||
|
],
|
||||||
|
"markers": "python_full_version >= '3.6.2'",
|
||||||
|
"version": "==3.3.0"
|
||||||
|
},
|
||||||
|
"asgiref": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9",
|
||||||
|
"sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==3.4.1"
|
||||||
|
},
|
||||||
|
"asyncio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41",
|
||||||
|
"sha256:b62c9157d36187eca799c378e572c969f0da87cd5fc42ca372d92cdb06e7e1de",
|
||||||
|
"sha256:c46a87b48213d7464f22d9a497b9eef8c1928b68320a2fa94240f969f6fec08c",
|
||||||
|
"sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.4.3"
|
||||||
|
},
|
||||||
|
"attrs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
|
||||||
|
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
|
"version": "==21.2.0"
|
||||||
|
},
|
||||||
|
"bech32": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899",
|
||||||
|
"sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==1.2.0"
|
||||||
|
},
|
||||||
|
"bitstring": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0de167daa6a00c9386255a7cac931b45e6e24e0ad7ea64f1f92a64ac23ad4578",
|
||||||
|
"sha256:a5848a3f63111785224dca8bb4c0a75b62ecdef56a042c8d6be74b16f7e860e7",
|
||||||
|
"sha256:e3e340e58900a948787a05e8c08772f1ccbe133f6f41fe3f0fa19a18a22bbf4f"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.1.9"
|
||||||
|
},
|
||||||
|
"cerberus": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.3.4"
|
||||||
|
},
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
|
||||||
|
"sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
|
||||||
|
],
|
||||||
|
"version": "==2021.5.30"
|
||||||
|
},
|
||||||
|
"charset-normalizer": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b",
|
||||||
|
"sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==2.0.4"
|
||||||
|
},
|
||||||
|
"click": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a",
|
||||||
|
"sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==8.0.1"
|
||||||
|
},
|
||||||
|
"ecdsa": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676",
|
||||||
|
"sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.17.0"
|
||||||
|
},
|
||||||
|
"embit": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:19f69929caf0d2fcfd4b708dd873384dfc36267944d02d5e6dfebc835f294e1b"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.4.6"
|
||||||
|
},
|
||||||
|
"environs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:72b867ff7b553076cdd90f3ee01ecc1cf854987639c9c459f0ed0d3d44ae490c",
|
||||||
|
"sha256:ee5466156b50fe03aa9fec6e720feea577b5bf515d7f21b2c46608272557ba26"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==9.3.3"
|
||||||
|
},
|
||||||
|
"fastapi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:644bb815bae326575c4b2842469fb83053a4b974b82fa792ff9283d17fbbd99d",
|
||||||
|
"sha256:94d2820906c36b9b8303796fb7271337ec89c74223229e3cfcf056b5a7d59e23"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.68.1"
|
||||||
|
},
|
||||||
|
"h11": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
|
||||||
|
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==0.12.0"
|
||||||
|
},
|
||||||
|
"httpcore": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b0d16f0012ec88d8cc848f5a55f8a03158405f4bca02ee49bc4ca2c1fda49f3e",
|
||||||
|
"sha256:db4c0dcb8323494d01b8c6d812d80091a31e520033e7b0120883d6f52da649ff"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==0.13.6"
|
||||||
|
},
|
||||||
|
"httptools": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:01b392a166adcc8bc2f526a939a8aabf89fe079243e1543fd0e7dc1b58d737cb",
|
||||||
|
"sha256:200fc1cdf733a9ff554c0bb97a4047785cfaad9875307d6087001db3eb2b417f",
|
||||||
|
"sha256:3ab1f390d8867f74b3b5ee2a7ecc9b8d7f53750bd45714bf1cb72a953d7dfa77",
|
||||||
|
"sha256:78d03dd39b09c99ec917d50189e6743adbfd18c15d5944392d2eabda688bf149",
|
||||||
|
"sha256:79dbc21f3612a78b28384e989b21872e2e3cf3968532601544696e4ed0007ce5",
|
||||||
|
"sha256:80ffa04fe8c8dfacf6e4cef8277347d35b0442c581f5814f3b0cf41b65c43c6e",
|
||||||
|
"sha256:813871f961edea6cb2fe312f2d9b27d12a51ba92545380126f80d0de1917ea15",
|
||||||
|
"sha256:94505026be56652d7a530ab03d89474dc6021019d6b8682281977163b3471ea0",
|
||||||
|
"sha256:a23166e5ae2775709cf4f7ad4c2048755ebfb272767d244e1a96d55ac775cca7",
|
||||||
|
"sha256:a289c27ccae399a70eacf32df9a44059ca2ba4ac444604b00a19a6c1f0809943",
|
||||||
|
"sha256:a7594f9a010cdf1e16a58b3bf26c9da39bbf663e3b8d46d39176999d71816658",
|
||||||
|
"sha256:b08d00d889a118f68f37f3c43e359aab24ee29eb2e3fe96d64c6a2ba8b9d6557",
|
||||||
|
"sha256:cc9be041e428c10f8b6ab358c6b393648f9457094e1dcc11b4906026d43cd380",
|
||||||
|
"sha256:d5682eeb10cca0606c4a8286a3391d4c3c5a36f0c448e71b8bd05be4e1694bfb",
|
||||||
|
"sha256:fd3b8905e21431ad306eeaf56644a68fdd621bf8f3097eff54d0f6bdf7262065"
|
||||||
|
],
|
||||||
|
"version": "==0.2.0"
|
||||||
|
},
|
||||||
|
"httpx": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:92ecd2c00c688b529eda11cedb15161eaf02dee9116712f621c70d9a40b2cdd0",
|
||||||
|
"sha256:9bd728a6c5ec0a9e243932a9983d57d3cc4a87bb4f554e1360fce407f78f9435"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.19.0"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
|
||||||
|
"sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
|
||||||
|
],
|
||||||
|
"version": "==3.2"
|
||||||
|
},
|
||||||
|
"importlib-metadata": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15",
|
||||||
|
"sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"
|
||||||
|
],
|
||||||
|
"markers": "python_version < '3.8'",
|
||||||
|
"version": "==4.8.1"
|
||||||
|
},
|
||||||
|
"lnurl": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:579982fd8c4d25bc84c61c74ec45cb7999fa1fa2426f5d5aeb0160ba333b9c92",
|
||||||
|
"sha256:8af07460115a48f3122a5a9c9a6062bee3897d5f6ab4c9a60f6561a83a8234f6"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.3.6"
|
||||||
|
},
|
||||||
|
"marshmallow": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c67929438fd73a2be92128caa0325b1b5ed8b626d91a094d2f7f2771bf1f1c0e",
|
||||||
|
"sha256:dd4724335d3c2b870b641ffe4a2f8728a1380cd2e7e2312756715ffeaa82b842"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==3.13.0"
|
||||||
|
},
|
||||||
|
"outcome": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c7dd9375cfd3c12db9801d080a3b63d4b0a261aa996c4c13152380587288d958",
|
||||||
|
"sha256:e862f01d4e626e63e8f92c38d1f8d5546d3f9cce989263c521b2e7990d186967"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==1.1.0"
|
||||||
|
},
|
||||||
|
"psycopg2-binary": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0b7dae87f0b729922e06f85f667de7bf16455d411971b2043bbd9577af9d1975",
|
||||||
|
"sha256:0f2e04bd2a2ab54fa44ee67fe2d002bb90cee1c0f1cc0ebc3148af7b02034cbd",
|
||||||
|
"sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616",
|
||||||
|
"sha256:1473c0215b0613dd938db54a653f68251a45a78b05f6fc21af4326f40e8360a2",
|
||||||
|
"sha256:14db1752acdd2187d99cb2ca0a1a6dfe57fc65c3281e0f20e597aac8d2a5bd90",
|
||||||
|
"sha256:1e3a362790edc0a365385b1ac4cc0acc429a0c0d662d829a50b6ce743ae61b5a",
|
||||||
|
"sha256:1e85b74cbbb3056e3656f1cc4781294df03383127a8114cbc6531e8b8367bf1e",
|
||||||
|
"sha256:20f1ab44d8c352074e2d7ca67dc00843067788791be373e67a0911998787ce7d",
|
||||||
|
"sha256:2f62c207d1740b0bde5c4e949f857b044818f734a3d57f1d0d0edc65050532ed",
|
||||||
|
"sha256:3242b9619de955ab44581a03a64bdd7d5e470cc4183e8fcadd85ab9d3756ce7a",
|
||||||
|
"sha256:35c4310f8febe41f442d3c65066ca93cccefd75013df3d8c736c5b93ec288140",
|
||||||
|
"sha256:4235f9d5ddcab0b8dbd723dca56ea2922b485ea00e1dafacf33b0c7e840b3d32",
|
||||||
|
"sha256:5ced67f1e34e1a450cdb48eb53ca73b60aa0af21c46b9b35ac3e581cf9f00e31",
|
||||||
|
"sha256:7360647ea04db2e7dff1648d1da825c8cf68dc5fbd80b8fb5b3ee9f068dcd21a",
|
||||||
|
"sha256:8c13d72ed6af7fd2c8acbd95661cf9477f94e381fce0792c04981a8283b52917",
|
||||||
|
"sha256:988b47ac70d204aed01589ed342303da7c4d84b56c2f4c4b8b00deda123372bf",
|
||||||
|
"sha256:995fc41ebda5a7a663a254a1dcac52638c3e847f48307b5416ee373da15075d7",
|
||||||
|
"sha256:a36c7eb6152ba5467fb264d73844877be8b0847874d4822b7cf2d3c0cb8cdcb0",
|
||||||
|
"sha256:aed4a9a7e3221b3e252c39d0bf794c438dc5453bc2963e8befe9d4cd324dff72",
|
||||||
|
"sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698",
|
||||||
|
"sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773",
|
||||||
|
"sha256:b4d7679a08fea64573c969f6994a2631908bb2c0e69a7235648642f3d2e39a68",
|
||||||
|
"sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76",
|
||||||
|
"sha256:ca86db5b561b894f9e5f115d6a159fff2a2570a652e07889d8a383b5fae66eb4",
|
||||||
|
"sha256:cfc523edecddaef56f6740d7de1ce24a2fdf94fd5e704091856a201872e37f9f",
|
||||||
|
"sha256:da113b70f6ec40e7d81b43d1b139b9db6a05727ab8be1ee559f3a69854a69d34",
|
||||||
|
"sha256:f6fac64a38f6768e7bc7b035b9e10d8a538a9fadce06b983fb3e6fa55ac5f5ce",
|
||||||
|
"sha256:f8559617b1fcf59a9aedba2c9838b5b6aa211ffedecabca412b92a1ff75aac1a",
|
||||||
|
"sha256:fbb42a541b1093385a2d8c7eec94d26d30437d0e77c1d25dae1dcc46741a385e"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.9.1"
|
||||||
|
},
|
||||||
|
"pydantic": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd",
|
||||||
|
"sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739",
|
||||||
|
"sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f",
|
||||||
|
"sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840",
|
||||||
|
"sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23",
|
||||||
|
"sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287",
|
||||||
|
"sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62",
|
||||||
|
"sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b",
|
||||||
|
"sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb",
|
||||||
|
"sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820",
|
||||||
|
"sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3",
|
||||||
|
"sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b",
|
||||||
|
"sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e",
|
||||||
|
"sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3",
|
||||||
|
"sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316",
|
||||||
|
"sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b",
|
||||||
|
"sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4",
|
||||||
|
"sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20",
|
||||||
|
"sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e",
|
||||||
|
"sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505",
|
||||||
|
"sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1",
|
||||||
|
"sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"
|
||||||
|
],
|
||||||
|
"markers": "python_full_version >= '3.6.1'",
|
||||||
|
"version": "==1.8.2"
|
||||||
|
},
|
||||||
|
"pypng": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:76f8a1539ec56451da7ab7121f12a361969fe0f2d48d703d198ce2a99d6c5afd"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.0.21"
|
||||||
|
},
|
||||||
|
"pyqrcode": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1b2812775fa6ff5c527977c4cd2ccb07051ca7d0bc0aecf937a43864abe5eff6",
|
||||||
|
"sha256:fdbf7634733e56b72e27f9bce46e4550b75a3a2c420414035cae9d9d26b234d5"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.2.1"
|
||||||
|
},
|
||||||
|
"pyscss": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:f1df571569021a23941a538eb154405dde80bed35dc1ea7c5f3e18e0144746bf"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.3.7"
|
||||||
|
},
|
||||||
|
"python-dotenv": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1",
|
||||||
|
"sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==0.19.0"
|
||||||
|
},
|
||||||
|
"pyyaml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf",
|
||||||
|
"sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696",
|
||||||
|
"sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393",
|
||||||
|
"sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77",
|
||||||
|
"sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922",
|
||||||
|
"sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5",
|
||||||
|
"sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8",
|
||||||
|
"sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10",
|
||||||
|
"sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc",
|
||||||
|
"sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018",
|
||||||
|
"sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e",
|
||||||
|
"sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253",
|
||||||
|
"sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347",
|
||||||
|
"sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183",
|
||||||
|
"sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541",
|
||||||
|
"sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb",
|
||||||
|
"sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185",
|
||||||
|
"sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc",
|
||||||
|
"sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db",
|
||||||
|
"sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa",
|
||||||
|
"sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46",
|
||||||
|
"sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122",
|
||||||
|
"sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b",
|
||||||
|
"sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63",
|
||||||
|
"sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df",
|
||||||
|
"sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc",
|
||||||
|
"sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247",
|
||||||
|
"sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6",
|
||||||
|
"sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"
|
||||||
|
],
|
||||||
|
"version": "==5.4.1"
|
||||||
|
},
|
||||||
|
"represent": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:026c0de2ee8385d1255b9c2426cd4f03fe9177ac94c09979bc601946c8493aa0",
|
||||||
|
"sha256:99142650756ef1998ce0661568f54a47dac8c638fb27e3816c02536575dbba8c"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==1.6.0.post0"
|
||||||
|
},
|
||||||
|
"rfc3986": {
|
||||||
|
"extras": [
|
||||||
|
"idna2008"
|
||||||
|
],
|
||||||
|
"hashes": [
|
||||||
|
"sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835",
|
||||||
|
"sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"
|
||||||
|
],
|
||||||
|
"version": "==1.5.0"
|
||||||
|
},
|
||||||
|
"shortuuid": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3c11d2007b915c43bee3e10625f068d8a349e04f0d81f08f5fa08507427ebf1f",
|
||||||
|
"sha256:492c7402ff91beb1342a5898bd61ea953985bf24a41cd9f247409aa2e03c8f77"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.0.1"
|
||||||
|
},
|
||||||
|
"six": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||||
|
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==1.16.0"
|
||||||
|
},
|
||||||
|
"sniffio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
|
||||||
|
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==1.2.0"
|
||||||
|
},
|
||||||
|
"sqlalchemy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:040bdfc1d76a9074717a3f43455685f781c581f94472b010cd6c4754754e1862",
|
||||||
|
"sha256:1fe5d8d39118c2b018c215c37b73fd6893c3e1d4895be745ca8ff6eb83333ed3",
|
||||||
|
"sha256:23927c3981d1ec6b4ea71eb99d28424b874d9c696a21e5fbd9fa322718be3708",
|
||||||
|
"sha256:24f9569e82a009a09ce2d263559acb3466eba2617203170e4a0af91e75b4f075",
|
||||||
|
"sha256:2578dbdbe4dbb0e5126fb37ffcd9793a25dcad769a95f171a2161030bea850ff",
|
||||||
|
"sha256:269990b3ab53cb035d662dcde51df0943c1417bdab707dc4a7e4114a710504b4",
|
||||||
|
"sha256:29cccc9606750fe10c5d0e8bd847f17a97f3850b8682aef1f56f5d5e1a5a64b1",
|
||||||
|
"sha256:37b83bf81b4b85dda273aaaed5f35ea20ad80606f672d94d2218afc565fb0173",
|
||||||
|
"sha256:63677d0c08524af4c5893c18dbe42141de7178001360b3de0b86217502ed3601",
|
||||||
|
"sha256:639940bbe1108ac667dcffc79925db2966826c270112e9159439ab6bb14f8d80",
|
||||||
|
"sha256:6a939a868fdaa4b504e8b9d4a61f21aac11e3fecc8a8214455e144939e3d2aea",
|
||||||
|
"sha256:6b8b8c80c7f384f06825612dd078e4a31f0185e8f1f6b8c19e188ff246334205",
|
||||||
|
"sha256:6c9e6cc9237de5660bcddea63f332428bb83c8e2015c26777281f7ffbd2efb84",
|
||||||
|
"sha256:6ec1044908414013ebfe363450c22f14698803ce97fbb47e53284d55c5165848",
|
||||||
|
"sha256:6fca33672578666f657c131552c4ef8979c1606e494f78cd5199742dfb26918b",
|
||||||
|
"sha256:751934967f5336a3e26fc5993ccad1e4fee982029f9317eb6153bc0bc3d2d2da",
|
||||||
|
"sha256:8be835aac18ec85351385e17b8665bd4d63083a7160a017bef3d640e8e65cadb",
|
||||||
|
"sha256:927ce09e49bff3104459e1451ce82983b0a3062437a07d883a4c66f0b344c9b5",
|
||||||
|
"sha256:94208867f34e60f54a33a37f1c117251be91a47e3bfdb9ab8a7847f20886ad06",
|
||||||
|
"sha256:94f667d86be82dd4cb17d08de0c3622e77ca865320e0b95eae6153faa7b4ecaf",
|
||||||
|
"sha256:9e9c25522933e569e8b53ccc644dc993cab87e922fb7e142894653880fdd419d",
|
||||||
|
"sha256:a0e306e9bb76fd93b29ae3a5155298e4c1b504c7cbc620c09c20858d32d16234",
|
||||||
|
"sha256:a8bfc1e1afe523e94974132d7230b82ca7fa2511aedde1f537ec54db0399541a",
|
||||||
|
"sha256:ac2244e64485c3778f012951fdc869969a736cd61375fde6096d08850d8be729",
|
||||||
|
"sha256:b4b0e44d586cd64b65b507fa116a3814a1a53d55dce4836d7c1a6eb2823ff8d1",
|
||||||
|
"sha256:baeb451ee23e264de3f577fee5283c73d9bbaa8cb921d0305c0bbf700094b65b",
|
||||||
|
"sha256:c7dc052432cd5d060d7437e217dd33c97025287f99a69a50e2dc1478dd610d64",
|
||||||
|
"sha256:d1a85dfc5dee741bf49cb9b6b6b8d2725a268e4992507cf151cba26b17d97c37",
|
||||||
|
"sha256:d90010304abb4102123d10cbad2cdf2c25a9f2e66a50974199b24b468509bad5",
|
||||||
|
"sha256:ddfb511e76d016c3a160910642d57f4587dc542ce5ee823b0d415134790eeeb9",
|
||||||
|
"sha256:e273367f4076bd7b9a8dc2e771978ef2bfd6b82526e80775a7db52bff8ca01dd",
|
||||||
|
"sha256:e5bb3463df697279e5459a7316ad5a60b04b0107f9392e88674d0ece70e9cf70",
|
||||||
|
"sha256:e8a1750b44ad6422ace82bf3466638f1aa0862dbb9689690d5f2f48cce3476c8",
|
||||||
|
"sha256:eab063a70cca4a587c28824e18be41d8ecc4457f8f15b2933584c6c6cccd30f0",
|
||||||
|
"sha256:ecce8c021894a77d89808222b1ff9687ad84db54d18e4bd0500ca766737faaf6",
|
||||||
|
"sha256:f4d972139d5000105fcda9539a76452039434013570d6059993120dc2a65e447",
|
||||||
|
"sha256:fd3b96f8c705af8e938eaa99cbd8fd1450f632d38cad55e7367c33b263bf98ec",
|
||||||
|
"sha256:fdd2ed7395df8ac2dbb10cefc44737b66c6a5cd7755c92524733d7a443e5b7e2"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.3.23"
|
||||||
|
},
|
||||||
|
"sqlalchemy-aio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7f77366f55d34891c87386dd0962a28b948b684e8ea5edb7daae4187c0b291bf",
|
||||||
|
"sha256:f767320427c22c66fa5840a1f17f3261110a8ddc8560558f4fbf12d31a66b17b"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.16.0"
|
||||||
|
},
|
||||||
|
"sse-starlette": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1c0cc62cc7d021a386dc06a16a9ddc3e2861d19da6bc2e654e65cc111e820456"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.6.2"
|
||||||
|
},
|
||||||
|
"starlette": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3c8e48e52736b3161e34c9f0e8153b4f32ec5d8995a3ee1d59410d92f75162ed",
|
||||||
|
"sha256:7d49f4a27f8742262ef1470608c59ddbc66baf37c148e938c7038e6bc7a998aa"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==0.14.2"
|
||||||
|
},
|
||||||
|
"typing-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:045dd532231acfa03628df5e0c66dba64e2cc8fc8b844538d4ad6d5dd6cb82dc",
|
||||||
|
"sha256:83af6730a045fda60f46510f7f1f094776d90321caa4d97d20ef38871bef4bd3",
|
||||||
|
"sha256:8bbffbd37fbeb9747a0241fdfde5ae99d4531ad1d1a41ccaea62100e15a5814c"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.10.0.1"
|
||||||
|
},
|
||||||
|
"uvicorn": {
|
||||||
|
"extras": [
|
||||||
|
"standard"
|
||||||
|
],
|
||||||
|
"hashes": [
|
||||||
|
"sha256:17f898c64c71a2640514d4089da2689e5db1ce5d4086c2d53699bf99513421c1",
|
||||||
|
"sha256:d9a3c0dd1ca86728d3e235182683b4cf94cd53a867c288eaeca80ee781b2caff"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.15.0"
|
||||||
|
},
|
||||||
|
"uvloop": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450",
|
||||||
|
"sha256:089b4834fd299d82d83a25e3335372f12117a7d38525217c2258e9b9f4578897",
|
||||||
|
"sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861",
|
||||||
|
"sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c",
|
||||||
|
"sha256:3a19828c4f15687675ea912cc28bbcb48e9bb907c801873bd1519b96b04fb805",
|
||||||
|
"sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d",
|
||||||
|
"sha256:647e481940379eebd314c00440314c81ea547aa636056f554d491e40503c8464",
|
||||||
|
"sha256:6ccd57ae8db17d677e9e06192e9c9ec4bd2066b77790f9aa7dede2cc4008ee8f",
|
||||||
|
"sha256:772206116b9b57cd625c8a88f2413df2fcfd0b496eb188b82a43bed7af2c2ec9",
|
||||||
|
"sha256:8e0d26fa5875d43ddbb0d9d79a447d2ace4180d9e3239788208527c4784f7cab",
|
||||||
|
"sha256:98d117332cc9e5ea8dfdc2b28b0a23f60370d02e1395f88f40d1effd2cb86c4f",
|
||||||
|
"sha256:b572256409f194521a9895aef274cea88731d14732343da3ecdb175228881638",
|
||||||
|
"sha256:bd53f7f5db562f37cd64a3af5012df8cac2c464c97e732ed556800129505bd64",
|
||||||
|
"sha256:bd8f42ea1ea8f4e84d265769089964ddda95eb2bb38b5cbe26712b0616c3edee",
|
||||||
|
"sha256:e814ac2c6f9daf4c36eb8e85266859f42174a4ff0d71b99405ed559257750382",
|
||||||
|
"sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"
|
||||||
|
],
|
||||||
|
"version": "==0.16.0"
|
||||||
|
},
|
||||||
|
"watchgod": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:48140d62b0ebe9dd9cf8381337f06351e1f2e70b2203fa9c6eff4e572ca84f29",
|
||||||
|
"sha256:d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7"
|
||||||
|
],
|
||||||
|
"version": "==0.7"
|
||||||
|
},
|
||||||
|
"websockets": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0dd4eb8e0bbf365d6f652711ce21b8fd2b596f873d32aabb0fbb53ec604418cc",
|
||||||
|
"sha256:1d0971cc7251aeff955aa742ec541ee8aaea4bb2ebf0245748fbec62f744a37e",
|
||||||
|
"sha256:1d6b4fddb12ab9adf87b843cd4316c4bd602db8d5efd2fb83147f0458fe85135",
|
||||||
|
"sha256:230a3506df6b5f446fed2398e58dcaafdff12d67fe1397dff196411a9e820d02",
|
||||||
|
"sha256:276d2339ebf0df4f45df453923ebd2270b87900eda5dfd4a6b0cfa15f82111c3",
|
||||||
|
"sha256:2cf04601633a4ec176b9cc3d3e73789c037641001dbfaf7c411f89cd3e04fcaf",
|
||||||
|
"sha256:3ddff38894c7857c476feb3538dd847514379d6dc844961dc99f04b0384b1b1b",
|
||||||
|
"sha256:48c222feb3ced18f3dc61168ca18952a22fb88e5eb8902d2bf1b50faefdc34a2",
|
||||||
|
"sha256:51d04df04ed9d08077d10ccbe21e6805791b78eac49d16d30a1f1fe2e44ba0af",
|
||||||
|
"sha256:597c28f3aa7a09e8c070a86b03107094ee5cdafcc0d55f2f2eac92faac8dc67d",
|
||||||
|
"sha256:5c8f0d82ea2468282e08b0cf5307f3ad022290ed50c45d5cb7767957ca782880",
|
||||||
|
"sha256:7189e51955f9268b2bdd6cc537e0faa06f8fffda7fb386e5922c6391de51b077",
|
||||||
|
"sha256:7df3596838b2a0c07c6f6d67752c53859a54993d4f062689fdf547cb56d0f84f",
|
||||||
|
"sha256:826ccf85d4514609219725ba4a7abd569228c2c9f1968e8be05be366f68291ec",
|
||||||
|
"sha256:836d14eb53b500fd92bd5db2fc5894f7c72b634f9c2a28f546f75967503d8e25",
|
||||||
|
"sha256:85db8090ba94e22d964498a47fdd933b8875a1add6ebc514c7ac8703eb97bbf0",
|
||||||
|
"sha256:85e701a6c316b7067f1e8675c638036a796fe5116783a4c932e7eb8e305a3ffe",
|
||||||
|
"sha256:900589e19200be76dd7cbaa95e9771605b5ce3f62512d039fb3bc5da9014912a",
|
||||||
|
"sha256:9147868bb0cc01e6846606cd65cbf9c58598f187b96d14dd1ca17338b08793bb",
|
||||||
|
"sha256:9e7fdc775fe7403dbd8bc883ba59576a6232eac96dacb56512daacf7af5d618d",
|
||||||
|
"sha256:ab5ee15d3462198c794c49ccd31773d8a2b8c17d622aa184f669d2b98c2f0857",
|
||||||
|
"sha256:ad893d889bc700a5835e0a95a3e4f2c39e91577ab232a3dc03c262a0f8fc4b5c",
|
||||||
|
"sha256:b2e71c4670ebe1067fa8632f0d081e47254ee2d3d409de54168b43b0ba9147e0",
|
||||||
|
"sha256:b43b13e5622c5a53ab12f3272e6f42f1ce37cd5b6684b2676cb365403295cd40",
|
||||||
|
"sha256:b4ad84b156cf50529b8ac5cc1638c2cf8680490e3fccb6121316c8c02620a2e4",
|
||||||
|
"sha256:be5fd35e99970518547edc906efab29afd392319f020c3c58b0e1a158e16ed20",
|
||||||
|
"sha256:caa68c95bc1776d3521f81eeb4d5b9438be92514ec2a79fececda814099c8314",
|
||||||
|
"sha256:d144b350045c53c8ff09aa1cfa955012dd32f00c7e0862c199edcabb1a8b32da",
|
||||||
|
"sha256:d2c2d9b24d3c65b5a02cac12cbb4e4194e590314519ed49db2f67ef561c3cf58",
|
||||||
|
"sha256:e9e5fd6dbdf95d99bc03732ded1fc8ef22ebbc05999ac7e0c7bf57fe6e4e5ae2",
|
||||||
|
"sha256:ebf459a1c069f9866d8569439c06193c586e72c9330db1390af7c6a0a32c4afd",
|
||||||
|
"sha256:f31722f1c033c198aa4a39a01905951c00bd1c74f922e8afc1b1c62adbcdd56a",
|
||||||
|
"sha256:f68c352a68e5fdf1e97288d5cec9296664c590c25932a8476224124aaf90dbcd"
|
||||||
|
],
|
||||||
|
"version": "==9.1"
|
||||||
|
},
|
||||||
|
"zipp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3",
|
||||||
|
"sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==3.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {
|
||||||
|
"appdirs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
|
||||||
|
"sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
|
||||||
|
],
|
||||||
|
"version": "==1.4.4"
|
||||||
|
},
|
||||||
|
"attrs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
|
||||||
|
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
|
"version": "==21.2.0"
|
||||||
|
},
|
||||||
|
"black": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==20.8b1"
|
||||||
|
},
|
||||||
|
"click": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a",
|
||||||
|
"sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==8.0.1"
|
||||||
|
},
|
||||||
|
"coverage": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c",
|
||||||
|
"sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6",
|
||||||
|
"sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45",
|
||||||
|
"sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a",
|
||||||
|
"sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03",
|
||||||
|
"sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529",
|
||||||
|
"sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a",
|
||||||
|
"sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a",
|
||||||
|
"sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2",
|
||||||
|
"sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6",
|
||||||
|
"sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759",
|
||||||
|
"sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53",
|
||||||
|
"sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a",
|
||||||
|
"sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4",
|
||||||
|
"sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff",
|
||||||
|
"sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502",
|
||||||
|
"sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793",
|
||||||
|
"sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb",
|
||||||
|
"sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905",
|
||||||
|
"sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821",
|
||||||
|
"sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b",
|
||||||
|
"sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81",
|
||||||
|
"sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0",
|
||||||
|
"sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b",
|
||||||
|
"sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3",
|
||||||
|
"sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184",
|
||||||
|
"sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701",
|
||||||
|
"sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a",
|
||||||
|
"sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82",
|
||||||
|
"sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638",
|
||||||
|
"sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5",
|
||||||
|
"sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083",
|
||||||
|
"sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6",
|
||||||
|
"sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90",
|
||||||
|
"sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465",
|
||||||
|
"sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a",
|
||||||
|
"sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3",
|
||||||
|
"sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e",
|
||||||
|
"sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066",
|
||||||
|
"sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf",
|
||||||
|
"sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b",
|
||||||
|
"sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae",
|
||||||
|
"sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669",
|
||||||
|
"sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873",
|
||||||
|
"sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b",
|
||||||
|
"sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6",
|
||||||
|
"sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb",
|
||||||
|
"sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160",
|
||||||
|
"sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c",
|
||||||
|
"sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079",
|
||||||
|
"sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d",
|
||||||
|
"sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||||
|
"version": "==5.5"
|
||||||
|
},
|
||||||
|
"importlib-metadata": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15",
|
||||||
|
"sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"
|
||||||
|
],
|
||||||
|
"markers": "python_version < '3.8'",
|
||||||
|
"version": "==4.8.1"
|
||||||
|
},
|
||||||
|
"iniconfig": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
|
||||||
|
"sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
|
||||||
|
],
|
||||||
|
"version": "==1.1.1"
|
||||||
|
},
|
||||||
|
"mypy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9",
|
||||||
|
"sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a",
|
||||||
|
"sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9",
|
||||||
|
"sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e",
|
||||||
|
"sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2",
|
||||||
|
"sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212",
|
||||||
|
"sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b",
|
||||||
|
"sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885",
|
||||||
|
"sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150",
|
||||||
|
"sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703",
|
||||||
|
"sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072",
|
||||||
|
"sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457",
|
||||||
|
"sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e",
|
||||||
|
"sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0",
|
||||||
|
"sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb",
|
||||||
|
"sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97",
|
||||||
|
"sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8",
|
||||||
|
"sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811",
|
||||||
|
"sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6",
|
||||||
|
"sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de",
|
||||||
|
"sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504",
|
||||||
|
"sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921",
|
||||||
|
"sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.910"
|
||||||
|
},
|
||||||
|
"mypy-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
|
||||||
|
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
|
||||||
|
],
|
||||||
|
"version": "==0.4.3"
|
||||||
|
},
|
||||||
|
"packaging": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
|
||||||
|
"sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==21.0"
|
||||||
|
},
|
||||||
|
"pathspec": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a",
|
||||||
|
"sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"
|
||||||
|
],
|
||||||
|
"version": "==0.9.0"
|
||||||
|
},
|
||||||
|
"pluggy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||||
|
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==0.13.1"
|
||||||
|
},
|
||||||
|
"py": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
|
||||||
|
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==1.10.0"
|
||||||
|
},
|
||||||
|
"pyparsing": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||||
|
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==2.4.7"
|
||||||
|
},
|
||||||
|
"pytest": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b",
|
||||||
|
"sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==6.2.4"
|
||||||
|
},
|
||||||
|
"pytest-cov": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a",
|
||||||
|
"sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.12.1"
|
||||||
|
},
|
||||||
|
"regex": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468",
|
||||||
|
"sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354",
|
||||||
|
"sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308",
|
||||||
|
"sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d",
|
||||||
|
"sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc",
|
||||||
|
"sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8",
|
||||||
|
"sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797",
|
||||||
|
"sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2",
|
||||||
|
"sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13",
|
||||||
|
"sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d",
|
||||||
|
"sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a",
|
||||||
|
"sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0",
|
||||||
|
"sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73",
|
||||||
|
"sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1",
|
||||||
|
"sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed",
|
||||||
|
"sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a",
|
||||||
|
"sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b",
|
||||||
|
"sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f",
|
||||||
|
"sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256",
|
||||||
|
"sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb",
|
||||||
|
"sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2",
|
||||||
|
"sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983",
|
||||||
|
"sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb",
|
||||||
|
"sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645",
|
||||||
|
"sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8",
|
||||||
|
"sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a",
|
||||||
|
"sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906",
|
||||||
|
"sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f",
|
||||||
|
"sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c",
|
||||||
|
"sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892",
|
||||||
|
"sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0",
|
||||||
|
"sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e",
|
||||||
|
"sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e",
|
||||||
|
"sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed",
|
||||||
|
"sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c",
|
||||||
|
"sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374",
|
||||||
|
"sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd",
|
||||||
|
"sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791",
|
||||||
|
"sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a",
|
||||||
|
"sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1",
|
||||||
|
"sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"
|
||||||
|
],
|
||||||
|
"version": "==2021.8.28"
|
||||||
|
},
|
||||||
|
"toml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
|
||||||
|
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==0.10.2"
|
||||||
|
},
|
||||||
|
"typed-ast": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace",
|
||||||
|
"sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff",
|
||||||
|
"sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266",
|
||||||
|
"sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528",
|
||||||
|
"sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6",
|
||||||
|
"sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808",
|
||||||
|
"sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4",
|
||||||
|
"sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363",
|
||||||
|
"sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341",
|
||||||
|
"sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04",
|
||||||
|
"sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41",
|
||||||
|
"sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e",
|
||||||
|
"sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3",
|
||||||
|
"sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899",
|
||||||
|
"sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805",
|
||||||
|
"sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c",
|
||||||
|
"sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c",
|
||||||
|
"sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39",
|
||||||
|
"sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a",
|
||||||
|
"sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3",
|
||||||
|
"sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7",
|
||||||
|
"sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f",
|
||||||
|
"sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075",
|
||||||
|
"sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0",
|
||||||
|
"sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40",
|
||||||
|
"sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428",
|
||||||
|
"sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927",
|
||||||
|
"sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3",
|
||||||
|
"sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f",
|
||||||
|
"sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"
|
||||||
|
],
|
||||||
|
"markers": "python_version < '3.8'",
|
||||||
|
"version": "==1.4.3"
|
||||||
|
},
|
||||||
|
"typing-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:045dd532231acfa03628df5e0c66dba64e2cc8fc8b844538d4ad6d5dd6cb82dc",
|
||||||
|
"sha256:83af6730a045fda60f46510f7f1f094776d90321caa4d97d20ef38871bef4bd3",
|
||||||
|
"sha256:8bbffbd37fbeb9747a0241fdfde5ae99d4531ad1d1a41ccaea62100e15a5814c"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.10.0.1"
|
||||||
|
},
|
||||||
|
"zipp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3",
|
||||||
|
"sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==3.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,28 +1,21 @@
|
||||||
from hypercorn.trio import serve
|
import asyncio
|
||||||
import trio
|
|
||||||
import trio_asyncio
|
|
||||||
from hypercorn.config import Config
|
|
||||||
|
|
||||||
from .commands import migrate_databases, transpile_scss, bundle_vendored
|
import uvloop
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
trio.run(migrate_databases)
|
from .commands import bundle_vendored, migrate_databases, transpile_scss
|
||||||
|
from .settings import (DEBUG, LNBITS_COMMIT, LNBITS_DATA_FOLDER,
|
||||||
|
LNBITS_SITE_TITLE, PORT, SERVICE_FEE, WALLET)
|
||||||
|
|
||||||
|
uvloop.install()
|
||||||
|
|
||||||
|
asyncio.create_task(migrate_databases())
|
||||||
transpile_scss()
|
transpile_scss()
|
||||||
bundle_vendored()
|
bundle_vendored()
|
||||||
|
|
||||||
from .app import create_app
|
from .app import create_app
|
||||||
|
|
||||||
app = trio.run(create_app)
|
app = create_app()
|
||||||
|
|
||||||
from .settings import (
|
|
||||||
LNBITS_SITE_TITLE,
|
|
||||||
SERVICE_FEE,
|
|
||||||
DEBUG,
|
|
||||||
LNBITS_DATA_FOLDER,
|
|
||||||
WALLET,
|
|
||||||
LNBITS_COMMIT,
|
|
||||||
HOST,
|
|
||||||
PORT
|
|
||||||
)
|
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"""Starting LNbits with
|
f"""Starting LNbits with
|
||||||
|
@ -35,6 +28,3 @@ print(
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
config = Config()
|
|
||||||
config.bind = [f"{HOST}:{PORT}"]
|
|
||||||
trio_asyncio.run(serve, app, config)
|
|
||||||
|
|
109
lnbits/app.py
109
lnbits/app.py
|
@ -1,38 +1,30 @@
|
||||||
import jinja2
|
import asyncio
|
||||||
from lnbits.jinja2_templating import Jinja2Templates
|
|
||||||
import sys
|
|
||||||
import warnings
|
|
||||||
import importlib
|
import importlib
|
||||||
|
from lnbits.core.tasks import register_task_listeners
|
||||||
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import trio
|
import warnings
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.exceptions import RequestValidationError
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.middleware.gzip import GZipMiddleware
|
from fastapi.middleware.gzip import GZipMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from .commands import db_migrate, handle_assets
|
|
||||||
from .core import core_app
|
|
||||||
from .helpers import (
|
|
||||||
get_valid_extensions,
|
|
||||||
get_js_vendored,
|
|
||||||
get_css_vendored,
|
|
||||||
url_for_vendored,
|
|
||||||
)
|
|
||||||
from .proxy_fix import ASGIProxyFix
|
|
||||||
from .tasks import (
|
|
||||||
webhook_handler,
|
|
||||||
invoice_listener,
|
|
||||||
run_deferred_async,
|
|
||||||
check_pending_payments,
|
|
||||||
internal_invoice_listener,
|
|
||||||
catch_everything_and_restart,
|
|
||||||
)
|
|
||||||
from .settings import WALLET
|
|
||||||
from .requestvars import g, request_global
|
|
||||||
import lnbits.settings
|
import lnbits.settings
|
||||||
|
|
||||||
async def create_app(config_object="lnbits.settings") -> FastAPI:
|
from .commands import db_migrate, handle_assets
|
||||||
|
from .core import core_app
|
||||||
|
from .core.views.generic import core_html_routes
|
||||||
|
from .helpers import (get_css_vendored, get_js_vendored, get_valid_extensions,
|
||||||
|
template_renderer, url_for_vendored)
|
||||||
|
from .requestvars import g
|
||||||
|
from .settings import WALLET
|
||||||
|
from .tasks import (catch_everything_and_restart, check_pending_payments, internal_invoice_listener,
|
||||||
|
invoice_listener, run_deferred_async, webhook_handler)
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(config_object="lnbits.settings") -> FastAPI:
|
||||||
"""Create application factory.
|
"""Create application factory.
|
||||||
:param config_object: The configuration object to use.
|
:param config_object: The configuration object to use.
|
||||||
"""
|
"""
|
||||||
|
@ -54,7 +46,16 @@ async def create_app(config_object="lnbits.settings") -> FastAPI:
|
||||||
)
|
)
|
||||||
|
|
||||||
g().config = lnbits.settings
|
g().config = lnbits.settings
|
||||||
g().templates = build_standard_jinja_templates()
|
g().base_url = f"http://{lnbits.settings.HOST}:{lnbits.settings.PORT}"
|
||||||
|
|
||||||
|
@app.exception_handler(RequestValidationError)
|
||||||
|
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||||
|
return template_renderer().TemplateResponse("error.html", {"request": request, "err": f"`{exc.errors()}` is not a valid UUID."})
|
||||||
|
|
||||||
|
# return HTMLResponse(
|
||||||
|
# status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
# content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
|
||||||
|
# )
|
||||||
|
|
||||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||||
# app.add_middleware(ASGIProxyFix)
|
# app.add_middleware(ASGIProxyFix)
|
||||||
|
@ -68,26 +69,6 @@ async def create_app(config_object="lnbits.settings") -> FastAPI:
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
def build_standard_jinja_templates():
|
|
||||||
t = Jinja2Templates(
|
|
||||||
loader=jinja2.FileSystemLoader(["lnbits/templates", "lnbits/core/templates"]),
|
|
||||||
)
|
|
||||||
t.env.globals["SITE_TITLE"] = lnbits.settings.LNBITS_SITE_TITLE
|
|
||||||
t.env.globals["SITE_TAGLINE"] = lnbits.settings.LNBITS_SITE_TAGLINE
|
|
||||||
t.env.globals["SITE_DESCRIPTION"] = lnbits.settings.LNBITS_SITE_DESCRIPTION
|
|
||||||
t.env.globals["LNBITS_THEME_OPTIONS"] = lnbits.settings.LNBITS_THEME_OPTIONS
|
|
||||||
t.env.globals["LNBITS_VERSION"] = lnbits.settings.LNBITS_COMMIT
|
|
||||||
t.env.globals["EXTENSIONS"] = get_valid_extensions()
|
|
||||||
|
|
||||||
if g().config.DEBUG:
|
|
||||||
t.env.globals["VENDORED_JS"] = map(url_for_vendored, get_js_vendored())
|
|
||||||
t.env.globals["VENDORED_CSS"] = map(url_for_vendored, get_css_vendored())
|
|
||||||
else:
|
|
||||||
t.env.globals["VENDORED_JS"] = ["/static/bundle.js"]
|
|
||||||
t.env.globals["VENDORED_CSS"] = ["/static/bundle.css"]
|
|
||||||
|
|
||||||
return t
|
|
||||||
|
|
||||||
def check_funding_source(app: FastAPI) -> None:
|
def check_funding_source(app: FastAPI) -> None:
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def check_wallet_status():
|
async def check_wallet_status():
|
||||||
|
@ -106,8 +87,9 @@ def check_funding_source(app: FastAPI) -> None:
|
||||||
|
|
||||||
|
|
||||||
def register_routes(app: FastAPI) -> None:
|
def register_routes(app: FastAPI) -> None:
|
||||||
"""Register Flask blueprints / LNbits extensions."""
|
"""Register FastAPI routes / LNbits extensions."""
|
||||||
app.include_router(core_app)
|
app.include_router(core_app)
|
||||||
|
app.include_router(core_html_routes)
|
||||||
|
|
||||||
for ext in get_valid_extensions():
|
for ext in get_valid_extensions():
|
||||||
try:
|
try:
|
||||||
|
@ -147,36 +129,23 @@ def register_async_tasks(app):
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def listeners():
|
async def listeners():
|
||||||
run_deferred_async()
|
loop = asyncio.get_event_loop()
|
||||||
trio.open_process(check_pending_payments)
|
loop.create_task(catch_everything_and_restart(check_pending_payments))
|
||||||
trio.open_process(invoice_listener)
|
loop.create_task(catch_everything_and_restart(invoice_listener))
|
||||||
trio.open_process(internal_invoice_listener)
|
loop.create_task(catch_everything_and_restart(internal_invoice_listener))
|
||||||
|
await register_task_listeners()
|
||||||
async with trio.open_nursery() as n:
|
await run_deferred_async()
|
||||||
pass
|
|
||||||
# n.start_soon(catch_everything_and_restart, check_pending_payments)
|
|
||||||
# n.start_soon(catch_everything_and_restart, invoice_listener)
|
|
||||||
# n.start_soon(catch_everything_and_restart, internal_invoice_listener)
|
|
||||||
|
|
||||||
@app.on_event("shutdown")
|
@app.on_event("shutdown")
|
||||||
async def stop_listeners():
|
async def stop_listeners():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def register_exception_handlers(app):
|
def register_exception_handlers(app):
|
||||||
@app.errorhandler(Exception)
|
@app.errorhandler(Exception)
|
||||||
async def basic_error(err):
|
async def basic_error(request: Request, err):
|
||||||
print("handled error", traceback.format_exc())
|
print("handled error", traceback.format_exc())
|
||||||
etype, value, tb = sys.exc_info()
|
etype, value, tb = sys.exc_info()
|
||||||
traceback.print_exception(etype, err, tb)
|
traceback.print_exception(etype, err, tb)
|
||||||
exc = traceback.format_exc()
|
exc = traceback.format_exc()
|
||||||
return (
|
return template_renderer().TemplateResponse("error.html", {"request": request, "err": err})
|
||||||
"\n\n".join(
|
|
||||||
[
|
|
||||||
"LNbits internal error!",
|
|
||||||
exc,
|
|
||||||
"If you believe this shouldn't be an error please bring it up on https://t.me/lnbits",
|
|
||||||
]
|
|
||||||
),
|
|
||||||
500,
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import trio
|
import asyncio
|
||||||
import warnings
|
import warnings
|
||||||
import click
|
import click
|
||||||
import importlib
|
import importlib
|
||||||
|
@ -18,7 +18,7 @@ from .settings import LNBITS_PATH
|
||||||
|
|
||||||
@click.command("migrate")
|
@click.command("migrate")
|
||||||
def db_migrate():
|
def db_migrate():
|
||||||
trio.run(migrate_databases)
|
asyncio.create_task(migrate_databases())
|
||||||
|
|
||||||
|
|
||||||
@click.command("assets")
|
@click.command("assets")
|
||||||
|
|
|
@ -6,14 +6,8 @@ db = Database("database")
|
||||||
|
|
||||||
core_app: APIRouter = APIRouter()
|
core_app: APIRouter = APIRouter()
|
||||||
|
|
||||||
from lnbits.tasks import record_async
|
|
||||||
|
|
||||||
from .tasks import register_listeners
|
|
||||||
from .views.api import * # noqa
|
from .views.api import * # noqa
|
||||||
from .views.generic import * # noqa
|
from .views.generic import * # noqa
|
||||||
from .views.public_api import * # noqa
|
from .views.public_api import * # noqa
|
||||||
|
|
||||||
|
|
||||||
@core_app.on_event("startup")
|
|
||||||
def do_startup():
|
|
||||||
record_async(register_listeners)
|
|
||||||
|
|
|
@ -54,20 +54,14 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
|
||||||
""",
|
""",
|
||||||
(user_id,),
|
(user_id,),
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
return (
|
return User(
|
||||||
User(
|
id = user['id'],
|
||||||
**{
|
email = user['email'],
|
||||||
**user,
|
extensions = [e[0] for e in extensions],
|
||||||
**{
|
wallets = [Wallet(**w) for w in wallets])
|
||||||
"extensions": [e[0] for e in extensions],
|
|
||||||
"wallets": [Wallet(**w) for w in wallets],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if user
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def update_user_extension(
|
async def update_user_extension(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import json
|
import json
|
||||||
import hmac
|
import hmac
|
||||||
import hashlib
|
import hashlib
|
||||||
from quart import url_for
|
from lnbits.helpers import url_for
|
||||||
from ecdsa import SECP256k1, SigningKey # type: ignore
|
from ecdsa import SECP256k1, SigningKey # type: ignore
|
||||||
from lnurl import encode as lnurl_encode # type: ignore
|
from lnurl import encode as lnurl_encode # type: ignore
|
||||||
from typing import List, NamedTuple, Optional, Dict
|
from typing import List, NamedTuple, Optional, Dict
|
||||||
|
@ -10,22 +10,6 @@ from pydantic import BaseModel
|
||||||
from lnbits.settings import WALLET
|
from lnbits.settings import WALLET
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
|
||||||
id: str
|
|
||||||
email: str
|
|
||||||
extensions: List[str] = []
|
|
||||||
wallets: List["Wallet"] = []
|
|
||||||
password: Optional[str] = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def wallet_ids(self) -> List[str]:
|
|
||||||
return [wallet.id for wallet in self.wallets]
|
|
||||||
|
|
||||||
def get_wallet(self, wallet_id: str) -> Optional["Wallet"]:
|
|
||||||
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
|
|
||||||
return w[0] if w else None
|
|
||||||
|
|
||||||
|
|
||||||
class Wallet(BaseModel):
|
class Wallet(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
|
@ -46,11 +30,12 @@ class Wallet(BaseModel):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def lnurlwithdraw_full(self) -> str:
|
def lnurlwithdraw_full(self) -> str:
|
||||||
|
|
||||||
url = url_for(
|
url = url_for(
|
||||||
"core.lnurl_full_withdraw",
|
"/withdraw",
|
||||||
|
external=True,
|
||||||
usr=self.user,
|
usr=self.user,
|
||||||
wal=self.id,
|
wal=self.id,
|
||||||
_external=True,
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
return lnurl_encode(url)
|
return lnurl_encode(url)
|
||||||
|
@ -73,6 +58,22 @@ class Wallet(BaseModel):
|
||||||
return await get_wallet_payment(self.id, payment_hash)
|
return await get_wallet_payment(self.id, payment_hash)
|
||||||
|
|
||||||
|
|
||||||
|
class User(BaseModel):
|
||||||
|
id: str
|
||||||
|
email: Optional[str] = None
|
||||||
|
extensions: List[str] = []
|
||||||
|
wallets: List[Wallet] = []
|
||||||
|
password: Optional[str] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wallet_ids(self) -> List[str]:
|
||||||
|
return [wallet.id for wallet in self.wallets]
|
||||||
|
|
||||||
|
def get_wallet(self, wallet_id: str) -> Optional["Wallet"]:
|
||||||
|
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
|
||||||
|
return w[0] if w else None
|
||||||
|
|
||||||
|
|
||||||
class Payment(BaseModel):
|
class Payment(BaseModel):
|
||||||
checking_id: str
|
checking_id: str
|
||||||
pending: bool
|
pending: bool
|
||||||
|
@ -83,10 +84,10 @@ class Payment(BaseModel):
|
||||||
bolt11: str
|
bolt11: str
|
||||||
preimage: str
|
preimage: str
|
||||||
payment_hash: str
|
payment_hash: str
|
||||||
extra: Dict
|
extra: Optional[Dict] = {}
|
||||||
wallet_id: str
|
wallet_id: str
|
||||||
webhook: str
|
webhook: Optional[str]
|
||||||
webhook_status: int
|
webhook_status: Optional[int]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Row):
|
def from_row(cls, row: Row):
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import trio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import httpx
|
import httpx
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
from typing import Optional, Tuple, Dict
|
from typing import Optional, Tuple, Dict
|
||||||
from urllib.parse import urlparse, parse_qs
|
from urllib.parse import urlparse, parse_qs
|
||||||
from quart import g, url_for
|
|
||||||
from lnurl import LnurlErrorResponse, decode as decode_lnurl # type: ignore
|
from lnurl import LnurlErrorResponse, decode as decode_lnurl # type: ignore
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -15,9 +14,10 @@ except ImportError: # pragma: nocover
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
from lnbits.db import Connection
|
from lnbits.db import Connection
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
from lnbits.helpers import url_for, urlsafe_short_hash
|
||||||
from lnbits.settings import WALLET
|
from lnbits.settings import WALLET
|
||||||
from lnbits.wallets.base import PaymentStatus, PaymentResponse
|
from lnbits.wallets.base import PaymentStatus, PaymentResponse
|
||||||
|
from lnbits.requestvars import g
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
from .crud import (
|
from .crud import (
|
||||||
|
@ -211,7 +211,7 @@ async def redeem_lnurl_withdraw(
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if wait_seconds:
|
if wait_seconds:
|
||||||
await trio.sleep(wait_seconds)
|
await asyncio.sleep(wait_seconds)
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"k1": res["k1"],
|
"k1": res["k1"],
|
||||||
|
@ -220,10 +220,9 @@ async def redeem_lnurl_withdraw(
|
||||||
|
|
||||||
try:
|
try:
|
||||||
params["balanceNotify"] = url_for(
|
params["balanceNotify"] = url_for(
|
||||||
"core.lnurl_balance_notify",
|
f"/withdraw/notify/{urlparse(lnurl_request).netloc}",
|
||||||
service=urlparse(lnurl_request).netloc,
|
external=True,
|
||||||
wal=wallet_id,
|
wal=wallet_id,
|
||||||
_external=True,
|
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
@ -242,7 +241,7 @@ async def perform_lnurlauth(
|
||||||
cb = urlparse(callback)
|
cb = urlparse(callback)
|
||||||
|
|
||||||
k1 = unhexlify(parse_qs(cb.query)["k1"][0])
|
k1 = unhexlify(parse_qs(cb.query)["k1"][0])
|
||||||
key = g.wallet.lnurlauth_key(cb.netloc)
|
key = g().wallet.lnurlauth_key(cb.netloc)
|
||||||
|
|
||||||
def int_to_bytes_suitable_der(x: int) -> bytes:
|
def int_to_bytes_suitable_der(x: int) -> bytes:
|
||||||
"""for strict DER we need to encode the integer with some quirks"""
|
"""for strict DER we need to encode the integer with some quirks"""
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import trio
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
@ -8,17 +8,19 @@ from . import db
|
||||||
from .crud import get_balance_notify
|
from .crud import get_balance_notify
|
||||||
from .models import Payment
|
from .models import Payment
|
||||||
|
|
||||||
api_invoice_listeners: List[trio.MemorySendChannel] = []
|
api_invoice_listeners: List[asyncio.Queue] = []
|
||||||
|
|
||||||
|
|
||||||
async def register_listeners():
|
async def register_task_listeners():
|
||||||
invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(5)
|
invoice_paid_queue = asyncio.Queue(5)
|
||||||
register_invoice_listener(invoice_paid_chan_send)
|
register_invoice_listener(invoice_paid_queue)
|
||||||
await wait_for_paid_invoices(invoice_paid_chan_recv)
|
asyncio.create_task(wait_for_paid_invoices(invoice_paid_queue))
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
|
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
||||||
async for payment in invoice_paid_chan:
|
while True:
|
||||||
|
payment = await invoice_paid_queue.get()
|
||||||
|
|
||||||
# send information to sse channel
|
# send information to sse channel
|
||||||
await dispatch_invoice_listener(payment)
|
await dispatch_invoice_listener(payment)
|
||||||
|
|
||||||
|
@ -43,8 +45,8 @@ async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
|
||||||
async def dispatch_invoice_listener(payment: Payment):
|
async def dispatch_invoice_listener(payment: Payment):
|
||||||
for send_channel in api_invoice_listeners:
|
for send_channel in api_invoice_listeners:
|
||||||
try:
|
try:
|
||||||
send_channel.send_nowait(payment)
|
send_channel.put_nowait(payment)
|
||||||
except trio.WouldBlock:
|
except asyncio.QueueFull:
|
||||||
print("removing sse listener", send_channel)
|
print("removing sse listener", send_channel)
|
||||||
api_invoice_listeners.remove(send_channel)
|
api_invoice_listeners.remove(send_channel)
|
||||||
|
|
||||||
|
|
|
@ -1,69 +1,60 @@
|
||||||
from fastapi.param_functions import Depends
|
import asyncio
|
||||||
from lnbits.auth_bearer import AuthBearer
|
|
||||||
from pydantic import BaseModel
|
|
||||||
import trio
|
|
||||||
import json
|
|
||||||
import httpx
|
|
||||||
import hashlib
|
import hashlib
|
||||||
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult
|
import json
|
||||||
from quart import current_app, make_response, url_for
|
|
||||||
|
|
||||||
from fastapi import Query
|
|
||||||
|
|
||||||
from http import HTTPStatus
|
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
from typing import Dict, List, Optional, Union
|
from http import HTTPStatus
|
||||||
|
from typing import Dict, Optional, Union
|
||||||
|
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from fastapi import Query, Request
|
||||||
|
from fastapi.exceptions import HTTPException
|
||||||
|
from fastapi.param_functions import Depends
|
||||||
|
from fastapi.params import Body
|
||||||
|
from sse_starlette.sse import EventSourceResponse
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from lnbits import bolt11, lnurl
|
from lnbits import bolt11, lnurl
|
||||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
from lnbits.core.models import Payment, Wallet
|
||||||
from lnbits.utils.exchange_rates import currencies, fiat_amount_as_satoshis
|
from lnbits.decorators import (WalletAdminKeyChecker, WalletInvoiceKeyChecker,
|
||||||
|
WalletTypeInfo, get_key_type)
|
||||||
|
from lnbits.helpers import url_for
|
||||||
from lnbits.requestvars import g
|
from lnbits.requestvars import g
|
||||||
|
from lnbits.utils.exchange_rates import currencies, fiat_amount_as_satoshis
|
||||||
|
|
||||||
from .. import core_app, db
|
from .. import core_app, db
|
||||||
from ..crud import get_payments, save_balance_check, update_wallet
|
from ..crud import get_payments, save_balance_check, update_wallet
|
||||||
from ..services import (
|
from ..services import (InvoiceFailure, PaymentFailure, create_invoice,
|
||||||
PaymentFailure,
|
pay_invoice, perform_lnurlauth)
|
||||||
InvoiceFailure,
|
|
||||||
create_invoice,
|
|
||||||
pay_invoice,
|
|
||||||
perform_lnurlauth,
|
|
||||||
)
|
|
||||||
from ..tasks import api_invoice_listeners
|
from ..tasks import api_invoice_listeners
|
||||||
|
|
||||||
|
|
||||||
@core_app.get(
|
@core_app.get("/api/v1/wallet")
|
||||||
"/api/v1/wallet",
|
async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
# dependencies=[Depends(AuthBearer())]
|
|
||||||
)
|
|
||||||
# @api_check_wallet_key("invoice")
|
|
||||||
async def api_wallet():
|
|
||||||
return (
|
return (
|
||||||
{"id": g().wallet.id, "name": g().wallet.name, "balance": g().wallet.balance_msat},
|
{"id": wallet.wallet.id, "name": wallet.wallet.name, "balance": wallet.wallet.balance_msat},
|
||||||
HTTPStatus.OK,
|
HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@core_app.put("/api/v1/wallet/{new_name}")
|
@core_app.put("/api/v1/wallet/{new_name}")
|
||||||
@api_check_wallet_key("invoice")
|
async def api_update_wallet(new_name: str, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
async def api_update_wallet(new_name: str):
|
await update_wallet(wallet.wallet.id, new_name)
|
||||||
await update_wallet(g().wallet.id, new_name)
|
|
||||||
return (
|
return (
|
||||||
{
|
{
|
||||||
"id": g().wallet.id,
|
"id": wallet.wallet.id,
|
||||||
"name": g().wallet.name,
|
"name": wallet.wallet.name,
|
||||||
"balance": g().wallet.balance_msat,
|
"balance": wallet.wallet.balance_msat,
|
||||||
},
|
},
|
||||||
HTTPStatus.OK,
|
HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/api/v1/payments")
|
@core_app.get("/api/v1/payments")
|
||||||
@api_check_wallet_key("invoice")
|
async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
async def api_payments():
|
return await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True)
|
||||||
return (
|
|
||||||
await get_payments(wallet_id=g().wallet.id, pending=True, complete=True),
|
|
||||||
HTTPStatus.OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
class CreateInvoiceData(BaseModel):
|
class CreateInvoiceData(BaseModel):
|
||||||
amount: int = Query(None, ge=1)
|
amount: int = Query(None, ge=1)
|
||||||
|
@ -75,9 +66,7 @@ class CreateInvoiceData(BaseModel):
|
||||||
extra: Optional[dict] = None
|
extra: Optional[dict] = None
|
||||||
webhook: Optional[str] = None
|
webhook: Optional[str] = None
|
||||||
|
|
||||||
@api_check_wallet_key("invoice")
|
async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||||
# async def api_payments_create_invoice(amount: List[str] = Query([type: str = Query(None)])):
|
|
||||||
async def api_payments_create_invoice(data: CreateInvoiceData):
|
|
||||||
if "description_hash" in data:
|
if "description_hash" in data:
|
||||||
description_hash = unhexlify(data.description_hash)
|
description_hash = unhexlify(data.description_hash)
|
||||||
memo = ""
|
memo = ""
|
||||||
|
@ -94,7 +83,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData):
|
||||||
async with db.connect() as conn:
|
async with db.connect() as conn:
|
||||||
try:
|
try:
|
||||||
payment_hash, payment_request = await create_invoice(
|
payment_hash, payment_request = await create_invoice(
|
||||||
wallet_id=g().wallet.id,
|
wallet_id=wallet.id,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
memo=memo,
|
memo=memo,
|
||||||
description_hash=description_hash,
|
description_hash=description_hash,
|
||||||
|
@ -121,10 +110,9 @@ async def api_payments_create_invoice(data: CreateInvoiceData):
|
||||||
params={
|
params={
|
||||||
"pr": payment_request,
|
"pr": payment_request,
|
||||||
"balanceNotify": url_for(
|
"balanceNotify": url_for(
|
||||||
"core.lnurl_balance_notify",
|
f"/withdraw/notify/{urlparse(data.lnurl_callback).netloc}",
|
||||||
service=urlparse(data.lnurl_callback).netloc,
|
external=True,
|
||||||
wal=g().wallet.id,
|
wal=g().wallet.id,
|
||||||
_external=True,
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
timeout=10,
|
timeout=10,
|
||||||
|
@ -152,10 +140,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@api_check_wallet_key("admin")
|
async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
|
||||||
async def api_payments_pay_invoice(
|
|
||||||
bolt11: str = Query(...), wallet: Optional[List[str]] = Query(None)
|
|
||||||
):
|
|
||||||
try:
|
try:
|
||||||
payment_hash = await pay_invoice(
|
payment_hash = await pay_invoice(
|
||||||
wallet_id=wallet.id,
|
wallet_id=wallet.id,
|
||||||
|
@ -180,11 +165,20 @@ async def api_payments_pay_invoice(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@core_app.post("/api/v1/payments")
|
@core_app.post("/api/v1/payments", deprecated=True,
|
||||||
async def api_payments_create(out: bool = True):
|
description="DEPRECATED. Use /api/v2/TBD and /api/v2/TBD instead")
|
||||||
if out is True:
|
async def api_payments_create(wallet: WalletTypeInfo = Depends(get_key_type), out: bool = True,
|
||||||
return await api_payments_pay_invoice()
|
invoiceData: Optional[CreateInvoiceData] = Body(None),
|
||||||
return await api_payments_create_invoice()
|
bolt11: Optional[str] = Query(None)):
|
||||||
|
|
||||||
|
if wallet.wallet_type < 0 or wallet.wallet_type > 2:
|
||||||
|
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
|
||||||
|
|
||||||
|
if out is True and wallet.wallet_type == 0:
|
||||||
|
if not bolt11:
|
||||||
|
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="BOLT11 string is invalid or not given")
|
||||||
|
return await api_payments_pay_invoice(bolt11, wallet.wallet) # admin key
|
||||||
|
return await api_payments_create_invoice(invoiceData, wallet.wallet) # invoice key
|
||||||
|
|
||||||
class CreateLNURLData(BaseModel):
|
class CreateLNURLData(BaseModel):
|
||||||
description_hash: str
|
description_hash: str
|
||||||
|
@ -193,8 +187,7 @@ class CreateLNURLData(BaseModel):
|
||||||
comment: Optional[str] = None
|
comment: Optional[str] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
|
|
||||||
@core_app.post("/api/v1/payments/lnurl")
|
@core_app.post("/api/v1/payments/lnurl", dependencies=[Depends(WalletAdminKeyChecker())])
|
||||||
@api_check_wallet_key("admin")
|
|
||||||
async def api_payments_pay_lnurl(data: CreateLNURLData):
|
async def api_payments_pay_lnurl(data: CreateLNURLData):
|
||||||
domain = urlparse(data.callback).netloc
|
domain = urlparse(data.callback).netloc
|
||||||
|
|
||||||
|
@ -259,11 +252,46 @@ async def api_payments_pay_lnurl(data: CreateLNURLData):
|
||||||
HTTPStatus.CREATED,
|
HTTPStatus.CREATED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def subscribe(request: Request, wallet: Wallet):
|
||||||
|
this_wallet_id = wallet.wallet.id
|
||||||
|
|
||||||
|
payment_queue = asyncio.Queue(0)
|
||||||
|
|
||||||
|
print("adding sse listener", payment_queue)
|
||||||
|
api_invoice_listeners.append(payment_queue)
|
||||||
|
|
||||||
|
send_queue = asyncio.Queue(0)
|
||||||
|
|
||||||
|
async def payment_received() -> None:
|
||||||
|
while True:
|
||||||
|
payment: Payment = await payment_queue.get()
|
||||||
|
if payment.wallet_id == this_wallet_id:
|
||||||
|
await send_queue.put(("payment-received", payment))
|
||||||
|
|
||||||
|
asyncio.create_task(payment_received())
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
typ, data = await send_queue.get()
|
||||||
|
message = [f"event: {typ}".encode("utf-8")]
|
||||||
|
|
||||||
|
if data:
|
||||||
|
jdata = json.dumps(dict(data.dict(), pending=False))
|
||||||
|
message.append(f"data: {jdata}".encode("utf-8"))
|
||||||
|
|
||||||
|
yield dict(data=jdata.encode("utf-8"), event=typ.encode("utf-8"))
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@core_app.get("/api/v1/payments/sse")
|
||||||
|
async def api_payments_sse(request: Request, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
|
return EventSourceResponse(subscribe(request, wallet))
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/api/v1/payments/{payment_hash}")
|
@core_app.get("/api/v1/payments/{payment_hash}")
|
||||||
@api_check_wallet_key("invoice")
|
async def api_payment(payment_hash, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
async def api_payment(payment_hash):
|
payment = await wallet.wallet.get_payment(payment_hash)
|
||||||
payment = await g().wallet.get_payment(payment_hash)
|
|
||||||
|
|
||||||
if not payment:
|
if not payment:
|
||||||
return {"message": "Payment does not exist."}, HTTPStatus.NOT_FOUND
|
return {"message": "Payment does not exist."}, HTTPStatus.NOT_FOUND
|
||||||
|
@ -280,62 +308,7 @@ async def api_payment(payment_hash):
|
||||||
HTTPStatus.OK,
|
HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@core_app.get("/api/v1/lnurlscan/{code}", dependencies=[Depends(WalletInvoiceKeyChecker())])
|
||||||
@core_app.get("/api/v1/payments/sse")
|
|
||||||
@api_check_wallet_key("invoice", accept_querystring=True)
|
|
||||||
async def api_payments_sse():
|
|
||||||
this_wallet_id = g().wallet.id
|
|
||||||
|
|
||||||
send_payment, receive_payment = trio.open_memory_channel(0)
|
|
||||||
|
|
||||||
print("adding sse listener", send_payment)
|
|
||||||
api_invoice_listeners.append(send_payment)
|
|
||||||
|
|
||||||
send_event, event_to_send = trio.open_memory_channel(0)
|
|
||||||
|
|
||||||
async def payment_received() -> None:
|
|
||||||
async for payment in receive_payment:
|
|
||||||
if payment.wallet_id == this_wallet_id:
|
|
||||||
await send_event.send(("payment-received", payment))
|
|
||||||
|
|
||||||
async def repeat_keepalive():
|
|
||||||
await trio.sleep(1)
|
|
||||||
while True:
|
|
||||||
await send_event.send(("keepalive", ""))
|
|
||||||
await trio.sleep(25)
|
|
||||||
|
|
||||||
current_app.nursery.start_soon(payment_received)
|
|
||||||
current_app.nursery.start_soon(repeat_keepalive)
|
|
||||||
|
|
||||||
async def send_events():
|
|
||||||
try:
|
|
||||||
async for typ, data in event_to_send:
|
|
||||||
message = [f"event: {typ}".encode("utf-8")]
|
|
||||||
|
|
||||||
if data:
|
|
||||||
jdata = json.dumps(dict(data._asdict(), pending=False))
|
|
||||||
message.append(f"data: {jdata}".encode("utf-8"))
|
|
||||||
|
|
||||||
yield b"\n".join(message) + b"\r\n\r\n"
|
|
||||||
except trio.Cancelled:
|
|
||||||
return
|
|
||||||
|
|
||||||
response = await make_response(
|
|
||||||
send_events(),
|
|
||||||
{
|
|
||||||
"Content-Type": "text/event-stream",
|
|
||||||
"Cache-Control": "no-cache",
|
|
||||||
"X-Accel-Buffering": "no",
|
|
||||||
"Connection": "keep-alive",
|
|
||||||
"Transfer-Encoding": "chunked",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
response.timeout = None
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/api/v1/lnurlscan/{code}")
|
|
||||||
@api_check_wallet_key("invoice")
|
|
||||||
async def api_lnurlscan(code: str):
|
async def api_lnurlscan(code: str):
|
||||||
try:
|
try:
|
||||||
url = lnurl.decode(code)
|
url = lnurl.decode(code)
|
||||||
|
@ -444,8 +417,7 @@ async def api_lnurlscan(code: str):
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
@core_app.post("/api/v1/lnurlauth")
|
@core_app.post("/api/v1/lnurlauth", dependencies=[Depends(WalletAdminKeyChecker())])
|
||||||
@api_check_wallet_key("admin")
|
|
||||||
async def api_perform_lnurlauth(callback: str):
|
async def api_perform_lnurlauth(callback: str):
|
||||||
err = await perform_lnurlauth(callback)
|
err = await perform_lnurlauth(callback)
|
||||||
if err:
|
if err:
|
||||||
|
@ -453,6 +425,6 @@ async def api_perform_lnurlauth(callback: str):
|
||||||
return "", HTTPStatus.OK
|
return "", HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
@core_app.route("/api/v1/currencies", methods=["GET"])
|
@core_app.get("/api/v1/currencies")
|
||||||
async def api_list_currencies_available():
|
async def api_list_currencies_available():
|
||||||
return list(currencies.keys())
|
return list(currencies.keys())
|
||||||
|
|
|
@ -1,51 +1,49 @@
|
||||||
from lnbits.requestvars import g
|
import asyncio
|
||||||
from os import path
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import jinja2
|
|
||||||
|
|
||||||
|
from fastapi import Request, status
|
||||||
|
from fastapi.exceptions import HTTPException
|
||||||
|
from fastapi.param_functions import Body
|
||||||
|
from fastapi.params import Depends, Query
|
||||||
|
from fastapi.responses import FileResponse, RedirectResponse
|
||||||
|
from fastapi.routing import APIRouter
|
||||||
|
from pydantic.types import UUID4
|
||||||
from starlette.responses import HTMLResponse
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
from lnbits.core import core_app, db
|
from lnbits.core import db
|
||||||
from lnbits.decorators import check_user_exists, validate_uuids
|
from lnbits.helpers import template_renderer, url_for
|
||||||
from lnbits.settings import LNBITS_ALLOWED_USERS, SERVICE_FEE, LNBITS_SITE_TITLE
|
from lnbits.requestvars import g
|
||||||
|
from lnbits.settings import (LNBITS_ALLOWED_USERS, LNBITS_SITE_TITLE,
|
||||||
|
SERVICE_FEE)
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (create_account, create_wallet, delete_wallet,
|
||||||
create_account,
|
get_balance_check, get_user, save_balance_notify,
|
||||||
get_user,
|
update_user_extension)
|
||||||
update_user_extension,
|
from ..services import pay_invoice, redeem_lnurl_withdraw
|
||||||
create_wallet,
|
|
||||||
delete_wallet,
|
|
||||||
get_balance_check,
|
|
||||||
save_balance_notify,
|
|
||||||
)
|
|
||||||
from ..services import redeem_lnurl_withdraw, pay_invoice
|
|
||||||
from fastapi import FastAPI, Request
|
|
||||||
from fastapi.responses import FileResponse
|
|
||||||
from lnbits.jinja2_templating import Jinja2Templates
|
|
||||||
|
|
||||||
|
core_html_routes: APIRouter = APIRouter(tags=["Core NON-API Website Routes"])
|
||||||
|
|
||||||
@core_app.get("/favicon.ico")
|
@core_html_routes.get("/favicon.ico")
|
||||||
async def favicon():
|
async def favicon():
|
||||||
return FileResponse("lnbits/core/static/favicon.ico")
|
return FileResponse("lnbits/core/static/favicon.ico")
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/", response_class=HTMLResponse)
|
@core_html_routes.get("/", response_class=HTMLResponse)
|
||||||
async def home(request: Request, lightning: str = None):
|
async def home(request: Request, lightning: str = None):
|
||||||
return g().templates.TemplateResponse("core/index.html", {"request": request, "lnurl": lightning})
|
return template_renderer().TemplateResponse("core/index.html", {"request": request, "lnurl": lightning})
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/extensions")
|
@core_html_routes.get("/extensions")
|
||||||
@validate_uuids(["usr"], required=True)
|
# @validate_uuids(["usr"], required=True)
|
||||||
@check_user_exists()
|
# @check_user_exists()
|
||||||
async def extensions(enable: str, disable: str):
|
async def extensions(request: Request, enable: str, disable: str):
|
||||||
extension_to_enable = enable
|
extension_to_enable = enable
|
||||||
extension_to_disable = disable
|
extension_to_disable = disable
|
||||||
|
|
||||||
if extension_to_enable and extension_to_disable:
|
if extension_to_enable and extension_to_disable:
|
||||||
abort(
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension.")
|
||||||
HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension."
|
|
||||||
)
|
|
||||||
|
|
||||||
if extension_to_enable:
|
if extension_to_enable:
|
||||||
await update_user_extension(
|
await update_user_extension(
|
||||||
|
@ -55,15 +53,16 @@ async def extensions(enable: str, disable: str):
|
||||||
await update_user_extension(
|
await update_user_extension(
|
||||||
user_id=g.user.id, extension=extension_to_disable, active=False
|
user_id=g.user.id, extension=extension_to_disable, active=False
|
||||||
)
|
)
|
||||||
return await templates.TemplateResponse("core/extensions.html", {"request": request, "user": get_user(g.user.id)})
|
return template_renderer().TemplateResponse("core/extensions.html", {"request": request, "user": get_user(g.user.id)})
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/wallet{usr}{wal}{nme}")
|
@core_html_routes.get("/wallet", response_class=HTMLResponse)
|
||||||
#Not sure how to validate
|
#Not sure how to validate
|
||||||
@validate_uuids(["usr", "wal"])
|
# @validate_uuids(["usr", "nme"])
|
||||||
async def wallet(request: Request, usr: Optional[str], wal: Optional[str], nme: Optional[str]):
|
async def wallet(request: Request = Query(None), nme: Optional[str] = Query(None),
|
||||||
user_id = usr
|
usr: Optional[UUID4] = Query(None), wal: Optional[UUID4] = Query(None)):
|
||||||
wallet_id = wal
|
user_id = usr.hex if usr else None
|
||||||
|
wallet_id = wal.hex if wal else None
|
||||||
wallet_name = nme
|
wallet_name = nme
|
||||||
service_fee = int(SERVICE_FEE) if int(SERVICE_FEE) == SERVICE_FEE else SERVICE_FEE
|
service_fee = int(SERVICE_FEE) if int(SERVICE_FEE) == SERVICE_FEE else SERVICE_FEE
|
||||||
|
|
||||||
|
@ -78,32 +77,29 @@ async def wallet(request: Request, usr: Optional[str], wal: Optional[str], nme:
|
||||||
else:
|
else:
|
||||||
user = await get_user(user_id)
|
user = await get_user(user_id)
|
||||||
if not user:
|
if not user:
|
||||||
abort(HTTPStatus.NOT_FOUND, "User does not exist.")
|
return template_renderer().TemplateResponse("error.html", {"request": request, "err": "User does not exist."})
|
||||||
return
|
|
||||||
|
|
||||||
if LNBITS_ALLOWED_USERS and user_id not in LNBITS_ALLOWED_USERS:
|
if LNBITS_ALLOWED_USERS and user_id not in LNBITS_ALLOWED_USERS:
|
||||||
abort(HTTPStatus.UNAUTHORIZED, "User not authorized.")
|
return template_renderer().TemplateResponse("error.html", {"request": request, "err": "User not authorized."})
|
||||||
|
|
||||||
if not wallet_id:
|
if not wallet_id:
|
||||||
if user.wallets and not wallet_name:
|
if user.wallets and not wallet_name:
|
||||||
wallet = user.wallets[0]
|
wallet = user.wallets[0]
|
||||||
else:
|
else:
|
||||||
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name)
|
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name)
|
||||||
|
|
||||||
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
|
return RedirectResponse(f"/wallet?usr={user.id}&wal={wallet.id}", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
|
||||||
|
|
||||||
wallet = user.get_wallet(wallet_id)
|
wallet = user.get_wallet(wallet_id)
|
||||||
if not wallet:
|
if not wallet:
|
||||||
abort(HTTPStatus.FORBIDDEN, "Not your wallet.")
|
return template_renderer().TemplateResponse("error.html", {"request": request, "err": "Wallet not found"})
|
||||||
|
|
||||||
return await templates.TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
"core/wallet.html", {"request":request,"user":user, "wallet":wallet, "service_fee":service_fee}
|
"core/wallet.html", {"request":request,"user":user.dict(), "wallet":wallet.dict(), "service_fee":service_fee}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/withdraw")
|
@core_html_routes.get("/withdraw")
|
||||||
@validate_uuids(["usr", "wal"], required=True)
|
# @validate_uuids(["usr", "wal"], required=True)
|
||||||
async def lnurl_full_withdraw():
|
async def lnurl_full_withdraw(request: Request):
|
||||||
user = await get_user(request.args.get("usr"))
|
user = await get_user(request.args.get("usr"))
|
||||||
if not user:
|
if not user:
|
||||||
return {"status": "ERROR", "reason": "User does not exist."}
|
return {"status": "ERROR", "reason": "User does not exist."}
|
||||||
|
@ -115,24 +111,22 @@ async def lnurl_full_withdraw():
|
||||||
return {
|
return {
|
||||||
"tag": "withdrawRequest",
|
"tag": "withdrawRequest",
|
||||||
"callback": url_for(
|
"callback": url_for(
|
||||||
"core.lnurl_full_withdraw_callback",
|
"/withdraw/cb",
|
||||||
|
external=True,
|
||||||
usr=user.id,
|
usr=user.id,
|
||||||
wal=wallet.id,
|
wal=wallet.id,
|
||||||
_external=True,
|
|
||||||
),
|
),
|
||||||
"k1": "0",
|
"k1": "0",
|
||||||
"minWithdrawable": 1000 if wallet.withdrawable_balance else 0,
|
"minWithdrawable": 1000 if wallet.withdrawable_balance else 0,
|
||||||
"maxWithdrawable": wallet.withdrawable_balance,
|
"maxWithdrawable": wallet.withdrawable_balance,
|
||||||
"defaultDescription": f"{LNBITS_SITE_TITLE} balance withdraw from {wallet.id[0:5]}",
|
"defaultDescription": f"{LNBITS_SITE_TITLE} balance withdraw from {wallet.id[0:5]}",
|
||||||
"balanceCheck": url_for(
|
"balanceCheck": url_for("/withdraw", external=True, usr=user.id, wal=wallet.id),
|
||||||
"core.lnurl_full_withdraw", usr=user.id, wal=wallet.id, _external=True
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/withdraw/cb")
|
@core_html_routes.get("/withdraw/cb")
|
||||||
@validate_uuids(["usr", "wal"], required=True)
|
# @validate_uuids(["usr", "wal"], required=True)
|
||||||
async def lnurl_full_withdraw_callback():
|
async def lnurl_full_withdraw_callback(request: Request):
|
||||||
user = await get_user(request.args.get("usr"))
|
user = await get_user(request.args.get("usr"))
|
||||||
if not user:
|
if not user:
|
||||||
return {"status": "ERROR", "reason": "User does not exist."}
|
return {"status": "ERROR", "reason": "User does not exist."}
|
||||||
|
@ -149,7 +143,7 @@ async def lnurl_full_withdraw_callback():
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
current_app.nursery.start_soon(pay)
|
asyncio.create_task(pay())
|
||||||
|
|
||||||
balance_notify = request.args.get("balanceNotify")
|
balance_notify = request.args.get("balanceNotify")
|
||||||
if balance_notify:
|
if balance_notify:
|
||||||
|
@ -158,53 +152,55 @@ async def lnurl_full_withdraw_callback():
|
||||||
return {"status": "OK"}
|
return {"status": "OK"}
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/deletewallet")
|
@core_html_routes.get("/deletewallet")
|
||||||
@validate_uuids(["usr", "wal"], required=True)
|
# @validate_uuids(["usr", "wal"], required=True)
|
||||||
@check_user_exists()
|
# @check_user_exists()
|
||||||
async def deletewallet():
|
async def deletewallet(request: Request):
|
||||||
wallet_id = request.args.get("wal", type=str)
|
wallet_id = request.path_params.get("wal", type=str)
|
||||||
user_wallet_ids = g.user.wallet_ids
|
user_wallet_ids = g().user.wallet_ids
|
||||||
|
|
||||||
if wallet_id not in user_wallet_ids:
|
if wallet_id not in user_wallet_ids:
|
||||||
abort(HTTPStatus.FORBIDDEN, "Not your wallet.")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "Not your wallet.")
|
||||||
else:
|
else:
|
||||||
await delete_wallet(user_id=g.user.id, wallet_id=wallet_id)
|
await delete_wallet(user_id=g().user.id, wallet_id=wallet_id)
|
||||||
user_wallet_ids.remove(wallet_id)
|
user_wallet_ids.remove(wallet_id)
|
||||||
|
|
||||||
if user_wallet_ids:
|
if user_wallet_ids:
|
||||||
return redirect(url_for("core.wallet", usr=g.user.id, wal=user_wallet_ids[0]))
|
return RedirectResponse(url_for("/wallet", usr=g().user.id, wal=user_wallet_ids[0]),
|
||||||
|
status_code=status.HTTP_307_TEMPORARY_REDIRECT)
|
||||||
|
|
||||||
return redirect(url_for("core.home"))
|
return RedirectResponse(url_for("/"), status_code=status.HTTP_307_TEMPORARY_REDIRECT)
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/withdraw/notify/{service}")
|
@core_html_routes.get("/withdraw/notify/{service}")
|
||||||
@validate_uuids(["wal"], required=True)
|
# @validate_uuids(["wal"], required=True)
|
||||||
async def lnurl_balance_notify(service: str):
|
async def lnurl_balance_notify(request: Request, service: str):
|
||||||
bc = await get_balance_check(request.args.get("wal"), service)
|
bc = await get_balance_check(request.args.get("wal"), service)
|
||||||
if bc:
|
if bc:
|
||||||
redeem_lnurl_withdraw(bc.wallet, bc.url)
|
redeem_lnurl_withdraw(bc.wallet, bc.url)
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/lnurlwallet")
|
@core_html_routes.get("/lnurlwallet")
|
||||||
async def lnurlwallet():
|
async def lnurlwallet(request: Request):
|
||||||
async with db.connect() as conn:
|
async with db.connect() as conn:
|
||||||
account = await create_account(conn=conn)
|
account = await create_account(conn=conn)
|
||||||
user = await get_user(account.id, conn=conn)
|
user = await get_user(account.id, conn=conn)
|
||||||
wallet = await create_wallet(user_id=user.id, conn=conn)
|
wallet = await create_wallet(user_id=user.id, conn=conn)
|
||||||
|
|
||||||
current_app.nursery.start_soon(
|
asyncio.create_task(
|
||||||
redeem_lnurl_withdraw,
|
redeem_lnurl_withdraw(
|
||||||
wallet.id,
|
wallet.id,
|
||||||
request.args.get("lightning"),
|
request.args.get("lightning"),
|
||||||
"LNbits initial funding: voucher redeem.",
|
"LNbits initial funding: voucher redeem.",
|
||||||
{"tag": "lnurlwallet"},
|
{"tag": "lnurlwallet"},
|
||||||
5, # wait 5 seconds before sending the invoice to the service
|
5 # wait 5 seconds before sending the invoice to the service
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
|
return RedirectResponse(f"/wallet?usr={user.id}&wal={wallet.id}", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/manifest/{usr}.webmanifest")
|
@core_html_routes.get("/manifest/{usr}.webmanifest")
|
||||||
async def manifest(usr: str):
|
async def manifest(usr: str):
|
||||||
user = await get_user(usr)
|
user = await get_user(usr)
|
||||||
if not user:
|
if not user:
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import trio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from quart import jsonify
|
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
|
|
||||||
|
@ -27,27 +26,27 @@ async def api_public_payment_longpolling(payment_hash):
|
||||||
except:
|
except:
|
||||||
return {"message": "Invalid bolt11 invoice."}, HTTPStatus.BAD_REQUEST
|
return {"message": "Invalid bolt11 invoice."}, HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
send_payment, receive_payment = trio.open_memory_channel(0)
|
payment_queue = asyncio.Queue(0)
|
||||||
|
|
||||||
print("adding standalone invoice listener", payment_hash, send_payment)
|
print("adding standalone invoice listener", payment_hash, payment_queue)
|
||||||
api_invoice_listeners.append(send_payment)
|
api_invoice_listeners.append(payment_queue)
|
||||||
|
|
||||||
response = None
|
response = None
|
||||||
|
|
||||||
async def payment_info_receiver(cancel_scope):
|
async def payment_info_receiver(cancel_scope):
|
||||||
async for payment in receive_payment:
|
async for payment in payment_queue.get():
|
||||||
if payment.payment_hash == payment_hash:
|
if payment.payment_hash == payment_hash:
|
||||||
nonlocal response
|
nonlocal response
|
||||||
response = ({"status": "paid"}, HTTPStatus.OK)
|
response = ({"status": "paid"}, HTTPStatus.OK)
|
||||||
cancel_scope.cancel()
|
cancel_scope.cancel()
|
||||||
|
|
||||||
async def timeouter(cancel_scope):
|
async def timeouter(cancel_scope):
|
||||||
await trio.sleep(45)
|
await asyncio.sleep(45)
|
||||||
cancel_scope.cancel()
|
cancel_scope.cancel()
|
||||||
|
|
||||||
async with trio.open_nursery() as nursery:
|
|
||||||
nursery.start_soon(payment_info_receiver, nursery.cancel_scope)
|
asyncio.create_task(payment_info_receiver())
|
||||||
nursery.start_soon(timeouter, nursery.cancel_scope)
|
asyncio.create_task(timeouter())
|
||||||
|
|
||||||
if response:
|
if response:
|
||||||
return response
|
return response
|
||||||
|
|
12
lnbits/db.py
12
lnbits/db.py
|
@ -1,12 +1,12 @@
|
||||||
import os
|
import os
|
||||||
import trio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
import datetime
|
import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from sqlalchemy import create_engine # type: ignore
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy_aio import TRIO_STRATEGY # type: ignore
|
from sqlalchemy_aio.base import AsyncConnection
|
||||||
from sqlalchemy_aio.base import AsyncConnection # type: ignore
|
from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY # type: ignore
|
||||||
|
|
||||||
from .settings import LNBITS_DATA_FOLDER, LNBITS_DATABASE_URL
|
from .settings import LNBITS_DATA_FOLDER, LNBITS_DATABASE_URL
|
||||||
|
|
||||||
|
@ -132,8 +132,8 @@ class Database(Compat):
|
||||||
else:
|
else:
|
||||||
self.schema = None
|
self.schema = None
|
||||||
|
|
||||||
self.engine = create_engine(database_uri, strategy=TRIO_STRATEGY)
|
self.engine = create_engine(database_uri, strategy=ASYNCIO_STRATEGY)
|
||||||
self.lock = trio.StrictFIFOLock()
|
self.lock = asyncio.Lock()
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
|
|
|
@ -1,36 +1,114 @@
|
||||||
from cerberus import Validator # type: ignore
|
|
||||||
from quart import g, abort, jsonify, request
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from fastapi.security import api_key
|
||||||
|
from lnbits.core.models import Wallet
|
||||||
from typing import List, Union
|
from typing import List, Union
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from cerberus import Validator # type: ignore
|
||||||
|
from fastapi.exceptions import HTTPException
|
||||||
|
from fastapi.openapi.models import APIKey, APIKeyIn
|
||||||
|
from fastapi.params import Security
|
||||||
|
from fastapi.security.api_key import APIKeyHeader, APIKeyQuery
|
||||||
|
from fastapi.security.base import SecurityBase
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
from lnbits.core.crud import get_user, get_wallet_for_key
|
from lnbits.core.crud import get_user, get_wallet_for_key
|
||||||
from lnbits.settings import LNBITS_ALLOWED_USERS
|
|
||||||
from lnbits.requestvars import g
|
from lnbits.requestvars import g
|
||||||
|
from lnbits.settings import LNBITS_ALLOWED_USERS
|
||||||
|
|
||||||
def api_check_wallet_key(key_type: str = "invoice", accept_querystring=False):
|
|
||||||
def wrap(view):
|
|
||||||
@wraps(view)
|
|
||||||
async def wrapped_view(**kwargs):
|
|
||||||
try:
|
|
||||||
key_value = request.headers.get("X-Api-Key") or request.args["api-key"]
|
|
||||||
g().wallet = await get_wallet_for_key(key_value, key_type)
|
|
||||||
except KeyError:
|
|
||||||
return (
|
|
||||||
jsonify({"message": "`X-Api-Key` header missing."}),
|
|
||||||
HTTPStatus.BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not g().wallet:
|
class KeyChecker(SecurityBase):
|
||||||
return jsonify({"message": "Wrong keys."}), HTTPStatus.UNAUTHORIZED
|
def __init__(self, scheme_name: str = None, auto_error: bool = True, api_key: str = None):
|
||||||
|
self.scheme_name = scheme_name or self.__class__.__name__
|
||||||
|
self.auto_error = auto_error
|
||||||
|
self._key_type = "invoice"
|
||||||
|
self._api_key = api_key
|
||||||
|
if api_key:
|
||||||
|
self.model: APIKey= APIKey(
|
||||||
|
**{"in": APIKeyIn.query}, name="X-API-KEY", description="Wallet API Key - QUERY"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.model: APIKey= APIKey(
|
||||||
|
**{"in": APIKeyIn.header}, name="X-API-KEY", description="Wallet API Key - HEADER"
|
||||||
|
)
|
||||||
|
self.wallet = None
|
||||||
|
|
||||||
return await view(**kwargs)
|
async def __call__(self, request: Request) -> Wallet:
|
||||||
|
try:
|
||||||
|
key_value = self._api_key if self._api_key else request.headers.get("X-API-KEY") or request.query_params["api-key"]
|
||||||
|
# FIXME: Find another way to validate the key. A fetch from DB should be avoided here.
|
||||||
|
# Also, we should not return the wallet here - thats silly.
|
||||||
|
# Possibly store it in a Redis DB
|
||||||
|
self.wallet = await get_wallet_for_key(key_value, self._key_type)
|
||||||
|
if not self.wallet:
|
||||||
|
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Invalid key or expired key.")
|
||||||
|
|
||||||
return wrapped_view
|
except KeyError:
|
||||||
|
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="`X-API-KEY` header missing.")
|
||||||
|
|
||||||
return wrap
|
class WalletInvoiceKeyChecker(KeyChecker):
|
||||||
|
"""
|
||||||
|
WalletInvoiceKeyChecker will ensure that the provided invoice
|
||||||
|
wallet key is correct and populate g().wallet with the wallet
|
||||||
|
for the key in `X-API-key`.
|
||||||
|
|
||||||
|
The checker will raise an HTTPException when the key is wrong in some ways.
|
||||||
|
"""
|
||||||
|
def __init__(self, scheme_name: str = None, auto_error: bool = True, api_key: str = None):
|
||||||
|
super().__init__(scheme_name, auto_error, api_key)
|
||||||
|
self._key_type = "invoice"
|
||||||
|
|
||||||
|
class WalletAdminKeyChecker(KeyChecker):
|
||||||
|
"""
|
||||||
|
WalletAdminKeyChecker will ensure that the provided admin
|
||||||
|
wallet key is correct and populate g().wallet with the wallet
|
||||||
|
for the key in `X-API-key`.
|
||||||
|
|
||||||
|
The checker will raise an HTTPException when the key is wrong in some ways.
|
||||||
|
"""
|
||||||
|
def __init__(self, scheme_name: str = None, auto_error: bool = True, api_key: str = None):
|
||||||
|
super().__init__(scheme_name, auto_error, api_key)
|
||||||
|
self._key_type = "admin"
|
||||||
|
|
||||||
|
class WalletTypeInfo():
|
||||||
|
wallet_type: int
|
||||||
|
wallet: Wallet
|
||||||
|
|
||||||
|
def __init__(self, wallet_type: int, wallet: Wallet) -> None:
|
||||||
|
self.wallet_type = wallet_type
|
||||||
|
self.wallet = wallet
|
||||||
|
|
||||||
|
|
||||||
|
api_key_header = APIKeyHeader(name="X-API-KEY", auto_error=False, description="Admin or Invoice key for wallet API's")
|
||||||
|
api_key_query = APIKeyQuery(name="api-key", auto_error=False, description="Admin or Invoice key for wallet API's")
|
||||||
|
async def get_key_type(r: Request,
|
||||||
|
api_key_header: str = Security(api_key_header),
|
||||||
|
api_key_query: str = Security(api_key_query)) -> WalletTypeInfo:
|
||||||
|
# 0: admin
|
||||||
|
# 1: invoice
|
||||||
|
# 2: invalid
|
||||||
|
try:
|
||||||
|
checker = WalletAdminKeyChecker(api_key=api_key_query)
|
||||||
|
await checker.__call__(r)
|
||||||
|
return WalletTypeInfo(0, checker.wallet)
|
||||||
|
except HTTPException as e:
|
||||||
|
if e.status_code == HTTPStatus.UNAUTHORIZED:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
checker = WalletInvoiceKeyChecker()
|
||||||
|
await checker.__call__(r)
|
||||||
|
return WalletTypeInfo(1, checker.wallet)
|
||||||
|
except HTTPException as e:
|
||||||
|
if e.status_code == HTTPStatus.UNAUTHORIZED:
|
||||||
|
return WalletTypeInfo(2, None)
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
|
||||||
def api_validate_post_request(*, schema: dict):
|
def api_validate_post_request(*, schema: dict):
|
||||||
def wrap(view):
|
def wrap(view):
|
||||||
|
@ -77,28 +155,4 @@ def check_user_exists(param: str = "usr"):
|
||||||
return wrap
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
def validate_uuids(
|
|
||||||
params: List[str], *, required: Union[bool, List[str]] = False, version: int = 4
|
|
||||||
):
|
|
||||||
def wrap(view):
|
|
||||||
@wraps(view)
|
|
||||||
async def wrapped_view(**kwargs):
|
|
||||||
query_params = {
|
|
||||||
param: request.args.get(param, type=str) for param in params
|
|
||||||
}
|
|
||||||
|
|
||||||
for param, value in query_params.items():
|
|
||||||
if not value and (required is True or (required and param in required)):
|
|
||||||
abort(HTTPStatus.BAD_REQUEST, f"`{param}` is required.")
|
|
||||||
|
|
||||||
if value:
|
|
||||||
try:
|
|
||||||
UUID(value, version=version)
|
|
||||||
except ValueError:
|
|
||||||
abort(HTTPStatus.BAD_REQUEST, f"`{param}` is not a valid UUID.")
|
|
||||||
|
|
||||||
return await view(**kwargs)
|
|
||||||
|
|
||||||
return wrapped_view
|
|
||||||
|
|
||||||
return wrap
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
from quart import jsonify, url_for, request
|
|
||||||
from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore
|
from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore
|
||||||
|
|
||||||
from lnbits.core.services import create_invoice
|
from lnbits.core.services import create_invoice
|
||||||
|
|
|
@ -2,7 +2,7 @@ import json
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from quart import url_for
|
|
||||||
from typing import Optional, List, Dict
|
from typing import Optional, List, Dict
|
||||||
from lnurl import encode as lnurl_encode # type: ignore
|
from lnurl import encode as lnurl_encode # type: ignore
|
||||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from quart import g, render_template, request
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from lnbits.decorators import check_user_exists, validate_uuids
|
from lnbits.decorators import check_user_exists
|
||||||
from lnbits.core.models import Payment
|
from lnbits.core.models import Payment
|
||||||
from lnbits.core.crud import get_standalone_payment
|
from lnbits.core.crud import get_standalone_payment
|
||||||
|
|
||||||
|
@ -15,8 +14,8 @@ from fastapi.templating import Jinja2Templates
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
@offlineshop_ext.get("/")
|
@offlineshop_ext.get("/")
|
||||||
@validate_uuids(["usr"], required=True)
|
# @validate_uuids(["usr"], required=True)
|
||||||
@check_user_exists()
|
# @check_user_exists()
|
||||||
async def index(request: Request):
|
async def index(request: Request):
|
||||||
return await templates.TemplateResponse("offlineshop/index.html", {"request": request,"user":g.user})
|
return await templates.TemplateResponse("offlineshop/index.html", {"request": request,"user":g.user})
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic.main import BaseModel
|
from pydantic.main import BaseModel
|
||||||
from quart import g, jsonify
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
|
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
|
||||||
|
|
||||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||||
from lnbits.utils.exchange_rates import currencies
|
from lnbits.utils.exchange_rates import currencies
|
||||||
|
from lnbits.requestvars import g
|
||||||
|
|
||||||
from . import offlineshop_ext
|
from . import offlineshop_ext
|
||||||
from .crud import (
|
from .crud import (
|
||||||
|
@ -27,7 +28,7 @@ async def api_list_currencies_available():
|
||||||
@offlineshop_ext.get("/api/v1/offlineshop")
|
@offlineshop_ext.get("/api/v1/offlineshop")
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_shop_from_wallet():
|
async def api_shop_from_wallet():
|
||||||
shop = await get_or_create_shop_by_wallet(g.wallet.id)
|
shop = await get_or_create_shop_by_wallet(g().wallet.id)
|
||||||
items = await get_items(shop.id)
|
items = await get_items(shop.id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -60,7 +61,7 @@ class CreateItemsData(BaseModel):
|
||||||
@offlineshop_ext.put("/api/v1/offlineshop/items/{item_id}")
|
@offlineshop_ext.put("/api/v1/offlineshop/items/{item_id}")
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_add_or_update_item(data: CreateItemsData, item_id=None):
|
async def api_add_or_update_item(data: CreateItemsData, item_id=None):
|
||||||
shop = await get_or_create_shop_by_wallet(g.wallet.id)
|
shop = await get_or_create_shop_by_wallet(g().wallet.id)
|
||||||
if item_id == None:
|
if item_id == None:
|
||||||
await add_item(
|
await add_item(
|
||||||
shop.id,
|
shop.id,
|
||||||
|
@ -87,7 +88,7 @@ async def api_add_or_update_item(data: CreateItemsData, item_id=None):
|
||||||
@offlineshop_ext.delete("/api/v1/offlineshop/items/{item_id}")
|
@offlineshop_ext.delete("/api/v1/offlineshop/items/{item_id}")
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_delete_item(item_id):
|
async def api_delete_item(item_id):
|
||||||
shop = await get_or_create_shop_by_wallet(g.wallet.id)
|
shop = await get_or_create_shop_by_wallet(g().wallet.id)
|
||||||
await delete_item_from_shop(shop.id, item_id)
|
await delete_item_from_shop(shop.id, item_id)
|
||||||
return "", HTTPStatus.NO_CONTENT
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
@ -104,7 +105,7 @@ async def api_set_method(data: CreateMethodData):
|
||||||
wordlist = data.wordlist.split("\n") if data.wordlist else None
|
wordlist = data.wordlist.split("\n") if data.wordlist else None
|
||||||
wordlist = [word.strip() for word in wordlist if word.strip()]
|
wordlist = [word.strip() for word in wordlist if word.strip()]
|
||||||
|
|
||||||
shop = await get_or_create_shop_by_wallet(g.wallet.id)
|
shop = await get_or_create_shop_by_wallet(g().wallet.id)
|
||||||
if not shop:
|
if not shop:
|
||||||
return "", HTTPStatus.NOT_FOUND
|
return "", HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
|
|
|
@ -6,31 +6,34 @@ from lnbits.decorators import check_user_exists, validate_uuids
|
||||||
|
|
||||||
from . import withdraw_ext
|
from . import withdraw_ext
|
||||||
from .crud import get_withdraw_link, chunks
|
from .crud import get_withdraw_link, chunks
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request, Response
|
||||||
|
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
@withdraw_ext.get("/")
|
@withdraw_ext.get("/", status_code=HTTPStatus.OK)
|
||||||
@validate_uuids(["usr"], required=True)
|
@validate_uuids(["usr"], required=True)
|
||||||
@check_user_exists()
|
@check_user_exists()
|
||||||
async def index(request: Request):
|
async def index(request: Request):
|
||||||
return await templates.TemplateResponse("withdraw/index.html", {"request":request,"user":g.user})
|
return await templates.TemplateResponse("withdraw/index.html", {"request":request,"user":g.user})
|
||||||
|
|
||||||
|
|
||||||
@withdraw_ext.get("/<link_id>")
|
@withdraw_ext.get("/{link_id}", status_code=HTTPStatus.OK)
|
||||||
async def display(request: Request, link_id):
|
async def display(request: Request, link_id, response: Response):
|
||||||
link = await get_withdraw_link(link_id, 0) or abort(
|
link = await get_withdraw_link(link_id, 0)
|
||||||
HTTPStatus.NOT_FOUND, "Withdraw link does not exist."
|
if not link:
|
||||||
)
|
response.status_code = HTTPStatus.NOT_FOUND
|
||||||
|
return "Withdraw link does not exist." #probably here is where we should return the 404??
|
||||||
return await templates.TemplateResponse("withdraw/display.html", {"request":request,"link":link, "unique":True})
|
return await templates.TemplateResponse("withdraw/display.html", {"request":request,"link":link, "unique":True})
|
||||||
|
|
||||||
|
|
||||||
@withdraw_ext.get("/img/<link_id>")
|
@withdraw_ext.get("/img/{link_id}", status_code=HTTPStatus.OK)
|
||||||
async def img(request: Request, link_id):
|
async def img(request: Request, link_id, response: Response):
|
||||||
link = await get_withdraw_link(link_id, 0) or abort(
|
link = await get_withdraw_link(link_id, 0)
|
||||||
HTTPStatus.NOT_FOUND, "Withdraw link does not exist."
|
if not link:
|
||||||
)
|
response.status_code = HTTPStatus.NOT_FOUND
|
||||||
|
return "Withdraw link does not exist."
|
||||||
qr = pyqrcode.create(link.lnurl)
|
qr = pyqrcode.create(link.lnurl)
|
||||||
stream = BytesIO()
|
stream = BytesIO()
|
||||||
qr.svg(stream, scale=3)
|
qr.svg(stream, scale=3)
|
||||||
|
@ -46,19 +49,21 @@ async def img(request: Request, link_id):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@withdraw_ext.get("/print/<link_id>")
|
@withdraw_ext.get("/print/{link_id}", status_code=HTTPStatus.OK)
|
||||||
async def print_qr(request: Request, link_id):
|
async def print_qr(request: Request, link_id, response: Response):
|
||||||
link = await get_withdraw_link(link_id) or abort(
|
link = await get_withdraw_link(link_id)
|
||||||
HTTPStatus.NOT_FOUND, "Withdraw link does not exist."
|
if not link:
|
||||||
)
|
response.status_code = HTTPStatus.NOT_FOUND
|
||||||
|
return "Withdraw link does not exist."
|
||||||
if link.uses == 0:
|
if link.uses == 0:
|
||||||
return await templates.TemplateResponse("withdraw/print_qr.html", {"request":request,link:link, unique:False})
|
return await templates.TemplateResponse("withdraw/print_qr.html", {"request":request,link:link, unique:False})
|
||||||
links = []
|
links = []
|
||||||
count = 0
|
count = 0
|
||||||
for x in link.usescsv.split(","):
|
for x in link.usescsv.split(","):
|
||||||
linkk = await get_withdraw_link(link_id, count) or abort(
|
linkk = await get_withdraw_link(link_id, count)
|
||||||
HTTPStatus.NOT_FOUND, "Withdraw link does not exist."
|
if not linkk:
|
||||||
)
|
response.status_code = HTTPStatus.NOT_FOUND
|
||||||
|
return "Withdraw link does not exist."
|
||||||
links.append(str(linkk.lnurl))
|
links.append(str(linkk.lnurl))
|
||||||
count = count + 1
|
count = count + 1
|
||||||
page_link = list(chunks(links, 2))
|
page_link = list(chunks(links, 2))
|
||||||
|
|
|
@ -5,7 +5,7 @@ from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
|
||||||
from lnbits.core.crud import get_user
|
from lnbits.core.crud import get_user
|
||||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from fastapi import FastAPI, Query
|
from fastapi import FastAPI, Query, Response
|
||||||
|
|
||||||
from . import withdraw_ext
|
from . import withdraw_ext
|
||||||
from .crud import (
|
from .crud import (
|
||||||
|
@ -19,47 +19,41 @@ from .crud import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@withdraw_ext.get("/api/v1/links")
|
@withdraw_ext.get("/api/v1/links", status_code=200)
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_links():
|
async def api_links(response: Response):
|
||||||
wallet_ids = [g.wallet.id]
|
wallet_ids = [g.wallet.id]
|
||||||
|
|
||||||
if "all_wallets" in request.args:
|
if "all_wallets" in request.args:
|
||||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
||||||
try:
|
try:
|
||||||
return (
|
return [
|
||||||
[
|
|
||||||
{
|
{
|
||||||
**link._asdict(),
|
**link._asdict(),
|
||||||
**{"lnurl": link.lnurl},
|
**{"lnurl": link.lnurl},
|
||||||
}
|
}
|
||||||
for link in await get_withdraw_links(wallet_ids)
|
for link in await get_withdraw_links(wallet_ids)
|
||||||
],
|
]
|
||||||
HTTPStatus.OK,
|
|
||||||
)
|
|
||||||
except LnurlInvalidUrl:
|
except LnurlInvalidUrl:
|
||||||
return (
|
response.status_code = HTTPStatus.UPGRADE_REQUIRED
|
||||||
{
|
return { "message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor." }
|
||||||
"message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor."
|
|
||||||
},
|
|
||||||
HTTPStatus.UPGRADE_REQUIRED,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@withdraw_ext.get("/api/v1/links/{link_id}")
|
@withdraw_ext.get("/api/v1/links/{link_id}", status_code=200)
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_link_retrieve(link_id):
|
async def api_link_retrieve(link_id, response: Response):
|
||||||
link = await get_withdraw_link(link_id, 0)
|
link = await get_withdraw_link(link_id, 0)
|
||||||
|
|
||||||
if not link:
|
if not link:
|
||||||
return ({"message": "Withdraw link does not exist."},
|
response.status_code = HTTPStatus.NOT_FOUND
|
||||||
HTTPStatus.NOT_FOUND,
|
return {"message": "Withdraw link does not exist."}
|
||||||
)
|
|
||||||
|
|
||||||
if link.wallet != g.wallet.id:
|
if link.wallet != g.wallet.id:
|
||||||
return {"message": "Not your withdraw link."}, HTTPStatus.FORBIDDEN
|
response.status_code = HTTPStatus.FORBIDDEN
|
||||||
|
return {"message": "Not your withdraw link."}
|
||||||
|
|
||||||
return {**link, **{"lnurl": link.lnurl}}, HTTPStatus.OK
|
return {**link, **{"lnurl": link.lnurl}}
|
||||||
|
|
||||||
class CreateData(BaseModel):
|
class CreateData(BaseModel):
|
||||||
title: str = Query(...)
|
title: str = Query(...)
|
||||||
|
@ -69,17 +63,15 @@ class CreateData(BaseModel):
|
||||||
wait_time: int = Query(..., ge=1)
|
wait_time: int = Query(..., ge=1)
|
||||||
is_unique: bool
|
is_unique: bool
|
||||||
|
|
||||||
@withdraw_ext.post("/api/v1/links")
|
@withdraw_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED)
|
||||||
@withdraw_ext.put("/api/v1/links/{link_id}")
|
@withdraw_ext.put("/api/v1/links/{link_id}")
|
||||||
@api_check_wallet_key("admin")
|
@api_check_wallet_key("admin")
|
||||||
async def api_link_create_or_update(data: CreateData, link_id: str = None):
|
async def api_link_create_or_update(data: CreateData, link_id: str = None, response: Response):
|
||||||
if data.max_withdrawable < data.min_withdrawable:
|
if data.max_withdrawable < data.min_withdrawable:
|
||||||
return (
|
response.status_code = HTTPStatus.BAD_REQUEST
|
||||||
{
|
return {
|
||||||
"message": "`max_withdrawable` needs to be at least `min_withdrawable`."
|
"message": "`max_withdrawable` needs to be at least `min_withdrawable`."
|
||||||
},
|
}
|
||||||
HTTPStatus.BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
usescsv = ""
|
usescsv = ""
|
||||||
for i in range(data.uses):
|
for i in range(data.uses):
|
||||||
|
@ -92,43 +84,41 @@ async def api_link_create_or_update(data: CreateData, link_id: str = None):
|
||||||
if link_id:
|
if link_id:
|
||||||
link = await get_withdraw_link(link_id, 0)
|
link = await get_withdraw_link(link_id, 0)
|
||||||
if not link:
|
if not link:
|
||||||
return (
|
response.status_code = HTTPStatus.NOT_FOUND
|
||||||
jsonify({"message": "Withdraw link does not exist."}),
|
return {"message": "Withdraw link does not exist."}
|
||||||
HTTPStatus.NOT_FOUND,
|
|
||||||
)
|
|
||||||
if link.wallet != g.wallet.id:
|
if link.wallet != g.wallet.id:
|
||||||
return {"message": "Not your withdraw link."}, HTTPStatus.FORBIDDEN
|
response.status_code = HTTPStatus.FORBIDDEN
|
||||||
|
return {"message": "Not your withdraw link."}
|
||||||
link = await update_withdraw_link(link_id, **data, usescsv=usescsv, used=0)
|
link = await update_withdraw_link(link_id, **data, usescsv=usescsv, used=0)
|
||||||
else:
|
else:
|
||||||
link = await create_withdraw_link(
|
link = await create_withdraw_link(
|
||||||
wallet_id=g.wallet.id, **data, usescsv=usescsv
|
wallet_id=g.wallet.id, **data, usescsv=usescsv
|
||||||
)
|
)
|
||||||
|
if link_id:
|
||||||
return ({**link, **{"lnurl": link.lnurl}},
|
response.status_code = HTTPStatus.OK
|
||||||
HTTPStatus.OK if link_id else HTTPStatus.CREATED,
|
return {**link, **{"lnurl": link.lnurl}}
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@withdraw_ext.delete("/api/v1/links/{link_id}")
|
@withdraw_ext.delete("/api/v1/links/{link_id}", status_code=HTTPStatus.NO_CONTENT)
|
||||||
@api_check_wallet_key("admin")
|
@api_check_wallet_key("admin")
|
||||||
async def api_link_delete(link_id):
|
async def api_link_delete(link_id, response: Response):
|
||||||
link = await get_withdraw_link(link_id)
|
link = await get_withdraw_link(link_id)
|
||||||
|
|
||||||
if not link:
|
if not link:
|
||||||
return ({"message": "Withdraw link does not exist."},
|
response.status_code = HTTPStatus.NOT_FOUND
|
||||||
HTTPStatus.NOT_FOUND,
|
return {"message": "Withdraw link does not exist."}
|
||||||
)
|
|
||||||
|
|
||||||
if link.wallet != g.wallet.id:
|
if link.wallet != g.wallet.id:
|
||||||
return {"message": "Not your withdraw link."}, HTTPStatus.FORBIDDEN
|
response.status_code = HTTPStatus.FORBIDDEN
|
||||||
|
return {"message": "Not your withdraw link."}
|
||||||
|
|
||||||
await delete_withdraw_link(link_id)
|
await delete_withdraw_link(link_id)
|
||||||
|
|
||||||
return "", HTTPStatus.NO_CONTENT
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@withdraw_ext.get("/api/v1/links/{the_hash}/{lnurl_id}")
|
@withdraw_ext.get("/api/v1/links/{the_hash}/{lnurl_id}", status_code=HTTPStatus.OK)
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_hash_retrieve(the_hash, lnurl_id):
|
async def api_hash_retrieve(the_hash, lnurl_id):
|
||||||
hashCheck = await get_hash_check(the_hash, lnurl_id)
|
hashCheck = await get_hash_check(the_hash, lnurl_id)
|
||||||
return hashCheck, HTTPStatus.OK
|
return hashCheck
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import glob
|
from typing import Any, List, NamedTuple, Optional
|
||||||
|
|
||||||
|
import jinja2
|
||||||
import shortuuid # type: ignore
|
import shortuuid # type: ignore
|
||||||
|
|
||||||
from typing import List, NamedTuple, Optional
|
from lnbits.jinja2_templating import Jinja2Templates
|
||||||
|
from lnbits.requestvars import g
|
||||||
|
|
||||||
from .settings import LNBITS_DISABLED_EXTENSIONS, LNBITS_PATH
|
import lnbits.settings as settings
|
||||||
|
|
||||||
|
|
||||||
class Extension(NamedTuple):
|
class Extension(NamedTuple):
|
||||||
|
@ -20,9 +24,9 @@ class Extension(NamedTuple):
|
||||||
|
|
||||||
class ExtensionManager:
|
class ExtensionManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._disabled: List[str] = LNBITS_DISABLED_EXTENSIONS
|
self._disabled: List[str] = settings.LNBITS_DISABLED_EXTENSIONS
|
||||||
self._extension_folders: List[str] = [
|
self._extension_folders: List[str] = [
|
||||||
x[1] for x in os.walk(os.path.join(LNBITS_PATH, "extensions"))
|
x[1] for x in os.walk(os.path.join(settings.LNBITS_PATH, "extensions"))
|
||||||
][0]
|
][0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -37,7 +41,7 @@ class ExtensionManager:
|
||||||
]:
|
]:
|
||||||
try:
|
try:
|
||||||
with open(
|
with open(
|
||||||
os.path.join(LNBITS_PATH, "extensions", extension, "config.json")
|
os.path.join(settings.LNBITS_PATH, "extensions", extension, "config.json")
|
||||||
) as json_file:
|
) as json_file:
|
||||||
config = json.load(json_file)
|
config = json.load(json_file)
|
||||||
is_valid = True
|
is_valid = True
|
||||||
|
@ -105,7 +109,7 @@ def get_css_vendored(prefer_minified: bool = False) -> List[str]:
|
||||||
def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]:
|
def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]:
|
||||||
paths: List[str] = []
|
paths: List[str] = []
|
||||||
for path in glob.glob(
|
for path in glob.glob(
|
||||||
os.path.join(LNBITS_PATH, "static/vendor/**"), recursive=True
|
os.path.join(settings.LNBITS_PATH, "static/vendor/**"), recursive=True
|
||||||
):
|
):
|
||||||
if path.endswith(".min" + ext):
|
if path.endswith(".min" + ext):
|
||||||
# path is minified
|
# path is minified
|
||||||
|
@ -131,4 +135,36 @@ def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]:
|
||||||
|
|
||||||
|
|
||||||
def url_for_vendored(abspath: str) -> str:
|
def url_for_vendored(abspath: str) -> str:
|
||||||
return "/" + os.path.relpath(abspath, LNBITS_PATH)
|
return "/" + os.path.relpath(abspath, settings.LNBITS_PATH)
|
||||||
|
|
||||||
|
def url_for(
|
||||||
|
endpoint: str,
|
||||||
|
external: Optional[bool] = False,
|
||||||
|
**params: Any,
|
||||||
|
) -> str:
|
||||||
|
base = g().base_url if external else ""
|
||||||
|
url_params = "?"
|
||||||
|
for key in params:
|
||||||
|
url_params += f"{key}={params[key]}&"
|
||||||
|
url = f"{base}{endpoint}{url_params}"
|
||||||
|
return url
|
||||||
|
|
||||||
|
def template_renderer() -> Jinja2Templates:
|
||||||
|
t = Jinja2Templates(
|
||||||
|
loader=jinja2.FileSystemLoader(["lnbits/templates", "lnbits/core/templates"]),
|
||||||
|
)
|
||||||
|
t.env.globals["SITE_TITLE"] = settings.LNBITS_SITE_TITLE
|
||||||
|
t.env.globals["SITE_TAGLINE"] = settings.LNBITS_SITE_TAGLINE
|
||||||
|
t.env.globals["SITE_DESCRIPTION"] = settings.LNBITS_SITE_DESCRIPTION
|
||||||
|
t.env.globals["LNBITS_THEME_OPTIONS"] = settings.LNBITS_THEME_OPTIONS
|
||||||
|
t.env.globals["LNBITS_VERSION"] = settings.LNBITS_COMMIT
|
||||||
|
t.env.globals["EXTENSIONS"] = get_valid_extensions()
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
t.env.globals["VENDORED_JS"] = map(url_for_vendored, get_js_vendored())
|
||||||
|
t.env.globals["VENDORED_CSS"] = map(url_for_vendored, get_css_vendored())
|
||||||
|
else:
|
||||||
|
t.env.globals["VENDORED_JS"] = ["/static/bundle.js"]
|
||||||
|
t.env.globals["VENDORED_CSS"] = ["/static/bundle.css"]
|
||||||
|
|
||||||
|
return t
|
||||||
|
|
|
@ -135,7 +135,7 @@ window.LNbits = {
|
||||||
return obj
|
return obj
|
||||||
},
|
},
|
||||||
user: function (data) {
|
user: function (data) {
|
||||||
var obj = _.object(['id', 'email', 'extensions', 'wallets'], data)
|
var obj = {id: data.id, email: data.email, extensions: data.extensions, wallets: data.wallets}
|
||||||
var mapWallet = this.wallet
|
var mapWallet = this.wallet
|
||||||
obj.wallets = obj.wallets
|
obj.wallets = obj.wallets
|
||||||
.map(function (obj) {
|
.map(function (obj) {
|
||||||
|
@ -153,35 +153,30 @@ window.LNbits = {
|
||||||
return obj
|
return obj
|
||||||
},
|
},
|
||||||
wallet: function (data) {
|
wallet: function (data) {
|
||||||
var obj = _.object(
|
newWallet = {id: data.id, name: data.name, adminkey: data.adminkey, inkey: data.inkey}
|
||||||
['id', 'name', 'user', 'adminkey', 'inkey', 'balance'],
|
newWallet.msat = data.balance_msat
|
||||||
data
|
newWallet.sat = Math.round(data.balance_msat / 1000)
|
||||||
)
|
newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(newWallet.sat)
|
||||||
obj.msat = obj.balance
|
newWallet.url = ['/wallet?usr=', data.user, '&wal=', data.id].join('')
|
||||||
obj.sat = Math.round(obj.balance / 1000)
|
return newWallet
|
||||||
obj.fsat = new Intl.NumberFormat(window.LOCALE).format(obj.sat)
|
|
||||||
obj.url = ['/wallet?usr=', obj.user, '&wal=', obj.id].join('')
|
|
||||||
return obj
|
|
||||||
},
|
},
|
||||||
payment: function (data) {
|
payment: function (data) {
|
||||||
var obj = _.object(
|
obj = {
|
||||||
[
|
checking_id:data.id,
|
||||||
'checking_id',
|
pending: data.pending,
|
||||||
'pending',
|
amount: data.amount,
|
||||||
'amount',
|
fee: data.fee,
|
||||||
'fee',
|
memo: data.memo,
|
||||||
'memo',
|
time: data.time,
|
||||||
'time',
|
bolt11: data.bolt11,
|
||||||
'bolt11',
|
preimage: data.preimage,
|
||||||
'preimage',
|
payment_hash: data.payment_hash,
|
||||||
'payment_hash',
|
extra: data.extra,
|
||||||
'extra',
|
wallet_id: data.wallet_id,
|
||||||
'wallet_id',
|
webhook: data.webhook,
|
||||||
'webhook',
|
webhook_status: data.webhook_status,
|
||||||
'webhook_status'
|
}
|
||||||
],
|
|
||||||
data
|
|
||||||
)
|
|
||||||
obj.date = Quasar.utils.date.formatDate(
|
obj.date = Quasar.utils.date.formatDate(
|
||||||
new Date(obj.time * 1000),
|
new Date(obj.time * 1000),
|
||||||
'YYYY-MM-DD HH:mm'
|
'YYYY-MM-DD HH:mm'
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import time
|
import time
|
||||||
import trio
|
import asyncio
|
||||||
import traceback
|
import traceback
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from quart import current_app
|
|
||||||
from typing import List, Callable
|
from typing import List, Callable
|
||||||
|
|
||||||
from lnbits.settings import WALLET
|
from lnbits.settings import WALLET
|
||||||
|
@ -25,21 +24,21 @@ def record_async(func: Callable) -> Callable:
|
||||||
return recorder
|
return recorder
|
||||||
|
|
||||||
|
|
||||||
def run_deferred_async():
|
async def run_deferred_async():
|
||||||
for func in deferred_async:
|
for func in deferred_async:
|
||||||
current_app.nursery.start_soon(catch_everything_and_restart, func)
|
asyncio.create_task(catch_everything_and_restart(func))
|
||||||
|
|
||||||
|
|
||||||
async def catch_everything_and_restart(func):
|
async def catch_everything_and_restart(func):
|
||||||
try:
|
try:
|
||||||
await func()
|
await func()
|
||||||
except trio.Cancelled:
|
except asyncio.CancelledError:
|
||||||
raise # because we must pass this up
|
raise # because we must pass this up
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print("caught exception in background task:", exc)
|
print("caught exception in background task:", exc)
|
||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
print("will restart the task in 5 seconds.")
|
print("will restart the task in 5 seconds.")
|
||||||
await trio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
await catch_everything_and_restart(func)
|
await catch_everything_and_restart(func)
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,10 +46,10 @@ async def send_push_promise(a, b) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
invoice_listeners: List[trio.MemorySendChannel] = []
|
invoice_listeners: List[asyncio.Queue] = []
|
||||||
|
|
||||||
|
|
||||||
def register_invoice_listener(send_chan: trio.MemorySendChannel):
|
def register_invoice_listener(send_chan: asyncio.Queue):
|
||||||
"""
|
"""
|
||||||
A method intended for extensions to call when they want to be notified about
|
A method intended for extensions to call when they want to be notified about
|
||||||
new invoice payments incoming.
|
new invoice payments incoming.
|
||||||
|
@ -65,18 +64,19 @@ async def webhook_handler():
|
||||||
return "", HTTPStatus.NO_CONTENT
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
internal_invoice_paid, internal_invoice_received = trio.open_memory_channel(0)
|
internal_invoice_queue = asyncio.Queue(0)
|
||||||
|
|
||||||
|
|
||||||
async def internal_invoice_listener():
|
async def internal_invoice_listener():
|
||||||
async for checking_id in internal_invoice_received:
|
while True:
|
||||||
current_app.nursery.start_soon(invoice_callback_dispatcher, checking_id)
|
checking_id = await internal_invoice_queue.get()
|
||||||
|
asyncio.create_task(invoice_callback_dispatcher(checking_id))
|
||||||
|
|
||||||
|
|
||||||
async def invoice_listener():
|
async def invoice_listener():
|
||||||
async for checking_id in WALLET.paid_invoices_stream():
|
async for checking_id in WALLET.paid_invoices_stream():
|
||||||
print("> got a payment notification", checking_id)
|
print("> got a payment notification", checking_id)
|
||||||
current_app.nursery.start_soon(invoice_callback_dispatcher, checking_id)
|
asyncio.create_task(invoice_callback_dispatcher(checking_id))
|
||||||
|
|
||||||
|
|
||||||
async def check_pending_payments():
|
async def check_pending_payments():
|
||||||
|
@ -100,7 +100,7 @@ async def check_pending_payments():
|
||||||
# that will be handled by the global invoice listeners, hopefully
|
# that will be handled by the global invoice listeners, hopefully
|
||||||
incoming = False
|
incoming = False
|
||||||
|
|
||||||
await trio.sleep(60 * 30) # every 30 minutes
|
await asyncio.sleep(60 * 30) # every 30 minutes
|
||||||
|
|
||||||
|
|
||||||
async def perform_balance_checks():
|
async def perform_balance_checks():
|
||||||
|
@ -108,7 +108,7 @@ async def perform_balance_checks():
|
||||||
for bc in await get_balance_checks():
|
for bc in await get_balance_checks():
|
||||||
redeem_lnurl_withdraw(bc.wallet, bc.url)
|
redeem_lnurl_withdraw(bc.wallet, bc.url)
|
||||||
|
|
||||||
await trio.sleep(60 * 60 * 6) # every 6 hours
|
await asyncio.sleep(60 * 60 * 6) # every 6 hours
|
||||||
|
|
||||||
|
|
||||||
async def invoice_callback_dispatcher(checking_id: str):
|
async def invoice_callback_dispatcher(checking_id: str):
|
||||||
|
@ -116,4 +116,4 @@ async def invoice_callback_dispatcher(checking_id: str):
|
||||||
if payment and payment.is_in:
|
if payment and payment.is_in:
|
||||||
await payment.set_pending(False)
|
await payment.set_pending(False)
|
||||||
for send_chan in invoice_listeners:
|
for send_chan in invoice_listeners:
|
||||||
await send_chan.send(payment)
|
await send_chan.put(payment)
|
||||||
|
|
36
lnbits/templates/error.html
Normal file
36
lnbits/templates/error.html
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{% extends "public.html" %} {% block page %}
|
||||||
|
<div class="row q-col-gutter-md justify-center">
|
||||||
|
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||||
|
<q-card class="q-pa-lg">
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<center>
|
||||||
|
<h3 class="q-my-none">Error</h3>
|
||||||
|
<br />
|
||||||
|
<q-icon
|
||||||
|
name="warning"
|
||||||
|
class="text-grey"
|
||||||
|
style="font-size: 20rem"
|
||||||
|
></q-icon>
|
||||||
|
|
||||||
|
<h5 class="q-my-none">{{ err }}</h5>
|
||||||
|
<h4>If you believe this shouldn't be an error please bring it up on https://t.me/lnbits</h4>
|
||||||
|
<br />
|
||||||
|
</center>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %} {% block scripts %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
|
@ -1,4 +1,4 @@
|
||||||
import trio
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
from typing import Callable, NamedTuple
|
from typing import Callable, NamedTuple
|
||||||
|
|
||||||
|
@ -219,12 +219,12 @@ async def btc_price(currency: str) -> float:
|
||||||
"to": currency.lower(),
|
"to": currency.lower(),
|
||||||
}
|
}
|
||||||
rates = []
|
rates = []
|
||||||
send_channel, receive_channel = trio.open_memory_channel(0)
|
send_channel = asyncio.Queue(0)
|
||||||
|
|
||||||
async def controller(nursery):
|
async def controller(nursery):
|
||||||
failures = 0
|
failures = 0
|
||||||
while True:
|
while True:
|
||||||
rate = await receive_channel.receive()
|
rate = await send_channel.get()
|
||||||
if rate:
|
if rate:
|
||||||
rates.append(rate)
|
rates.append(rate)
|
||||||
else:
|
else:
|
||||||
|
@ -248,10 +248,9 @@ async def btc_price(currency: str) -> float:
|
||||||
except Exception:
|
except Exception:
|
||||||
await send_channel.send(None)
|
await send_channel.send(None)
|
||||||
|
|
||||||
async with trio.open_nursery() as nursery:
|
# asyncio.create_task(controller, nursery)
|
||||||
nursery.start_soon(controller, nursery)
|
for key, provider in exchange_rate_providers.items():
|
||||||
for key, provider in exchange_rate_providers.items():
|
asyncio.create_task(fetch_price(key, provider))
|
||||||
nursery.start_soon(fetch_price, key, provider)
|
|
||||||
|
|
||||||
if not rates:
|
if not rates:
|
||||||
return 9999999999
|
return 9999999999
|
||||||
|
|
|
@ -3,7 +3,7 @@ try:
|
||||||
except ImportError: # pragma: nocover
|
except ImportError: # pragma: nocover
|
||||||
LightningRpc = None
|
LightningRpc = None
|
||||||
|
|
||||||
import trio
|
import asyncio
|
||||||
import random
|
import random
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ class CLightningWallet(Wallet):
|
||||||
raise KeyError("supplied an invalid checking_id")
|
raise KeyError("supplied an invalid checking_id")
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
stream = await trio.open_unix_socket(self.rpc)
|
stream = await asyncio.open_unix_socket(self.rpc)
|
||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
while True:
|
while True:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import trio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import httpx
|
import httpx
|
||||||
from os import getenv
|
from os import getenv
|
||||||
|
@ -146,4 +146,4 @@ class LNbitsWallet(Wallet):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
print("lost connection to lnbits /payments/sse, retrying in 5 seconds")
|
print("lost connection to lnbits /payments/sse, retrying in 5 seconds")
|
||||||
await trio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import trio
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
import json
|
import json
|
||||||
import base64
|
import base64
|
||||||
|
@ -183,4 +183,4 @@ class LndRestWallet(Wallet):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
print("lost connection to lnd invoices stream, retrying in 5 seconds")
|
print("lost connection to lnd invoices stream, retrying in 5 seconds")
|
||||||
await trio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import json
|
import json
|
||||||
import trio
|
import asyncio
|
||||||
import httpx
|
import httpx
|
||||||
from os import getenv
|
from os import getenv
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Optional, Dict, AsyncGenerator
|
from typing import Optional, Dict, AsyncGenerator
|
||||||
from quart import request
|
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
|
@ -117,8 +116,9 @@ class LNPayWallet(Wallet):
|
||||||
return PaymentStatus(statuses[r.json()["settled"]])
|
return PaymentStatus(statuses[r.json()["settled"]])
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
self.send, receive = trio.open_memory_channel(0)
|
self.queue = asyncio.Queue(0)
|
||||||
async for value in receive:
|
while True:
|
||||||
|
value = await self.queue.get()
|
||||||
yield value
|
yield value
|
||||||
|
|
||||||
async def webhook_listener(self):
|
async def webhook_listener(self):
|
||||||
|
@ -143,6 +143,6 @@ class LNPayWallet(Wallet):
|
||||||
)
|
)
|
||||||
data = r.json()
|
data = r.json()
|
||||||
if data["settled"]:
|
if data["settled"]:
|
||||||
await self.send.send(lntx_id)
|
await self.queue.put(lntx_id)
|
||||||
|
|
||||||
return "", HTTPStatus.NO_CONTENT
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import trio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import httpx
|
import httpx
|
||||||
from os import getenv
|
from os import getenv
|
||||||
|
@ -150,4 +150,4 @@ class LntxbotWallet(Wallet):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
print("lost connection to lntxbot /payments/stream, retrying in 5 seconds")
|
print("lost connection to lntxbot /payments/stream, retrying in 5 seconds")
|
||||||
await trio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import trio
|
import asyncio
|
||||||
|
from lnbits.helpers import url_for
|
||||||
import hmac
|
import hmac
|
||||||
import httpx
|
import httpx
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from os import getenv
|
from os import getenv
|
||||||
from typing import Optional, AsyncGenerator
|
from typing import Optional, AsyncGenerator
|
||||||
from quart import request, url_for
|
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
|
@ -63,7 +63,7 @@ class OpenNodeWallet(Wallet):
|
||||||
json={
|
json={
|
||||||
"amount": amount,
|
"amount": amount,
|
||||||
"description": memo or "",
|
"description": memo or "",
|
||||||
"callback_url": url_for("webhook_listener", _external=True),
|
"callback_url": url_for("/webhook_listener", _external=True),
|
||||||
},
|
},
|
||||||
timeout=40,
|
timeout=40,
|
||||||
)
|
)
|
||||||
|
@ -125,8 +125,9 @@ class OpenNodeWallet(Wallet):
|
||||||
return PaymentStatus(statuses[r.json()["data"]["status"]])
|
return PaymentStatus(statuses[r.json()["data"]["status"]])
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
self.send, receive = trio.open_memory_channel(0)
|
self.queue = asyncio.Queue(0)
|
||||||
async for value in receive:
|
while True:
|
||||||
|
value = await self.queue.get()
|
||||||
yield value
|
yield value
|
||||||
|
|
||||||
async def webhook_listener(self):
|
async def webhook_listener(self):
|
||||||
|
@ -141,5 +142,5 @@ class OpenNodeWallet(Wallet):
|
||||||
print("invalid webhook, not from opennode")
|
print("invalid webhook, not from opennode")
|
||||||
return "", HTTPStatus.NO_CONTENT
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
await self.send.send(charge_id)
|
await self.queue.put(charge_id)
|
||||||
return "", HTTPStatus.NO_CONTENT
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import trio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import httpx
|
import httpx
|
||||||
import random
|
import random
|
||||||
|
@ -199,4 +199,4 @@ class SparkWallet(Wallet):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
print("lost connection to spark /stream, retrying in 5 seconds")
|
print("lost connection to spark /stream, retrying in 5 seconds")
|
||||||
await trio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user