mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 10:34:20 +01:00
add nostr
This commit is contained in:
@@ -29,4 +29,9 @@ LIGHTNING_FEE_PERCENT=1.0
|
||||
LIGHTNING_RESERVE_FEE_MIN=4000
|
||||
|
||||
LNBITS_ENDPOINT=https://legend.lnbits.com
|
||||
LNBITS_KEY=yourkeyasdasdasd
|
||||
LNBITS_KEY=yourkeyasdasdasd
|
||||
|
||||
# NOSTR
|
||||
|
||||
# this is the hex private key on which you want to receive nostr DMs to
|
||||
# NOSTR_PRIVATE_KEY=hex_nostrprivatekey_here
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "cashu/nostr"]
|
||||
path = cashu/nostr
|
||||
url = https://github.com/callebtc/python-nostr/
|
||||
@@ -52,5 +52,7 @@ if not MINT_URL:
|
||||
LNBITS_ENDPOINT = env.str("LNBITS_ENDPOINT", default=None)
|
||||
LNBITS_KEY = env.str("LNBITS_KEY", default=None)
|
||||
|
||||
NOSTR_PRIVATE_KEY = env.str("NOSTR_PRIVATE_KEY", default=None)
|
||||
|
||||
MAX_ORDER = 64
|
||||
VERSION = "0.6.0"
|
||||
|
||||
1
cashu/nostr
Submodule
1
cashu/nostr
Submodule
Submodule cashu/nostr added at dfca8bfdc5
@@ -15,6 +15,7 @@ from os.path import isdir, join
|
||||
|
||||
import click
|
||||
from loguru import logger
|
||||
import threading
|
||||
|
||||
from cashu.core.base import Proof
|
||||
from cashu.core.helpers import sum_proofs
|
||||
@@ -28,6 +29,7 @@ from cashu.core.settings import (
|
||||
SOCKS_HOST,
|
||||
SOCKS_PORT,
|
||||
TOR,
|
||||
NOSTR_PRIVATE_KEY,
|
||||
VERSION,
|
||||
)
|
||||
from cashu.tor.tor import TorProxy
|
||||
@@ -39,8 +41,9 @@ from cashu.wallet.crud import (
|
||||
)
|
||||
from cashu.wallet.wallet import Wallet as Wallet
|
||||
|
||||
from nostr.client.client import NostrClient
|
||||
from nostr.key import PublicKey
|
||||
from cashu.nostr.nostr.client.client import NostrClient
|
||||
from cashu.nostr.nostr.key import PublicKey
|
||||
from cashu.nostr.nostr.event import Event
|
||||
|
||||
|
||||
async def init_wallet(wallet: Wallet):
|
||||
@@ -438,29 +441,36 @@ async def info(ctx):
|
||||
return
|
||||
|
||||
|
||||
@cli.command("nostr", help="Receive tokens via nostr.")
|
||||
@cli.command("nreceive", help="Receive tokens via nostr.")
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def nostr(ctx):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_mint()
|
||||
client = NostrClient(
|
||||
privatekey_hex="bfc6e7b0b998645d45aa451a3b9a3174bfe696fba78e86a86637a16f43e6c683"
|
||||
)
|
||||
print(f"Your nostr public key: {client.public_key.hex()}")
|
||||
if NOSTR_PRIVATE_KEY is None:
|
||||
print(
|
||||
"Warning!\n\nYou don't have a private key set in your .env file. I will create a random private key for this session but I will not remember it. If you lose this key, you will lose access to the DMs you receive on it."
|
||||
)
|
||||
print("")
|
||||
client = NostrClient(privatekey_hex=NOSTR_PRIVATE_KEY)
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
def get_token_callback(token):
|
||||
def get_token_callback(event: Event, decrypted_content):
|
||||
print(
|
||||
f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}"
|
||||
)
|
||||
try:
|
||||
proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(token))]
|
||||
proofs = [
|
||||
Proof(**p)
|
||||
for p in json.loads(base64.urlsafe_b64decode(decrypted_content))
|
||||
]
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
asyncio.run(wallet.redeem(proofs))
|
||||
wallet.status()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
import threading
|
||||
|
||||
t = threading.Thread(
|
||||
target=client.get_dm,
|
||||
args=(
|
||||
@@ -472,7 +482,7 @@ async def nostr(ctx):
|
||||
t.start()
|
||||
|
||||
|
||||
@cli.command("nostrsend", help="Send tokens via nostr.")
|
||||
@cli.command("nsend", help="Send tokens via nostr.")
|
||||
@click.argument("amount", type=int)
|
||||
@click.argument(
|
||||
"pubkey",
|
||||
@@ -490,16 +500,11 @@ async def nostrsend(ctx, amount: int, pubkey: str):
|
||||
)
|
||||
token = await wallet.serialize_proofs(send_proofs)
|
||||
|
||||
from random import randrange
|
||||
|
||||
# token = f"Token {randrange(1000)}"
|
||||
print(token)
|
||||
wallet.status()
|
||||
|
||||
client = NostrClient(
|
||||
privatekey_hex="bfc6e7b0b598645d45aa451a3b9a3174bfe696fba78e86a86637a16f4ee6d683"
|
||||
)
|
||||
client = NostrClient(privatekey_hex=NOSTR_PRIVATE_KEY)
|
||||
await asyncio.sleep(1)
|
||||
client.dm(token, PublicKey(bytes.fromhex(pubkey)))
|
||||
print("Sent")
|
||||
print(f"Token sent to {pubkey}")
|
||||
client.close()
|
||||
|
||||
137
nostr/bech32.py
137
nostr/bech32.py
@@ -1,137 +0,0 @@
|
||||
# Copyright (c) 2017, 2020 Pieter Wuille
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
"""Reference implementation for Bech32/Bech32m and segwit addresses."""
|
||||
|
||||
|
||||
from enum import Enum
|
||||
|
||||
class Encoding(Enum):
|
||||
"""Enumeration type to list the various supported encodings."""
|
||||
BECH32 = 1
|
||||
BECH32M = 2
|
||||
|
||||
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
BECH32M_CONST = 0x2bc830a3
|
||||
|
||||
def bech32_polymod(values):
|
||||
"""Internal function that computes the Bech32 checksum."""
|
||||
generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
|
||||
chk = 1
|
||||
for value in values:
|
||||
top = chk >> 25
|
||||
chk = (chk & 0x1ffffff) << 5 ^ value
|
||||
for i in range(5):
|
||||
chk ^= generator[i] if ((top >> i) & 1) else 0
|
||||
return chk
|
||||
|
||||
|
||||
def bech32_hrp_expand(hrp):
|
||||
"""Expand the HRP into values for checksum computation."""
|
||||
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
|
||||
|
||||
|
||||
def bech32_verify_checksum(hrp, data):
|
||||
"""Verify a checksum given HRP and converted data characters."""
|
||||
const = bech32_polymod(bech32_hrp_expand(hrp) + data)
|
||||
if const == 1:
|
||||
return Encoding.BECH32
|
||||
if const == BECH32M_CONST:
|
||||
return Encoding.BECH32M
|
||||
return None
|
||||
|
||||
def bech32_create_checksum(hrp, data, spec):
|
||||
"""Compute the checksum values given HRP and data."""
|
||||
values = bech32_hrp_expand(hrp) + data
|
||||
const = BECH32M_CONST if spec == Encoding.BECH32M else 1
|
||||
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const
|
||||
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
|
||||
|
||||
|
||||
def bech32_encode(hrp, data, spec):
|
||||
"""Compute a Bech32 string given HRP and data values."""
|
||||
combined = data + bech32_create_checksum(hrp, data, spec)
|
||||
return hrp + '1' + ''.join([CHARSET[d] for d in combined])
|
||||
|
||||
def bech32_decode(bech):
|
||||
"""Validate a Bech32/Bech32m string, and determine HRP and data."""
|
||||
if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
|
||||
(bech.lower() != bech and bech.upper() != bech)):
|
||||
return (None, None, None)
|
||||
bech = bech.lower()
|
||||
pos = bech.rfind('1')
|
||||
if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
|
||||
return (None, None, None)
|
||||
if not all(x in CHARSET for x in bech[pos+1:]):
|
||||
return (None, None, None)
|
||||
hrp = bech[:pos]
|
||||
data = [CHARSET.find(x) for x in bech[pos+1:]]
|
||||
spec = bech32_verify_checksum(hrp, data)
|
||||
if spec is None:
|
||||
return (None, None, None)
|
||||
return (hrp, data[:-6], spec)
|
||||
|
||||
def convertbits(data, frombits, tobits, pad=True):
|
||||
"""General power-of-2 base conversion."""
|
||||
acc = 0
|
||||
bits = 0
|
||||
ret = []
|
||||
maxv = (1 << tobits) - 1
|
||||
max_acc = (1 << (frombits + tobits - 1)) - 1
|
||||
for value in data:
|
||||
if value < 0 or (value >> frombits):
|
||||
return None
|
||||
acc = ((acc << frombits) | value) & max_acc
|
||||
bits += frombits
|
||||
while bits >= tobits:
|
||||
bits -= tobits
|
||||
ret.append((acc >> bits) & maxv)
|
||||
if pad:
|
||||
if bits:
|
||||
ret.append((acc << (tobits - bits)) & maxv)
|
||||
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
|
||||
return None
|
||||
return ret
|
||||
|
||||
|
||||
def decode(hrp, addr):
|
||||
"""Decode a segwit address."""
|
||||
hrpgot, data, spec = bech32_decode(addr)
|
||||
if hrpgot != hrp:
|
||||
return (None, None)
|
||||
decoded = convertbits(data[1:], 5, 8, False)
|
||||
if decoded is None or len(decoded) < 2 or len(decoded) > 40:
|
||||
return (None, None)
|
||||
if data[0] > 16:
|
||||
return (None, None)
|
||||
if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
|
||||
return (None, None)
|
||||
if data[0] == 0 and spec != Encoding.BECH32 or data[0] != 0 and spec != Encoding.BECH32M:
|
||||
return (None, None)
|
||||
return (data[0], decoded)
|
||||
|
||||
|
||||
def encode(hrp, witver, witprog):
|
||||
"""Encode a segwit address."""
|
||||
spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M
|
||||
ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec)
|
||||
if decode(hrp, ret) == (None, None):
|
||||
return None
|
||||
return ret
|
||||
@@ -1,86 +0,0 @@
|
||||
import base64
|
||||
import getpass
|
||||
from hashlib import md5
|
||||
|
||||
from Cryptodome import Random
|
||||
from Cryptodome.Cipher import AES
|
||||
|
||||
BLOCK_SIZE = 16
|
||||
|
||||
import getpass
|
||||
|
||||
|
||||
class AESCipher(object):
|
||||
"""This class is compatible with crypto-js/aes.js
|
||||
|
||||
Encrypt and decrypt in Javascript using:
|
||||
import AES from "crypto-js/aes.js";
|
||||
import Utf8 from "crypto-js/enc-utf8.js";
|
||||
AES.encrypt(decrypted, password).toString()
|
||||
AES.decrypt(encrypted, password).toString(Utf8);
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, key=None, description=""):
|
||||
self.key = key
|
||||
self.description = description + " "
|
||||
|
||||
def pad(self, data):
|
||||
length = BLOCK_SIZE - (len(data) % BLOCK_SIZE)
|
||||
return data + (chr(length) * length).encode()
|
||||
|
||||
def unpad(self, data):
|
||||
return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))]
|
||||
|
||||
@property
|
||||
def passphrase(self):
|
||||
passphrase = self.key if self.key is not None else None
|
||||
if passphrase is None:
|
||||
passphrase = getpass.getpass(f"Enter {self.description}password:")
|
||||
return passphrase
|
||||
|
||||
def bytes_to_key(self, data, salt, output=48):
|
||||
# extended from https://gist.github.com/gsakkis/4546068
|
||||
assert len(salt) == 8, len(salt)
|
||||
data += salt
|
||||
key = md5(data).digest()
|
||||
final_key = key
|
||||
while len(final_key) < output:
|
||||
key = md5(key + data).digest()
|
||||
final_key += key
|
||||
return final_key[:output]
|
||||
|
||||
def decrypt(self, encrypted: str) -> str: # type: ignore
|
||||
"""Decrypts a string using AES-256-CBC."""
|
||||
passphrase = self.passphrase
|
||||
encrypted = base64.b64decode(encrypted) # type: ignore
|
||||
assert encrypted[0:8] == b"Salted__"
|
||||
salt = encrypted[8:16]
|
||||
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
||||
key = key_iv[:32]
|
||||
iv = key_iv[32:]
|
||||
aes = AES.new(key, AES.MODE_CBC, iv)
|
||||
try:
|
||||
return self.unpad(aes.decrypt(encrypted[16:])).decode() # type: ignore
|
||||
except UnicodeDecodeError:
|
||||
raise ValueError("Wrong passphrase")
|
||||
|
||||
def encrypt(self, message: bytes) -> str:
|
||||
passphrase = self.passphrase
|
||||
salt = Random.new().read(8)
|
||||
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
||||
key = key_iv[:32]
|
||||
iv = key_iv[32:]
|
||||
aes = AES.new(key, AES.MODE_CBC, iv)
|
||||
return base64.b64encode(
|
||||
b"Salted__" + salt + aes.encrypt(self.pad(message))
|
||||
).decode()
|
||||
|
||||
|
||||
# # if this file is executed directly, ask for a macaroon and encrypt it
|
||||
# if __name__ == "__main__":
|
||||
# macaroon = input("Enter macaroon: ")
|
||||
# macaroon = load_macaroon(macaroon)
|
||||
# macaroon = AESCipher(description="encryption").encrypt(macaroon.encode())
|
||||
# logger.info("Encrypted macaroon:")
|
||||
# logger.info(macaroon)
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
from Cryptodome import Random
|
||||
from Cryptodome.Cipher import AES
|
||||
|
||||
plain_text = "This is the text to encrypts"
|
||||
|
||||
# encrypted = "7mH9jq3K9xNfWqIyu9gNpUz8qBvGwsrDJ+ACExdV1DvGgY8q39dkxVKeXD7LWCDrPnoD/ZFHJMRMis8v9lwHfNgJut8EVTMuJJi8oTgJevOBXl+E+bJPwej9hY3k20rgCQistNRtGHUzdWyOv7S1tg==".encode()
|
||||
# iv = "GzDzqOVShWu3Pl2313FBpQ==".encode()
|
||||
|
||||
key = bytes.fromhex("3aa925cb69eb613e2928f8a18279c78b1dca04541dfd064df2eda66b59880795")
|
||||
|
||||
BLOCK_SIZE = 16
|
||||
|
||||
class AESCipher(object):
|
||||
"""This class is compatible with crypto.createCipheriv('aes-256-cbc')
|
||||
|
||||
"""
|
||||
def __init__(self, key=None):
|
||||
self.key = key
|
||||
|
||||
def pad(self, data):
|
||||
length = BLOCK_SIZE - (len(data) % BLOCK_SIZE)
|
||||
return data + (chr(length) * length).encode()
|
||||
|
||||
def unpad(self, data):
|
||||
return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))]
|
||||
|
||||
def encrypt(self, plain_text):
|
||||
cipher = AES.new(self.key, AES.MODE_CBC)
|
||||
b = plain_text.encode("UTF-8")
|
||||
return cipher.iv, cipher.encrypt(self.pad(b))
|
||||
|
||||
def decrypt(self, iv, enc_text):
|
||||
cipher = AES.new(self.key, AES.MODE_CBC, iv=iv)
|
||||
return self.unpad(cipher.decrypt(enc_text).decode("UTF-8"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
aes = AESCipher(key=key)
|
||||
iv, enc_text = aes.encrypt(plain_text)
|
||||
dec_text = aes.decrypt(iv, enc_text)
|
||||
print(dec_text)
|
||||
@@ -1,165 +0,0 @@
|
||||
from typing import *
|
||||
import ssl
|
||||
import time
|
||||
import json
|
||||
import os
|
||||
import base64
|
||||
|
||||
from nostr.event import Event
|
||||
from nostr.relay_manager import RelayManager
|
||||
from nostr.message_type import ClientMessageType
|
||||
from nostr.key import PrivateKey, PublicKey
|
||||
|
||||
from nostr.filter import Filter, Filters
|
||||
from nostr.event import Event, EventKind
|
||||
from nostr.relay_manager import RelayManager
|
||||
from nostr.message_type import ClientMessageType
|
||||
|
||||
# from aes import AESCipher
|
||||
from . import cbc
|
||||
|
||||
|
||||
class NostrClient:
|
||||
relays = [
|
||||
"wss://nostr.zebedee.cloud"
|
||||
] # ["wss://nostr.oxtr.dev"] # ["wss://relay.nostr.info"] "wss://nostr-pub.wellorder.net" "ws://91.237.88.218:2700", "wss://nostrrr.bublina.eu.org", ""wss://nostr-relay.freeberty.net"", , "wss://nostr.oxtr.dev", "wss://relay.nostr.info", "wss://nostr-pub.wellorder.net" , "wss://relayer.fiatjaf.com", "wss://nodestr.fmt.wiz.biz/"
|
||||
relay_manager = RelayManager()
|
||||
private_key: PrivateKey
|
||||
public_key: PublicKey
|
||||
|
||||
def __init__(self, privatekey_hex: str = "", relays: List[str] = []):
|
||||
self.generate_keys(privatekey_hex)
|
||||
|
||||
if len(relays):
|
||||
self.relays = relays
|
||||
|
||||
for relay in self.relays:
|
||||
self.relay_manager.add_relay(relay)
|
||||
self.relay_manager.open_connections(
|
||||
{"cert_reqs": ssl.CERT_NONE}
|
||||
) # NOTE: This disables ssl certificate verification
|
||||
|
||||
def close(self):
|
||||
self.relay_manager.close_connections()
|
||||
|
||||
def generate_keys(self, privatekey_hex: str = None):
|
||||
pk = bytes.fromhex(privatekey_hex) if privatekey_hex else None
|
||||
self.private_key = PrivateKey(pk)
|
||||
self.public_key = self.private_key.public_key
|
||||
print(f"Private key: {self.private_key.bech32()} ({self.private_key.hex()})")
|
||||
print(f"Public key: {self.public_key.bech32()} ({self.public_key.hex()})")
|
||||
|
||||
def post(self, message: str):
|
||||
event = Event(self.public_key.hex(), message, kind=EventKind.TEXT_NOTE)
|
||||
event.sign(self.private_key.hex())
|
||||
message = json.dumps([ClientMessageType.EVENT, event.to_json_object()])
|
||||
print("Publishing message:")
|
||||
print(message)
|
||||
self.relay_manager.publish_message(message)
|
||||
|
||||
def get_post(self, sender_publickey: PublicKey):
|
||||
filters = Filters(
|
||||
[Filter(authors=[sender_publickey.hex()], kinds=[EventKind.TEXT_NOTE])]
|
||||
)
|
||||
subscription_id = os.urandom(4).hex()
|
||||
self.relay_manager.add_subscription(subscription_id, filters)
|
||||
|
||||
request = [ClientMessageType.REQUEST, subscription_id]
|
||||
request.extend(filters.to_json_array())
|
||||
message = json.dumps(request)
|
||||
print("Subscribing to events:")
|
||||
print(message)
|
||||
self.relay_manager.publish_message(message)
|
||||
|
||||
message_received = False
|
||||
while True:
|
||||
while self.relay_manager.message_pool.has_events():
|
||||
event_msg = self.relay_manager.message_pool.get_event()
|
||||
print(event_msg.event.content)
|
||||
message_received = True
|
||||
break
|
||||
else:
|
||||
time.sleep(0.1)
|
||||
|
||||
def dm(self, message: str, to_pubkey: PublicKey):
|
||||
|
||||
shared_secret = self.private_key.compute_shared_secret(
|
||||
to_pubkey.hex()
|
||||
)
|
||||
|
||||
# print("shared secret: ", shared_secret.hex())
|
||||
# print("plain text:", message)
|
||||
aes = cbc.AESCipher(key=shared_secret)
|
||||
iv, enc_text = aes.encrypt(message)
|
||||
# print("encrypt iv: ", iv)
|
||||
content = f"{base64.b64encode(enc_text).decode('utf-8')}?iv={base64.b64encode(iv).decode('utf-8')}"
|
||||
|
||||
|
||||
event = Event(
|
||||
self.public_key.hex(),
|
||||
content,
|
||||
tags=[["p", to_pubkey.hex()]],
|
||||
kind=EventKind.ENCRYPTED_DIRECT_MESSAGE,
|
||||
)
|
||||
event.sign(self.private_key.hex())
|
||||
event_message = json.dumps([ClientMessageType.EVENT, event.to_json_object()])
|
||||
# print("DM message:")
|
||||
# print(event_message)
|
||||
|
||||
time.sleep(1)
|
||||
self.relay_manager.publish_message(event_message)
|
||||
|
||||
def get_dm(self, sender_publickey: PublicKey, callback_func=None):
|
||||
filters = Filters(
|
||||
[
|
||||
Filter(
|
||||
kinds=[EventKind.ENCRYPTED_DIRECT_MESSAGE],
|
||||
tags={"#p": [sender_publickey.hex()]},
|
||||
)
|
||||
]
|
||||
)
|
||||
subscription_id = os.urandom(4).hex()
|
||||
self.relay_manager.add_subscription(subscription_id, filters)
|
||||
|
||||
request = [ClientMessageType.REQUEST, subscription_id]
|
||||
request.extend(filters.to_json_array())
|
||||
message = json.dumps(request)
|
||||
# print("Subscribing to events:")
|
||||
# print(message)
|
||||
self.relay_manager.publish_message(message)
|
||||
|
||||
while True:
|
||||
while self.relay_manager.message_pool.has_events():
|
||||
event_msg = self.relay_manager.message_pool.get_event()
|
||||
|
||||
if "?iv=" in event_msg.event.content:
|
||||
try:
|
||||
shared_secret = self.private_key.compute_shared_secret(
|
||||
event_msg.event.public_key
|
||||
)
|
||||
# print("shared secret: ", shared_secret.hex())
|
||||
# print("plain text:", message)
|
||||
aes = cbc.AESCipher(key=shared_secret)
|
||||
enc_text_b64, iv_b64 = event_msg.event.content.split("?iv=")
|
||||
iv = base64.decodebytes(iv_b64.encode("utf-8"))
|
||||
enc_text = base64.decodebytes(enc_text_b64.encode("utf-8"))
|
||||
# print("decrypt iv: ", iv)
|
||||
dec_text = aes.decrypt(iv, enc_text)
|
||||
print(f"From {event_msg.event.public_key[:5]}...: {dec_text}")
|
||||
if callback_func:
|
||||
callback_func(dec_text)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
print(f"\nFrom {event_msg.event.public_key[:5]}...: {event_msg.event.content}")
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
async def subscribe(self):
|
||||
while True:
|
||||
while self.relay_manager.message_pool.has_events():
|
||||
event_msg = self.relay_manager.message_pool.get_event()
|
||||
print(event_msg.event.content)
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import time
|
||||
import json
|
||||
from enum import IntEnum
|
||||
from secp256k1 import PrivateKey, PublicKey
|
||||
from hashlib import sha256
|
||||
|
||||
class EventKind(IntEnum):
|
||||
SET_METADATA = 0
|
||||
TEXT_NOTE = 1
|
||||
RECOMMEND_RELAY = 2
|
||||
CONTACTS = 3
|
||||
ENCRYPTED_DIRECT_MESSAGE = 4
|
||||
DELETE = 5
|
||||
|
||||
class Event():
|
||||
def __init__(
|
||||
self,
|
||||
public_key: str,
|
||||
content: str,
|
||||
created_at: int=int(time.time()),
|
||||
kind: int=EventKind.TEXT_NOTE,
|
||||
tags: "list[list[str]]"=[],
|
||||
id: str=None,
|
||||
signature: str=None) -> None:
|
||||
if not isinstance(content, str):
|
||||
raise TypeError("Argument 'content' must be of type str")
|
||||
|
||||
self.id = id if not id is None else Event.compute_id(public_key, created_at, kind, tags, content)
|
||||
self.public_key = public_key
|
||||
self.content = content
|
||||
self.created_at = created_at
|
||||
self.kind = kind
|
||||
self.tags = tags
|
||||
self.signature = signature
|
||||
|
||||
@staticmethod
|
||||
def serialize(public_key: str, created_at: int, kind: int, tags: "list[list[str]]", content: str) -> bytes:
|
||||
data = [0, public_key, created_at, kind, tags, content]
|
||||
data_str = json.dumps(data, separators=(',', ':'), ensure_ascii=False)
|
||||
return data_str.encode()
|
||||
|
||||
@staticmethod
|
||||
def compute_id(public_key: str, created_at: int, kind: int, tags: "list[list[str]]", content: str) -> str:
|
||||
return sha256(Event.serialize(public_key, created_at, kind, tags, content)).hexdigest()
|
||||
|
||||
def sign(self, private_key_hex: str) -> None:
|
||||
sk = PrivateKey(bytes.fromhex(private_key_hex))
|
||||
sig = sk.schnorr_sign(bytes.fromhex(self.id), None, raw=True)
|
||||
self.signature = sig.hex()
|
||||
|
||||
def verify(self) -> bool:
|
||||
pub_key = PublicKey(bytes.fromhex("02" + self.public_key), True) # add 02 for schnorr (bip340)
|
||||
event_id = Event.compute_id(self.public_key, self.created_at, self.kind, self.tags, self.content)
|
||||
return pub_key.schnorr_verify(bytes.fromhex(event_id), bytes.fromhex(self.signature), None, raw=True)
|
||||
|
||||
def to_json_object(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"pubkey": self.public_key,
|
||||
"created_at": self.created_at,
|
||||
"kind": self.kind,
|
||||
"tags": self.tags,
|
||||
"content": self.content,
|
||||
"sig": self.signature
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
from collections import UserList
|
||||
from .event import Event
|
||||
|
||||
|
||||
class Filter:
|
||||
def __init__(
|
||||
self,
|
||||
ids: "list[str]" = None,
|
||||
kinds: "list[int]" = None,
|
||||
authors: "list[str]" = None,
|
||||
since: int = None,
|
||||
until: int = None,
|
||||
tags: "dict[str, list[str]]" = None,
|
||||
limit: int = None,
|
||||
) -> None:
|
||||
self.IDs = ids
|
||||
self.kinds = kinds
|
||||
self.authors = authors
|
||||
self.since = since
|
||||
self.until = until
|
||||
self.tags = tags
|
||||
self.limit = limit
|
||||
|
||||
def matches(self, event: Event) -> bool:
|
||||
if self.IDs != None and event.id not in self.IDs:
|
||||
return False
|
||||
if self.kinds != None and event.kind not in self.kinds:
|
||||
return False
|
||||
if self.authors != None and event.public_key not in self.authors:
|
||||
return False
|
||||
if self.since != None and event.created_at < self.since:
|
||||
return False
|
||||
if self.until != None and event.created_at > self.until:
|
||||
return False
|
||||
if self.tags != None and len(event.tags) == 0:
|
||||
return False
|
||||
if self.tags != None:
|
||||
e_tag_identifiers = [e_tag[0] for e_tag in event.tags]
|
||||
for f_tag, f_tag_values in self.tags.items():
|
||||
if f_tag[1:] not in e_tag_identifiers:
|
||||
return False
|
||||
for e_tag in event.tags:
|
||||
if e_tag[1] not in f_tag_values:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def to_json_object(self) -> dict:
|
||||
res = {}
|
||||
if self.IDs != None:
|
||||
res["ids"] = self.IDs
|
||||
if self.kinds != None:
|
||||
res["kinds"] = self.kinds
|
||||
if self.authors != None:
|
||||
res["authors"] = self.authors
|
||||
if self.since != None:
|
||||
res["since"] = self.since
|
||||
if self.until != None:
|
||||
res["until"] = self.until
|
||||
if self.tags != None:
|
||||
for tag, values in self.tags.items():
|
||||
res[tag] = values
|
||||
if self.limit != None:
|
||||
res["limit"] = self.limit
|
||||
|
||||
return res
|
||||
|
||||
|
||||
class Filters(UserList):
|
||||
def __init__(self, initlist: "list[Filter]" = []) -> None:
|
||||
super().__init__(initlist)
|
||||
self.data: "list[Filter]"
|
||||
|
||||
def match(self, event: Event):
|
||||
for filter in self.data:
|
||||
if filter.matches(event):
|
||||
return True
|
||||
return False
|
||||
|
||||
def to_json_array(self) -> list:
|
||||
return [filter.to_json_object() for filter in self.data]
|
||||
86
nostr/key.py
86
nostr/key.py
@@ -1,86 +0,0 @@
|
||||
import secrets
|
||||
import base64
|
||||
import secp256k1
|
||||
from cffi import FFI
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives import padding
|
||||
from . import bech32
|
||||
|
||||
class PublicKey:
|
||||
def __init__(self, raw_bytes: bytes) -> None:
|
||||
self.raw_bytes = raw_bytes
|
||||
|
||||
def bech32(self) -> str:
|
||||
converted_bits = bech32.convertbits(self.raw_bytes, 8, 5)
|
||||
return bech32.bech32_encode("npub", converted_bits, bech32.Encoding.BECH32)
|
||||
|
||||
def hex(self) -> str:
|
||||
return self.raw_bytes.hex()
|
||||
|
||||
def verify_signed_message_hash(self, hash: str, sig: str) -> bool:
|
||||
pk = secp256k1.PublicKey(b"\x02" + self.raw_bytes, True)
|
||||
return pk.schnorr_verify(bytes.fromhex(hash), bytes.fromhex(sig), None, True)
|
||||
|
||||
class PrivateKey:
|
||||
def __init__(self, raw_secret: bytes=None) -> None:
|
||||
if not raw_secret is None:
|
||||
self.raw_secret = raw_secret
|
||||
else:
|
||||
self.raw_secret = secrets.token_bytes(32)
|
||||
|
||||
sk = secp256k1.PrivateKey(self.raw_secret)
|
||||
self.public_key = PublicKey(sk.pubkey.serialize()[1:])
|
||||
|
||||
def bech32(self) -> str:
|
||||
converted_bits = bech32.convertbits(self.raw_secret, 8, 5)
|
||||
return bech32.bech32_encode("nsec", converted_bits, bech32.Encoding.BECH32)
|
||||
|
||||
def hex(self) -> str:
|
||||
return self.raw_secret.hex()
|
||||
|
||||
def tweak_add(self, scalar: bytes) -> bytes:
|
||||
sk = secp256k1.PrivateKey(self.raw_secret)
|
||||
return sk.tweak_add(scalar)
|
||||
|
||||
def compute_shared_secret(self, public_key_hex: str) -> bytes:
|
||||
pk = secp256k1.PublicKey(bytes.fromhex("02" + public_key_hex), True)
|
||||
return pk.ecdh(self.raw_secret, hashfn=copy_x)
|
||||
|
||||
def encrypt_message(self, message: str, public_key_hex: str) -> str:
|
||||
padder = padding.PKCS7(128).padder()
|
||||
padded_data = padder.update(message.encode()) + padder.finalize()
|
||||
|
||||
iv = secrets.token_bytes(16)
|
||||
cipher = Cipher(algorithms.AES(self.compute_shared_secret(public_key_hex)), modes.CBC(iv))
|
||||
|
||||
encryptor = cipher.encryptor()
|
||||
encrypted_message = encryptor.update(padded_data) + encryptor.finalize()
|
||||
|
||||
return f"{base64.b64encode(encrypted_message).decode()}?iv={base64.b64encode(iv).decode()}"
|
||||
|
||||
def decrypt_message(self, encoded_message: str, public_key_hex: str) -> str:
|
||||
encoded_data = encoded_message.split('?iv=')
|
||||
encoded_content, encoded_iv = encoded_data[0], encoded_data[1]
|
||||
|
||||
iv = base64.b64decode(encoded_iv)
|
||||
cipher = Cipher(algorithms.AES(self.compute_shared_secret(public_key_hex)), modes.CBC(iv))
|
||||
encrypted_content = base64.b64decode(encoded_content)
|
||||
|
||||
decryptor = cipher.decryptor()
|
||||
decrypted_message = decryptor.update(encrypted_content) + decryptor.finalize()
|
||||
|
||||
unpadder = padding.PKCS7(128).unpadder()
|
||||
unpadded_data = unpadder.update(decrypted_message) + unpadder.finalize()
|
||||
|
||||
return unpadded_data.decode()
|
||||
|
||||
def sign_message_hash(self, hash: bytes) -> str:
|
||||
sk = secp256k1.PrivateKey(self.raw_secret)
|
||||
sig = sk.schnorr_sign(hash, None, raw=True)
|
||||
return sig.hex()
|
||||
|
||||
ffi = FFI()
|
||||
@ffi.callback("int (unsigned char *, const unsigned char *, const unsigned char *, void *)")
|
||||
def copy_x(output, x32, y32, data):
|
||||
ffi.memmove(output, x32, 32)
|
||||
return 1
|
||||
@@ -1,78 +0,0 @@
|
||||
import json
|
||||
from queue import Queue
|
||||
from threading import Lock
|
||||
from .message_type import RelayMessageType
|
||||
from .event import Event
|
||||
|
||||
|
||||
class EventMessage:
|
||||
def __init__(self, event: Event, subscription_id: str, url: str) -> None:
|
||||
self.event = event
|
||||
self.subscription_id = subscription_id
|
||||
self.url = url
|
||||
|
||||
|
||||
class NoticeMessage:
|
||||
def __init__(self, content: str, url: str) -> None:
|
||||
self.content = content
|
||||
self.url = url
|
||||
|
||||
|
||||
class EndOfStoredEventsMessage:
|
||||
def __init__(self, subscription_id: str, url: str) -> None:
|
||||
self.subscription_id = subscription_id
|
||||
self.url = url
|
||||
|
||||
|
||||
class MessagePool:
|
||||
def __init__(self) -> None:
|
||||
self.events: Queue[EventMessage] = Queue()
|
||||
self.notices: Queue[NoticeMessage] = Queue()
|
||||
self.eose_notices: Queue[EndOfStoredEventsMessage] = Queue()
|
||||
self._unique_events: set = set()
|
||||
self.lock: Lock = Lock()
|
||||
|
||||
def add_message(self, message: str, url: str):
|
||||
self._process_message(message, url)
|
||||
|
||||
def get_event(self):
|
||||
return self.events.get()
|
||||
|
||||
def get_notice(self):
|
||||
return self.notices.get()
|
||||
|
||||
def get_eose_notice(self):
|
||||
return self.eose_notices.get()
|
||||
|
||||
def has_events(self):
|
||||
return self.events.qsize() > 0
|
||||
|
||||
def has_notices(self):
|
||||
return self.notices.qsize() > 0
|
||||
|
||||
def has_eose_notices(self):
|
||||
return self.eose_notices.qsize() > 0
|
||||
|
||||
def _process_message(self, message: str, url: str):
|
||||
message_json = json.loads(message)
|
||||
message_type = message_json[0]
|
||||
if message_type == RelayMessageType.EVENT:
|
||||
subscription_id = message_json[1]
|
||||
e = message_json[2]
|
||||
event = Event(
|
||||
e["pubkey"],
|
||||
e["content"],
|
||||
e["created_at"],
|
||||
e["kind"],
|
||||
e["tags"],
|
||||
e["id"],
|
||||
e["sig"],
|
||||
)
|
||||
with self.lock:
|
||||
if not event.id in self._unique_events:
|
||||
self.events.put(EventMessage(event, subscription_id, url))
|
||||
self._unique_events.add(event.id)
|
||||
elif message_type == RelayMessageType.NOTICE:
|
||||
self.notices.put(NoticeMessage(message_json[1], url))
|
||||
elif message_type == RelayMessageType.END_OF_STORED_EVENTS:
|
||||
self.eose_notices.put(EndOfStoredEventsMessage(message_json[1], url))
|
||||
@@ -1,15 +0,0 @@
|
||||
class ClientMessageType:
|
||||
EVENT = "EVENT"
|
||||
REQUEST = "REQ"
|
||||
CLOSE = "CLOSE"
|
||||
|
||||
class RelayMessageType:
|
||||
EVENT = "EVENT"
|
||||
NOTICE = "NOTICE"
|
||||
END_OF_STORED_EVENTS = "EOSE"
|
||||
|
||||
@staticmethod
|
||||
def is_valid(type: str) -> bool:
|
||||
if type == RelayMessageType.EVENT or type == RelayMessageType.NOTICE or type == RelayMessageType.END_OF_STORED_EVENTS:
|
||||
return True
|
||||
return False
|
||||
@@ -1,137 +0,0 @@
|
||||
# Copyright (c) 2017, 2020 Pieter Wuille
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
"""Reference implementation for Bech32/Bech32m and segwit addresses."""
|
||||
|
||||
|
||||
from enum import Enum
|
||||
|
||||
class Encoding(Enum):
|
||||
"""Enumeration type to list the various supported encodings."""
|
||||
BECH32 = 1
|
||||
BECH32M = 2
|
||||
|
||||
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
BECH32M_CONST = 0x2bc830a3
|
||||
|
||||
def bech32_polymod(values):
|
||||
"""Internal function that computes the Bech32 checksum."""
|
||||
generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
|
||||
chk = 1
|
||||
for value in values:
|
||||
top = chk >> 25
|
||||
chk = (chk & 0x1ffffff) << 5 ^ value
|
||||
for i in range(5):
|
||||
chk ^= generator[i] if ((top >> i) & 1) else 0
|
||||
return chk
|
||||
|
||||
|
||||
def bech32_hrp_expand(hrp):
|
||||
"""Expand the HRP into values for checksum computation."""
|
||||
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
|
||||
|
||||
|
||||
def bech32_verify_checksum(hrp, data):
|
||||
"""Verify a checksum given HRP and converted data characters."""
|
||||
const = bech32_polymod(bech32_hrp_expand(hrp) + data)
|
||||
if const == 1:
|
||||
return Encoding.BECH32
|
||||
if const == BECH32M_CONST:
|
||||
return Encoding.BECH32M
|
||||
return None
|
||||
|
||||
def bech32_create_checksum(hrp, data, spec):
|
||||
"""Compute the checksum values given HRP and data."""
|
||||
values = bech32_hrp_expand(hrp) + data
|
||||
const = BECH32M_CONST if spec == Encoding.BECH32M else 1
|
||||
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const
|
||||
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
|
||||
|
||||
|
||||
def bech32_encode(hrp, data, spec):
|
||||
"""Compute a Bech32 string given HRP and data values."""
|
||||
combined = data + bech32_create_checksum(hrp, data, spec)
|
||||
return hrp + '1' + ''.join([CHARSET[d] for d in combined])
|
||||
|
||||
def bech32_decode(bech):
|
||||
"""Validate a Bech32/Bech32m string, and determine HRP and data."""
|
||||
if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
|
||||
(bech.lower() != bech and bech.upper() != bech)):
|
||||
return (None, None, None)
|
||||
bech = bech.lower()
|
||||
pos = bech.rfind('1')
|
||||
if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
|
||||
return (None, None, None)
|
||||
if not all(x in CHARSET for x in bech[pos+1:]):
|
||||
return (None, None, None)
|
||||
hrp = bech[:pos]
|
||||
data = [CHARSET.find(x) for x in bech[pos+1:]]
|
||||
spec = bech32_verify_checksum(hrp, data)
|
||||
if spec is None:
|
||||
return (None, None, None)
|
||||
return (hrp, data[:-6], spec)
|
||||
|
||||
def convertbits(data, frombits, tobits, pad=True):
|
||||
"""General power-of-2 base conversion."""
|
||||
acc = 0
|
||||
bits = 0
|
||||
ret = []
|
||||
maxv = (1 << tobits) - 1
|
||||
max_acc = (1 << (frombits + tobits - 1)) - 1
|
||||
for value in data:
|
||||
if value < 0 or (value >> frombits):
|
||||
return None
|
||||
acc = ((acc << frombits) | value) & max_acc
|
||||
bits += frombits
|
||||
while bits >= tobits:
|
||||
bits -= tobits
|
||||
ret.append((acc >> bits) & maxv)
|
||||
if pad:
|
||||
if bits:
|
||||
ret.append((acc << (tobits - bits)) & maxv)
|
||||
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
|
||||
return None
|
||||
return ret
|
||||
|
||||
|
||||
def decode(hrp, addr):
|
||||
"""Decode a segwit address."""
|
||||
hrpgot, data, spec = bech32_decode(addr)
|
||||
if hrpgot != hrp:
|
||||
return (None, None)
|
||||
decoded = convertbits(data[1:], 5, 8, False)
|
||||
if decoded is None or len(decoded) < 2 or len(decoded) > 40:
|
||||
return (None, None)
|
||||
if data[0] > 16:
|
||||
return (None, None)
|
||||
if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
|
||||
return (None, None)
|
||||
if data[0] == 0 and spec != Encoding.BECH32 or data[0] != 0 and spec != Encoding.BECH32M:
|
||||
return (None, None)
|
||||
return (data[0], decoded)
|
||||
|
||||
|
||||
def encode(hrp, witver, witprog):
|
||||
"""Encode a segwit address."""
|
||||
spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M
|
||||
ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec)
|
||||
if decode(hrp, ret) == (None, None):
|
||||
return None
|
||||
return ret
|
||||
@@ -1,86 +0,0 @@
|
||||
import base64
|
||||
import getpass
|
||||
from hashlib import md5
|
||||
|
||||
from Cryptodome import Random
|
||||
from Cryptodome.Cipher import AES
|
||||
|
||||
BLOCK_SIZE = 16
|
||||
|
||||
import getpass
|
||||
|
||||
|
||||
class AESCipher(object):
|
||||
"""This class is compatible with crypto-js/aes.js
|
||||
|
||||
Encrypt and decrypt in Javascript using:
|
||||
import AES from "crypto-js/aes.js";
|
||||
import Utf8 from "crypto-js/enc-utf8.js";
|
||||
AES.encrypt(decrypted, password).toString()
|
||||
AES.decrypt(encrypted, password).toString(Utf8);
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, key=None, description=""):
|
||||
self.key = key
|
||||
self.description = description + " "
|
||||
|
||||
def pad(self, data):
|
||||
length = BLOCK_SIZE - (len(data) % BLOCK_SIZE)
|
||||
return data + (chr(length) * length).encode()
|
||||
|
||||
def unpad(self, data):
|
||||
return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))]
|
||||
|
||||
@property
|
||||
def passphrase(self):
|
||||
passphrase = self.key if self.key is not None else None
|
||||
if passphrase is None:
|
||||
passphrase = getpass.getpass(f"Enter {self.description}password:")
|
||||
return passphrase
|
||||
|
||||
def bytes_to_key(self, data, salt, output=48):
|
||||
# extended from https://gist.github.com/gsakkis/4546068
|
||||
assert len(salt) == 8, len(salt)
|
||||
data += salt
|
||||
key = md5(data).digest()
|
||||
final_key = key
|
||||
while len(final_key) < output:
|
||||
key = md5(key + data).digest()
|
||||
final_key += key
|
||||
return final_key[:output]
|
||||
|
||||
def decrypt(self, encrypted: str) -> str: # type: ignore
|
||||
"""Decrypts a string using AES-256-CBC."""
|
||||
passphrase = self.passphrase
|
||||
encrypted = base64.b64decode(encrypted) # type: ignore
|
||||
assert encrypted[0:8] == b"Salted__"
|
||||
salt = encrypted[8:16]
|
||||
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
||||
key = key_iv[:32]
|
||||
iv = key_iv[32:]
|
||||
aes = AES.new(key, AES.MODE_CBC, iv)
|
||||
try:
|
||||
return self.unpad(aes.decrypt(encrypted[16:])).decode() # type: ignore
|
||||
except UnicodeDecodeError:
|
||||
raise ValueError("Wrong passphrase")
|
||||
|
||||
def encrypt(self, message: bytes) -> str:
|
||||
passphrase = self.passphrase
|
||||
salt = Random.new().read(8)
|
||||
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
||||
key = key_iv[:32]
|
||||
iv = key_iv[32:]
|
||||
aes = AES.new(key, AES.MODE_CBC, iv)
|
||||
return base64.b64encode(
|
||||
b"Salted__" + salt + aes.encrypt(self.pad(message))
|
||||
).decode()
|
||||
|
||||
|
||||
# # if this file is executed directly, ask for a macaroon and encrypt it
|
||||
# if __name__ == "__main__":
|
||||
# macaroon = input("Enter macaroon: ")
|
||||
# macaroon = load_macaroon(macaroon)
|
||||
# macaroon = AESCipher(description="encryption").encrypt(macaroon.encode())
|
||||
# logger.info("Encrypted macaroon:")
|
||||
# logger.info(macaroon)
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
from Cryptodome import Random
|
||||
from Cryptodome.Cipher import AES
|
||||
|
||||
plain_text = "This is the text to encrypts"
|
||||
|
||||
# encrypted = "7mH9jq3K9xNfWqIyu9gNpUz8qBvGwsrDJ+ACExdV1DvGgY8q39dkxVKeXD7LWCDrPnoD/ZFHJMRMis8v9lwHfNgJut8EVTMuJJi8oTgJevOBXl+E+bJPwej9hY3k20rgCQistNRtGHUzdWyOv7S1tg==".encode()
|
||||
# iv = "GzDzqOVShWu3Pl2313FBpQ==".encode()
|
||||
|
||||
key = bytes.fromhex("3aa925cb69eb613e2928f8a18279c78b1dca04541dfd064df2eda66b59880795")
|
||||
|
||||
BLOCK_SIZE = 16
|
||||
|
||||
class AESCipher(object):
|
||||
"""This class is compatible with crypto.createCipheriv('aes-256-cbc')
|
||||
|
||||
"""
|
||||
def __init__(self, key=None):
|
||||
self.key = key
|
||||
|
||||
def pad(self, data):
|
||||
length = BLOCK_SIZE - (len(data) % BLOCK_SIZE)
|
||||
return data + (chr(length) * length).encode()
|
||||
|
||||
def unpad(self, data):
|
||||
return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))]
|
||||
|
||||
def encrypt(self, plain_text):
|
||||
cipher = AES.new(self.key, AES.MODE_CBC)
|
||||
b = plain_text.encode("UTF-8")
|
||||
return cipher.iv, cipher.encrypt(self.pad(b))
|
||||
|
||||
def decrypt(self, iv, enc_text):
|
||||
cipher = AES.new(self.key, AES.MODE_CBC, iv=iv)
|
||||
return self.unpad(cipher.decrypt(enc_text).decode("UTF-8"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
aes = AESCipher(key=key)
|
||||
iv, enc_text = aes.encrypt(plain_text)
|
||||
dec_text = aes.decrypt(iv, enc_text)
|
||||
print(dec_text)
|
||||
@@ -1,162 +0,0 @@
|
||||
from typing import *
|
||||
import ssl
|
||||
import time
|
||||
import json
|
||||
import os
|
||||
import base64
|
||||
|
||||
from nostr.event import Event
|
||||
from nostr.relay_manager import RelayManager
|
||||
from nostr.message_type import ClientMessageType
|
||||
from nostr.key import PrivateKey, PublicKey
|
||||
|
||||
from nostr.filter import Filter, Filters
|
||||
from nostr.event import Event, EventKind
|
||||
from nostr.relay_manager import RelayManager
|
||||
from nostr.message_type import ClientMessageType
|
||||
|
||||
# from aes import AESCipher
|
||||
from . import cbc
|
||||
|
||||
|
||||
class NostrClient:
|
||||
relays = [
|
||||
"wss://nostr.zebedee.cloud",
|
||||
"wss://nostr-relay.digitalmob.ro",
|
||||
] # ["wss://nostr.oxtr.dev"] # ["wss://relay.nostr.info"] "wss://nostr-pub.wellorder.net" "ws://91.237.88.218:2700", "wss://nostrrr.bublina.eu.org", ""wss://nostr-relay.freeberty.net"", , "wss://nostr.oxtr.dev", "wss://relay.nostr.info", "wss://nostr-pub.wellorder.net" , "wss://relayer.fiatjaf.com", "wss://nodestr.fmt.wiz.biz/", "wss://no.str.cr", "wss://nostr-relay.digitalmob.ro"
|
||||
relay_manager = RelayManager()
|
||||
private_key: PrivateKey
|
||||
public_key: PublicKey
|
||||
|
||||
def __init__(self, privatekey_hex: str = "", relays: List[str] = []):
|
||||
self.generate_keys(privatekey_hex)
|
||||
|
||||
if len(relays):
|
||||
self.relays = relays
|
||||
|
||||
for relay in self.relays:
|
||||
self.relay_manager.add_relay(relay)
|
||||
self.relay_manager.open_connections(
|
||||
{"cert_reqs": ssl.CERT_NONE}
|
||||
) # NOTE: This disables ssl certificate verification
|
||||
|
||||
def close(self):
|
||||
self.relay_manager.close_connections()
|
||||
|
||||
def generate_keys(self, privatekey_hex: str = None):
|
||||
pk = bytes.fromhex(privatekey_hex) if privatekey_hex else None
|
||||
self.private_key = PrivateKey(pk)
|
||||
self.public_key = self.private_key.public_key
|
||||
print(f"Private key: {self.private_key.bech32()} ({self.private_key.hex()})")
|
||||
print(f"Public key: {self.public_key.bech32()} ({self.public_key.hex()})")
|
||||
|
||||
def post(self, message: str):
|
||||
event = Event(self.public_key.hex(), message, kind=EventKind.TEXT_NOTE)
|
||||
event.sign(self.private_key.hex())
|
||||
message = json.dumps([ClientMessageType.EVENT, event.to_json_object()])
|
||||
# print("Publishing message:")
|
||||
# print(message)
|
||||
self.relay_manager.publish_message(message)
|
||||
|
||||
def get_post(self, sender_publickey: PublicKey):
|
||||
filters = Filters(
|
||||
[Filter(authors=[sender_publickey.hex()], kinds=[EventKind.TEXT_NOTE])]
|
||||
)
|
||||
subscription_id = os.urandom(4).hex()
|
||||
self.relay_manager.add_subscription(subscription_id, filters)
|
||||
|
||||
request = [ClientMessageType.REQUEST, subscription_id]
|
||||
request.extend(filters.to_json_array())
|
||||
message = json.dumps(request)
|
||||
# print("Subscribing to events:")
|
||||
# print(message)
|
||||
self.relay_manager.publish_message(message)
|
||||
|
||||
message_received = False
|
||||
while True:
|
||||
while self.relay_manager.message_pool.has_events():
|
||||
event_msg = self.relay_manager.message_pool.get_event()
|
||||
print(event_msg.event.content)
|
||||
message_received = True
|
||||
break
|
||||
else:
|
||||
time.sleep(0.1)
|
||||
|
||||
def dm(self, message: str, to_pubkey: PublicKey):
|
||||
|
||||
shared_secret = self.private_key.compute_shared_secret(to_pubkey.hex())
|
||||
|
||||
# print("shared secret: ", shared_secret.hex())
|
||||
# print("plain text:", message)
|
||||
aes = cbc.AESCipher(key=shared_secret)
|
||||
iv, enc_text = aes.encrypt(message)
|
||||
# print("encrypt iv: ", iv)
|
||||
content = f"{base64.b64encode(enc_text).decode('utf-8')}?iv={base64.b64encode(iv).decode('utf-8')}"
|
||||
|
||||
event = Event(
|
||||
self.public_key.hex(),
|
||||
content,
|
||||
tags=[["p", to_pubkey.hex()]],
|
||||
kind=EventKind.ENCRYPTED_DIRECT_MESSAGE,
|
||||
)
|
||||
event.sign(self.private_key.hex())
|
||||
event_message = json.dumps([ClientMessageType.EVENT, event.to_json_object()])
|
||||
# print("DM message:")
|
||||
# print(event_message)
|
||||
|
||||
time.sleep(1)
|
||||
self.relay_manager.publish_message(event_message)
|
||||
|
||||
def get_dm(self, sender_publickey: PublicKey, callback_func=None):
|
||||
filters = Filters(
|
||||
[
|
||||
Filter(
|
||||
kinds=[EventKind.ENCRYPTED_DIRECT_MESSAGE],
|
||||
tags={"#p": [sender_publickey.hex()]},
|
||||
)
|
||||
]
|
||||
)
|
||||
subscription_id = os.urandom(4).hex()
|
||||
self.relay_manager.add_subscription(subscription_id, filters)
|
||||
|
||||
request = [ClientMessageType.REQUEST, subscription_id]
|
||||
request.extend(filters.to_json_array())
|
||||
message = json.dumps(request)
|
||||
# print("Subscribing to events:")
|
||||
# print(message)
|
||||
self.relay_manager.publish_message(message)
|
||||
|
||||
while True:
|
||||
while self.relay_manager.message_pool.has_events():
|
||||
event_msg = self.relay_manager.message_pool.get_event()
|
||||
|
||||
if "?iv=" in event_msg.event.content:
|
||||
try:
|
||||
shared_secret = self.private_key.compute_shared_secret(
|
||||
event_msg.event.public_key
|
||||
)
|
||||
# print("shared secret: ", shared_secret.hex())
|
||||
# print("plain text:", message)
|
||||
aes = cbc.AESCipher(key=shared_secret)
|
||||
enc_text_b64, iv_b64 = event_msg.event.content.split("?iv=")
|
||||
iv = base64.decodebytes(iv_b64.encode("utf-8"))
|
||||
enc_text = base64.decodebytes(enc_text_b64.encode("utf-8"))
|
||||
# print("decrypt iv: ", iv)
|
||||
dec_text = aes.decrypt(iv, enc_text)
|
||||
# print(f"From {event_msg.event.public_key[:5]}...: {dec_text}")
|
||||
if callback_func:
|
||||
callback_func(event_msg.event, dec_text)
|
||||
except:
|
||||
pass
|
||||
# else:
|
||||
# print(f"\nFrom {event_msg.event.public_key[:5]}...: {event_msg.event.content}")
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
async def subscribe(self):
|
||||
while True:
|
||||
while self.relay_manager.message_pool.has_events():
|
||||
event_msg = self.relay_manager.message_pool.get_event()
|
||||
print(event_msg.event.content)
|
||||
break
|
||||
time.sleep(0.1)
|
||||
@@ -1,65 +0,0 @@
|
||||
import time
|
||||
import json
|
||||
from enum import IntEnum
|
||||
from secp256k1 import PrivateKey, PublicKey
|
||||
from hashlib import sha256
|
||||
|
||||
class EventKind(IntEnum):
|
||||
SET_METADATA = 0
|
||||
TEXT_NOTE = 1
|
||||
RECOMMEND_RELAY = 2
|
||||
CONTACTS = 3
|
||||
ENCRYPTED_DIRECT_MESSAGE = 4
|
||||
DELETE = 5
|
||||
|
||||
class Event():
|
||||
def __init__(
|
||||
self,
|
||||
public_key: str,
|
||||
content: str,
|
||||
created_at: int=int(time.time()),
|
||||
kind: int=EventKind.TEXT_NOTE,
|
||||
tags: "list[list[str]]"=[],
|
||||
id: str=None,
|
||||
signature: str=None) -> None:
|
||||
if not isinstance(content, str):
|
||||
raise TypeError("Argument 'content' must be of type str")
|
||||
|
||||
self.id = id if not id is None else Event.compute_id(public_key, created_at, kind, tags, content)
|
||||
self.public_key = public_key
|
||||
self.content = content
|
||||
self.created_at = created_at
|
||||
self.kind = kind
|
||||
self.tags = tags
|
||||
self.signature = signature
|
||||
|
||||
@staticmethod
|
||||
def serialize(public_key: str, created_at: int, kind: int, tags: "list[list[str]]", content: str) -> bytes:
|
||||
data = [0, public_key, created_at, kind, tags, content]
|
||||
data_str = json.dumps(data, separators=(',', ':'), ensure_ascii=False)
|
||||
return data_str.encode()
|
||||
|
||||
@staticmethod
|
||||
def compute_id(public_key: str, created_at: int, kind: int, tags: "list[list[str]]", content: str) -> str:
|
||||
return sha256(Event.serialize(public_key, created_at, kind, tags, content)).hexdigest()
|
||||
|
||||
def sign(self, private_key_hex: str) -> None:
|
||||
sk = PrivateKey(bytes.fromhex(private_key_hex))
|
||||
sig = sk.schnorr_sign(bytes.fromhex(self.id), None, raw=True)
|
||||
self.signature = sig.hex()
|
||||
|
||||
def verify(self) -> bool:
|
||||
pub_key = PublicKey(bytes.fromhex("02" + self.public_key), True) # add 02 for schnorr (bip340)
|
||||
event_id = Event.compute_id(self.public_key, self.created_at, self.kind, self.tags, self.content)
|
||||
return pub_key.schnorr_verify(bytes.fromhex(event_id), bytes.fromhex(self.signature), None, raw=True)
|
||||
|
||||
def to_json_object(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"pubkey": self.public_key,
|
||||
"created_at": self.created_at,
|
||||
"kind": self.kind,
|
||||
"tags": self.tags,
|
||||
"content": self.content,
|
||||
"sig": self.signature
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
from collections import UserList
|
||||
from .event import Event
|
||||
|
||||
|
||||
class Filter:
|
||||
def __init__(
|
||||
self,
|
||||
ids: "list[str]" = None,
|
||||
kinds: "list[int]" = None,
|
||||
authors: "list[str]" = None,
|
||||
since: int = None,
|
||||
until: int = None,
|
||||
tags: "dict[str, list[str]]" = None,
|
||||
limit: int = None,
|
||||
) -> None:
|
||||
self.IDs = ids
|
||||
self.kinds = kinds
|
||||
self.authors = authors
|
||||
self.since = since
|
||||
self.until = until
|
||||
self.tags = tags
|
||||
self.limit = limit
|
||||
|
||||
def matches(self, event: Event) -> bool:
|
||||
if self.IDs != None and event.id not in self.IDs:
|
||||
return False
|
||||
if self.kinds != None and event.kind not in self.kinds:
|
||||
return False
|
||||
if self.authors != None and event.public_key not in self.authors:
|
||||
return False
|
||||
if self.since != None and event.created_at < self.since:
|
||||
return False
|
||||
if self.until != None and event.created_at > self.until:
|
||||
return False
|
||||
if self.tags != None and len(event.tags) == 0:
|
||||
return False
|
||||
if self.tags != None:
|
||||
e_tag_identifiers = [e_tag[0] for e_tag in event.tags]
|
||||
for f_tag, f_tag_values in self.tags.items():
|
||||
if f_tag[1:] not in e_tag_identifiers:
|
||||
return False
|
||||
for e_tag in event.tags:
|
||||
if e_tag[1] not in f_tag_values:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def to_json_object(self) -> dict:
|
||||
res = {}
|
||||
if self.IDs != None:
|
||||
res["ids"] = self.IDs
|
||||
if self.kinds != None:
|
||||
res["kinds"] = self.kinds
|
||||
if self.authors != None:
|
||||
res["authors"] = self.authors
|
||||
if self.since != None:
|
||||
res["since"] = self.since
|
||||
if self.until != None:
|
||||
res["until"] = self.until
|
||||
if self.tags != None:
|
||||
for tag, values in self.tags.items():
|
||||
res[tag] = values
|
||||
if self.limit != None:
|
||||
res["limit"] = self.limit
|
||||
|
||||
return res
|
||||
|
||||
|
||||
class Filters(UserList):
|
||||
def __init__(self, initlist: "list[Filter]" = []) -> None:
|
||||
super().__init__(initlist)
|
||||
self.data: "list[Filter]"
|
||||
|
||||
def match(self, event: Event):
|
||||
for filter in self.data:
|
||||
if filter.matches(event):
|
||||
return True
|
||||
return False
|
||||
|
||||
def to_json_array(self) -> list:
|
||||
return [filter.to_json_object() for filter in self.data]
|
||||
@@ -1,86 +0,0 @@
|
||||
import secrets
|
||||
import base64
|
||||
import secp256k1
|
||||
from cffi import FFI
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives import padding
|
||||
from . import bech32
|
||||
|
||||
class PublicKey:
|
||||
def __init__(self, raw_bytes: bytes) -> None:
|
||||
self.raw_bytes = raw_bytes
|
||||
|
||||
def bech32(self) -> str:
|
||||
converted_bits = bech32.convertbits(self.raw_bytes, 8, 5)
|
||||
return bech32.bech32_encode("npub", converted_bits, bech32.Encoding.BECH32)
|
||||
|
||||
def hex(self) -> str:
|
||||
return self.raw_bytes.hex()
|
||||
|
||||
def verify_signed_message_hash(self, hash: str, sig: str) -> bool:
|
||||
pk = secp256k1.PublicKey(b"\x02" + self.raw_bytes, True)
|
||||
return pk.schnorr_verify(bytes.fromhex(hash), bytes.fromhex(sig), None, True)
|
||||
|
||||
class PrivateKey:
|
||||
def __init__(self, raw_secret: bytes=None) -> None:
|
||||
if not raw_secret is None:
|
||||
self.raw_secret = raw_secret
|
||||
else:
|
||||
self.raw_secret = secrets.token_bytes(32)
|
||||
|
||||
sk = secp256k1.PrivateKey(self.raw_secret)
|
||||
self.public_key = PublicKey(sk.pubkey.serialize()[1:])
|
||||
|
||||
def bech32(self) -> str:
|
||||
converted_bits = bech32.convertbits(self.raw_secret, 8, 5)
|
||||
return bech32.bech32_encode("nsec", converted_bits, bech32.Encoding.BECH32)
|
||||
|
||||
def hex(self) -> str:
|
||||
return self.raw_secret.hex()
|
||||
|
||||
def tweak_add(self, scalar: bytes) -> bytes:
|
||||
sk = secp256k1.PrivateKey(self.raw_secret)
|
||||
return sk.tweak_add(scalar)
|
||||
|
||||
def compute_shared_secret(self, public_key_hex: str) -> bytes:
|
||||
pk = secp256k1.PublicKey(bytes.fromhex("02" + public_key_hex), True)
|
||||
return pk.ecdh(self.raw_secret, hashfn=copy_x)
|
||||
|
||||
def encrypt_message(self, message: str, public_key_hex: str) -> str:
|
||||
padder = padding.PKCS7(128).padder()
|
||||
padded_data = padder.update(message.encode()) + padder.finalize()
|
||||
|
||||
iv = secrets.token_bytes(16)
|
||||
cipher = Cipher(algorithms.AES(self.compute_shared_secret(public_key_hex)), modes.CBC(iv))
|
||||
|
||||
encryptor = cipher.encryptor()
|
||||
encrypted_message = encryptor.update(padded_data) + encryptor.finalize()
|
||||
|
||||
return f"{base64.b64encode(encrypted_message).decode()}?iv={base64.b64encode(iv).decode()}"
|
||||
|
||||
def decrypt_message(self, encoded_message: str, public_key_hex: str) -> str:
|
||||
encoded_data = encoded_message.split('?iv=')
|
||||
encoded_content, encoded_iv = encoded_data[0], encoded_data[1]
|
||||
|
||||
iv = base64.b64decode(encoded_iv)
|
||||
cipher = Cipher(algorithms.AES(self.compute_shared_secret(public_key_hex)), modes.CBC(iv))
|
||||
encrypted_content = base64.b64decode(encoded_content)
|
||||
|
||||
decryptor = cipher.decryptor()
|
||||
decrypted_message = decryptor.update(encrypted_content) + decryptor.finalize()
|
||||
|
||||
unpadder = padding.PKCS7(128).unpadder()
|
||||
unpadded_data = unpadder.update(decrypted_message) + unpadder.finalize()
|
||||
|
||||
return unpadded_data.decode()
|
||||
|
||||
def sign_message_hash(self, hash: bytes) -> str:
|
||||
sk = secp256k1.PrivateKey(self.raw_secret)
|
||||
sig = sk.schnorr_sign(hash, None, raw=True)
|
||||
return sig.hex()
|
||||
|
||||
ffi = FFI()
|
||||
@ffi.callback("int (unsigned char *, const unsigned char *, const unsigned char *, void *)")
|
||||
def copy_x(output, x32, y32, data):
|
||||
ffi.memmove(output, x32, 32)
|
||||
return 1
|
||||
@@ -1,78 +0,0 @@
|
||||
import json
|
||||
from queue import Queue
|
||||
from threading import Lock
|
||||
from .message_type import RelayMessageType
|
||||
from .event import Event
|
||||
|
||||
|
||||
class EventMessage:
|
||||
def __init__(self, event: Event, subscription_id: str, url: str) -> None:
|
||||
self.event = event
|
||||
self.subscription_id = subscription_id
|
||||
self.url = url
|
||||
|
||||
|
||||
class NoticeMessage:
|
||||
def __init__(self, content: str, url: str) -> None:
|
||||
self.content = content
|
||||
self.url = url
|
||||
|
||||
|
||||
class EndOfStoredEventsMessage:
|
||||
def __init__(self, subscription_id: str, url: str) -> None:
|
||||
self.subscription_id = subscription_id
|
||||
self.url = url
|
||||
|
||||
|
||||
class MessagePool:
|
||||
def __init__(self) -> None:
|
||||
self.events: Queue[EventMessage] = Queue()
|
||||
self.notices: Queue[NoticeMessage] = Queue()
|
||||
self.eose_notices: Queue[EndOfStoredEventsMessage] = Queue()
|
||||
self._unique_events: set = set()
|
||||
self.lock: Lock = Lock()
|
||||
|
||||
def add_message(self, message: str, url: str):
|
||||
self._process_message(message, url)
|
||||
|
||||
def get_event(self):
|
||||
return self.events.get()
|
||||
|
||||
def get_notice(self):
|
||||
return self.notices.get()
|
||||
|
||||
def get_eose_notice(self):
|
||||
return self.eose_notices.get()
|
||||
|
||||
def has_events(self):
|
||||
return self.events.qsize() > 0
|
||||
|
||||
def has_notices(self):
|
||||
return self.notices.qsize() > 0
|
||||
|
||||
def has_eose_notices(self):
|
||||
return self.eose_notices.qsize() > 0
|
||||
|
||||
def _process_message(self, message: str, url: str):
|
||||
message_json = json.loads(message)
|
||||
message_type = message_json[0]
|
||||
if message_type == RelayMessageType.EVENT:
|
||||
subscription_id = message_json[1]
|
||||
e = message_json[2]
|
||||
event = Event(
|
||||
e["pubkey"],
|
||||
e["content"],
|
||||
e["created_at"],
|
||||
e["kind"],
|
||||
e["tags"],
|
||||
e["id"],
|
||||
e["sig"],
|
||||
)
|
||||
with self.lock:
|
||||
if not event.id in self._unique_events:
|
||||
self.events.put(EventMessage(event, subscription_id, url))
|
||||
self._unique_events.add(event.id)
|
||||
elif message_type == RelayMessageType.NOTICE:
|
||||
self.notices.put(NoticeMessage(message_json[1], url))
|
||||
elif message_type == RelayMessageType.END_OF_STORED_EVENTS:
|
||||
self.eose_notices.put(EndOfStoredEventsMessage(message_json[1], url))
|
||||
@@ -1,15 +0,0 @@
|
||||
class ClientMessageType:
|
||||
EVENT = "EVENT"
|
||||
REQUEST = "REQ"
|
||||
CLOSE = "CLOSE"
|
||||
|
||||
class RelayMessageType:
|
||||
EVENT = "EVENT"
|
||||
NOTICE = "NOTICE"
|
||||
END_OF_STORED_EVENTS = "EOSE"
|
||||
|
||||
@staticmethod
|
||||
def is_valid(type: str) -> bool:
|
||||
if type == RelayMessageType.EVENT or type == RelayMessageType.NOTICE or type == RelayMessageType.END_OF_STORED_EVENTS:
|
||||
return True
|
||||
return False
|
||||
@@ -1,123 +0,0 @@
|
||||
import json
|
||||
from threading import Lock
|
||||
from websocket import WebSocketApp
|
||||
from .event import Event
|
||||
from .filter import Filters
|
||||
from .message_pool import MessagePool
|
||||
from .message_type import RelayMessageType
|
||||
from .subscription import Subscription
|
||||
|
||||
|
||||
class RelayPolicy:
|
||||
def __init__(self, should_read: bool = True, should_write: bool = True) -> None:
|
||||
self.should_read = should_read
|
||||
self.should_write = should_write
|
||||
|
||||
def to_json_object(self) -> dict[str, bool]:
|
||||
return {"read": self.should_read, "write": self.should_write}
|
||||
|
||||
|
||||
class Relay:
|
||||
def __init__(
|
||||
self,
|
||||
url: str,
|
||||
policy: RelayPolicy,
|
||||
message_pool: MessagePool,
|
||||
subscriptions: dict[str, Subscription] = {},
|
||||
) -> None:
|
||||
self.url = url
|
||||
self.policy = policy
|
||||
self.message_pool = message_pool
|
||||
self.subscriptions = subscriptions
|
||||
self.lock = Lock()
|
||||
self.ws = WebSocketApp(
|
||||
url,
|
||||
on_open=self._on_open,
|
||||
on_message=self._on_message,
|
||||
on_error=self._on_error,
|
||||
on_close=self._on_close,
|
||||
)
|
||||
|
||||
def connect(self, ssl_options: dict = None):
|
||||
self.ws.run_forever(sslopt=ssl_options)
|
||||
|
||||
def close(self):
|
||||
self.ws.close()
|
||||
|
||||
def publish(self, message: str):
|
||||
self.ws.send(message)
|
||||
|
||||
def add_subscription(self, id, filters: Filters):
|
||||
with self.lock:
|
||||
self.subscriptions[id] = Subscription(id, filters)
|
||||
|
||||
def close_subscription(self, id: str) -> None:
|
||||
with self.lock:
|
||||
self.subscriptions.pop(id)
|
||||
|
||||
def update_subscription(self, id: str, filters: Filters) -> None:
|
||||
with self.lock:
|
||||
subscription = self.subscriptions[id]
|
||||
subscription.filters = filters
|
||||
|
||||
def to_json_object(self) -> dict:
|
||||
return {
|
||||
"url": self.url,
|
||||
"policy": self.policy.to_json_object(),
|
||||
"subscriptions": [
|
||||
subscription.to_json_object()
|
||||
for subscription in self.subscriptions.values()
|
||||
],
|
||||
}
|
||||
|
||||
def _on_open(self, class_obj):
|
||||
pass
|
||||
|
||||
def _on_close(self, class_obj, status_code, message):
|
||||
pass
|
||||
|
||||
def _on_message(self, class_obj, message: str):
|
||||
if self._is_valid_message(message):
|
||||
self.message_pool.add_message(message, self.url)
|
||||
|
||||
def _on_error(self, class_obj, error):
|
||||
pass
|
||||
|
||||
def _is_valid_message(self, message: str) -> bool:
|
||||
message = message.strip("\n")
|
||||
if not message or message[0] != "[" or message[-1] != "]":
|
||||
return False
|
||||
|
||||
message_json = json.loads(message)
|
||||
message_type = message_json[0]
|
||||
if not RelayMessageType.is_valid(message_type):
|
||||
return False
|
||||
if message_type == RelayMessageType.EVENT:
|
||||
if not len(message_json) == 3:
|
||||
return False
|
||||
|
||||
subscription_id = message_json[1]
|
||||
with self.lock:
|
||||
if subscription_id not in self.subscriptions:
|
||||
return False
|
||||
|
||||
e = message_json[2]
|
||||
event = Event(
|
||||
e["pubkey"],
|
||||
e["content"],
|
||||
e["created_at"],
|
||||
e["kind"],
|
||||
e["tags"],
|
||||
e["id"],
|
||||
e["sig"],
|
||||
)
|
||||
if not event.verify():
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
subscription = self.subscriptions[subscription_id]
|
||||
|
||||
if not subscription.filters.match(event):
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -1,43 +0,0 @@
|
||||
import threading
|
||||
from .filter import Filters
|
||||
from .message_pool import MessagePool
|
||||
from .relay import Relay, RelayPolicy
|
||||
|
||||
class RelayManager:
|
||||
def __init__(self) -> None:
|
||||
self.relays: dict[str, Relay] = {}
|
||||
self.message_pool = MessagePool()
|
||||
|
||||
def add_relay(self, url: str, read: bool=True, write: bool=True, subscriptions={}):
|
||||
policy = RelayPolicy(read, write)
|
||||
relay = Relay(url, policy, self.message_pool, subscriptions)
|
||||
self.relays[url] = relay
|
||||
|
||||
def remove_relay(self, url: str):
|
||||
self.relays.pop(url)
|
||||
|
||||
def add_subscription(self, id: str, filters: Filters):
|
||||
for relay in self.relays.values():
|
||||
relay.add_subscription(id, filters)
|
||||
|
||||
def close_subscription(self, id: str):
|
||||
for relay in self.relays.values():
|
||||
relay.close_subscription(id)
|
||||
|
||||
def open_connections(self, ssl_options: dict=None):
|
||||
for relay in self.relays.values():
|
||||
threading.Thread(
|
||||
target=relay.connect,
|
||||
args=(ssl_options,),
|
||||
name=f"{relay.url}-thread"
|
||||
).start()
|
||||
|
||||
def close_connections(self):
|
||||
for relay in self.relays.values():
|
||||
relay.close()
|
||||
|
||||
def publish_message(self, message: str):
|
||||
for relay in self.relays.values():
|
||||
if relay.policy.should_write:
|
||||
relay.publish(message)
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
from .filter import Filters
|
||||
|
||||
class Subscription:
|
||||
def __init__(self, id: str, filters: Filters=None) -> None:
|
||||
self.id = id
|
||||
self.filters = filters
|
||||
|
||||
def to_json_object(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"filters": self.filters.to_json_array()
|
||||
}
|
||||
124
nostr/relay.py
124
nostr/relay.py
@@ -1,124 +0,0 @@
|
||||
import json
|
||||
from threading import Lock
|
||||
from websocket import WebSocketApp
|
||||
from .event import Event
|
||||
from .filter import Filters
|
||||
from .message_pool import MessagePool
|
||||
from .message_type import RelayMessageType
|
||||
from .subscription import Subscription
|
||||
|
||||
|
||||
class RelayPolicy:
|
||||
def __init__(self, should_read: bool = True, should_write: bool = True) -> None:
|
||||
self.should_read = should_read
|
||||
self.should_write = should_write
|
||||
|
||||
def to_json_object(self) -> dict[str, bool]:
|
||||
return {"read": self.should_read, "write": self.should_write}
|
||||
|
||||
|
||||
class Relay:
|
||||
def __init__(
|
||||
self,
|
||||
url: str,
|
||||
policy: RelayPolicy,
|
||||
message_pool: MessagePool,
|
||||
subscriptions: dict[str, Subscription] = {},
|
||||
) -> None:
|
||||
self.url = url
|
||||
self.policy = policy
|
||||
self.message_pool = message_pool
|
||||
self.subscriptions = subscriptions
|
||||
self.lock = Lock()
|
||||
self.ws = WebSocketApp(
|
||||
url,
|
||||
on_open=self._on_open,
|
||||
on_message=self._on_message,
|
||||
on_error=self._on_error,
|
||||
on_close=self._on_close,
|
||||
)
|
||||
|
||||
def connect(self, ssl_options: dict = None):
|
||||
self.ws.run_forever(sslopt=ssl_options)
|
||||
|
||||
def close(self):
|
||||
self.ws.close()
|
||||
|
||||
def publish(self, message: str):
|
||||
self.ws.send(message)
|
||||
|
||||
def add_subscription(self, id, filters: Filters):
|
||||
with self.lock:
|
||||
self.subscriptions[id] = Subscription(id, filters)
|
||||
|
||||
def close_subscription(self, id: str) -> None:
|
||||
with self.lock:
|
||||
self.subscriptions.pop(id)
|
||||
|
||||
def update_subscription(self, id: str, filters: Filters) -> None:
|
||||
with self.lock:
|
||||
subscription = self.subscriptions[id]
|
||||
subscription.filters = filters
|
||||
|
||||
def to_json_object(self) -> dict:
|
||||
return {
|
||||
"url": self.url,
|
||||
"policy": self.policy.to_json_object(),
|
||||
"subscriptions": [
|
||||
subscription.to_json_object()
|
||||
for subscription in self.subscriptions.values()
|
||||
],
|
||||
}
|
||||
|
||||
def _on_open(self, class_obj):
|
||||
pass
|
||||
|
||||
def _on_close(self, class_obj, status_code, message):
|
||||
pass
|
||||
|
||||
def _on_message(self, class_obj, message: str):
|
||||
if self._is_valid_message(message):
|
||||
self.message_pool.add_message(message, self.url)
|
||||
|
||||
def _on_error(self, class_obj, error):
|
||||
# print(error)
|
||||
pass
|
||||
|
||||
def _is_valid_message(self, message: str) -> bool:
|
||||
message = message.strip("\n")
|
||||
if not message or message[0] != "[" or message[-1] != "]":
|
||||
return False
|
||||
|
||||
message_json = json.loads(message)
|
||||
message_type = message_json[0]
|
||||
if not RelayMessageType.is_valid(message_type):
|
||||
return False
|
||||
if message_type == RelayMessageType.EVENT:
|
||||
if not len(message_json) == 3:
|
||||
return False
|
||||
|
||||
subscription_id = message_json[1]
|
||||
with self.lock:
|
||||
if subscription_id not in self.subscriptions:
|
||||
return False
|
||||
|
||||
e = message_json[2]
|
||||
event = Event(
|
||||
e["pubkey"],
|
||||
e["content"],
|
||||
e["created_at"],
|
||||
e["kind"],
|
||||
e["tags"],
|
||||
e["id"],
|
||||
e["sig"],
|
||||
)
|
||||
if not event.verify():
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
subscription = self.subscriptions[subscription_id]
|
||||
|
||||
if not subscription.filters.match(event):
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -1,43 +0,0 @@
|
||||
import threading
|
||||
from .filter import Filters
|
||||
from .message_pool import MessagePool
|
||||
from .relay import Relay, RelayPolicy
|
||||
|
||||
class RelayManager:
|
||||
def __init__(self) -> None:
|
||||
self.relays: dict[str, Relay] = {}
|
||||
self.message_pool = MessagePool()
|
||||
|
||||
def add_relay(self, url: str, read: bool=True, write: bool=True, subscriptions={}):
|
||||
policy = RelayPolicy(read, write)
|
||||
relay = Relay(url, policy, self.message_pool, subscriptions)
|
||||
self.relays[url] = relay
|
||||
|
||||
def remove_relay(self, url: str):
|
||||
self.relays.pop(url)
|
||||
|
||||
def add_subscription(self, id: str, filters: Filters):
|
||||
for relay in self.relays.values():
|
||||
relay.add_subscription(id, filters)
|
||||
|
||||
def close_subscription(self, id: str):
|
||||
for relay in self.relays.values():
|
||||
relay.close_subscription(id)
|
||||
|
||||
def open_connections(self, ssl_options: dict=None):
|
||||
for relay in self.relays.values():
|
||||
threading.Thread(
|
||||
target=relay.connect,
|
||||
args=(ssl_options,),
|
||||
name=f"{relay.url}-thread"
|
||||
).start()
|
||||
|
||||
def close_connections(self):
|
||||
for relay in self.relays.values():
|
||||
relay.close()
|
||||
|
||||
def publish_message(self, message: str):
|
||||
for relay in self.relays.values():
|
||||
if relay.policy.should_write:
|
||||
relay.publish(message)
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
from .filter import Filters
|
||||
|
||||
class Subscription:
|
||||
def __init__(self, id: str, filters: Filters=None) -> None:
|
||||
self.id = id
|
||||
self.filters = filters
|
||||
|
||||
def to_json_object(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"filters": self.filters.to_json_array()
|
||||
}
|
||||
297
poetry.lock
generated
297
poetry.lock
generated
@@ -18,17 +18,19 @@ trio = ["trio (>=0.16,<0.22)"]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "22.1.0"
|
||||
version = "22.2.0"
|
||||
description = "Classes Without Boilerplate"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
||||
docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
||||
tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
|
||||
tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
|
||||
cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"]
|
||||
dev = ["attrs[docs,tests]"]
|
||||
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"]
|
||||
tests = ["attrs[tests-no-zope]", "zope.interface"]
|
||||
tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=0.971,<0.990)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||
tests_no_zope = ["cloudpickle", "hypothesis", "mypy (>=0.971,<0.990)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||
|
||||
[[package]]
|
||||
name = "bech32"
|
||||
@@ -48,7 +50,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "22.10.0"
|
||||
version = "22.12.0"
|
||||
description = "The uncompromising code formatter."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -71,7 +73,7 @@ uvloop = ["uvloop (>=0.15.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2022.9.24"
|
||||
version = "2022.12.7"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -113,15 +115,15 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.5"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "6.5.0"
|
||||
version = "7.0.1"
|
||||
description = "Code coverage measurement for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -185,6 +187,17 @@ django = ["dj-database-url", "dj-email-url", "django-cache-url"]
|
||||
lint = ["flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)"]
|
||||
tests = ["dj-database-url", "dj-email-url", "django-cache-url", "pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.1.0"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.83.0"
|
||||
@@ -221,7 +234,7 @@ python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "5.0.0"
|
||||
version = "5.2.0"
|
||||
description = "Read metadata from Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -232,7 +245,7 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
|
||||
zipp = ">=0.5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
|
||||
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
perf = ["ipython"]
|
||||
testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
|
||||
|
||||
@@ -246,17 +259,17 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "isort"
|
||||
version = "5.10.1"
|
||||
version = "5.11.4"
|
||||
description = "A Python utility / library to sort Python imports."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6.1,<4.0"
|
||||
python-versions = ">=3.7.0"
|
||||
|
||||
[package.extras]
|
||||
colors = ["colorama (>=0.4.3,<0.5.0)"]
|
||||
pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
|
||||
pipfile-deprecated-finder = ["pipreqs", "requirementslib"]
|
||||
plugins = ["setuptools"]
|
||||
requirements_deprecated_finder = ["pip-api", "pipreqs"]
|
||||
requirements-deprecated-finder = ["pip-api", "pipreqs"]
|
||||
|
||||
[[package]]
|
||||
name = "loguru"
|
||||
@@ -275,7 +288,7 @@ dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils
|
||||
|
||||
[[package]]
|
||||
name = "marshmallow"
|
||||
version = "3.18.0"
|
||||
version = "3.19.0"
|
||||
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -285,9 +298,9 @@ python-versions = ">=3.7"
|
||||
packaging = ">=17.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["flake8 (==5.0.4)", "flake8-bugbear (==22.9.11)", "mypy (==0.971)", "pre-commit (>=2.4,<3.0)", "pytest", "pytz", "simplejson", "tox"]
|
||||
docs = ["alabaster (==0.7.12)", "autodocsumm (==0.2.9)", "sphinx (==5.1.1)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"]
|
||||
lint = ["flake8 (==5.0.4)", "flake8-bugbear (==22.9.11)", "mypy (==0.971)", "pre-commit (>=2.4,<3.0)"]
|
||||
dev = ["flake8 (==5.0.4)", "flake8-bugbear (==22.10.25)", "mypy (==0.990)", "pre-commit (>=2.4,<3.0)", "pytest", "pytz", "simplejson", "tox"]
|
||||
docs = ["alabaster (==0.7.12)", "autodocsumm (==0.2.9)", "sphinx (==5.3.0)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"]
|
||||
lint = ["flake8 (==5.0.4)", "flake8-bugbear (==22.10.25)", "mypy (==0.990)", "pre-commit (>=2.4,<3.0)"]
|
||||
tests = ["pytest", "pytz", "simplejson"]
|
||||
|
||||
[[package]]
|
||||
@@ -330,18 +343,15 @@ attrs = ">=19.2.0"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "21.3"
|
||||
version = "22.0"
|
||||
description = "Core utilities for Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.10.1"
|
||||
version = "0.10.3"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -349,15 +359,15 @@ python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "2.5.2"
|
||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
version = "2.6.0"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
|
||||
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
|
||||
docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"]
|
||||
test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
@@ -374,14 +384,6 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "py"
|
||||
version = "1.11.0"
|
||||
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.21"
|
||||
@@ -413,17 +415,6 @@ typing-extensions = ">=4.1.0"
|
||||
dotenv = ["python-dotenv (>=0.10.4)"]
|
||||
email = ["email-validator (>=1.0.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.0.9"
|
||||
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6.8"
|
||||
|
||||
[package.extras]
|
||||
diagrams = ["jinja2", "railroad-diagrams"]
|
||||
|
||||
[[package]]
|
||||
name = "PySocks"
|
||||
version = "1.7.1"
|
||||
@@ -434,7 +425,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.1.3"
|
||||
version = "7.2.0"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -443,12 +434,12 @@ python-versions = ">=3.7"
|
||||
[package.dependencies]
|
||||
attrs = ">=19.2.0"
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
py = ">=1.8.2"
|
||||
tomli = ">=1.0.0"
|
||||
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||
@@ -640,11 +631,11 @@ python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.12"
|
||||
version = "1.26.13"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
|
||||
@@ -693,7 +684,7 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.9.0"
|
||||
version = "3.11.0"
|
||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -714,8 +705,8 @@ anyio = [
|
||||
{file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"},
|
||||
]
|
||||
attrs = [
|
||||
{file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
|
||||
{file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
|
||||
{file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"},
|
||||
{file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"},
|
||||
]
|
||||
bech32 = [
|
||||
{file = "bech32-1.2.0-py3-none-any.whl", hash = "sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981"},
|
||||
@@ -727,31 +718,22 @@ bitstring = [
|
||||
{file = "bitstring-3.1.9.tar.gz", hash = "sha256:a5848a3f63111785224dca8bb4c0a75b62ecdef56a042c8d6be74b16f7e860e7"},
|
||||
]
|
||||
black = [
|
||||
{file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"},
|
||||
{file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"},
|
||||
{file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"},
|
||||
{file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"},
|
||||
{file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"},
|
||||
{file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"},
|
||||
{file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"},
|
||||
{file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"},
|
||||
{file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"},
|
||||
{file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"},
|
||||
{file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"},
|
||||
{file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"},
|
||||
{file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"},
|
||||
{file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"},
|
||||
{file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"},
|
||||
{file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"},
|
||||
{file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"},
|
||||
{file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"},
|
||||
{file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"},
|
||||
{file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"},
|
||||
{file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"},
|
||||
{file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"},
|
||||
{file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"},
|
||||
{file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"},
|
||||
{file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"},
|
||||
{file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"},
|
||||
{file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"},
|
||||
{file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"},
|
||||
{file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"},
|
||||
{file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"},
|
||||
{file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"},
|
||||
{file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"},
|
||||
{file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"},
|
||||
]
|
||||
certifi = [
|
||||
{file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
|
||||
{file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
|
||||
{file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"},
|
||||
{file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
|
||||
]
|
||||
cffi = [
|
||||
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
|
||||
@@ -828,60 +810,61 @@ click = [
|
||||
{file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
|
||||
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
coverage = [
|
||||
{file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"},
|
||||
{file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"},
|
||||
{file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"},
|
||||
{file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"},
|
||||
{file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"},
|
||||
{file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"},
|
||||
{file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"},
|
||||
{file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"},
|
||||
{file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"},
|
||||
{file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"},
|
||||
{file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"},
|
||||
{file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"},
|
||||
{file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"},
|
||||
{file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"},
|
||||
{file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"},
|
||||
{file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"},
|
||||
{file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"},
|
||||
{file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"},
|
||||
{file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"},
|
||||
{file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"},
|
||||
{file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"},
|
||||
{file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"},
|
||||
{file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"},
|
||||
{file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"},
|
||||
{file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"},
|
||||
{file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"},
|
||||
{file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"},
|
||||
{file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"},
|
||||
{file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"},
|
||||
{file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"},
|
||||
{file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"},
|
||||
{file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"},
|
||||
{file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"},
|
||||
{file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"},
|
||||
{file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"},
|
||||
{file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"},
|
||||
{file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"},
|
||||
{file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"},
|
||||
{file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"},
|
||||
{file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"},
|
||||
{file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"},
|
||||
{file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"},
|
||||
{file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"},
|
||||
{file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"},
|
||||
{file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"},
|
||||
{file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"},
|
||||
{file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"},
|
||||
{file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"},
|
||||
{file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"},
|
||||
{file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b3695c4f4750bca943b3e1f74ad4be8d29e4aeab927d50772c41359107bd5d5c"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa6a5a224b7f4cfb226f4fc55a57e8537fcc096f42219128c2c74c0e7d0953e1"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74f70cd92669394eaf8d7756d1b195c8032cf7bbbdfce3bc489d4e15b3b8cf73"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b66bb21a23680dee0be66557dc6b02a3152ddb55edf9f6723fa4a93368f7158d"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d87717959d4d0ee9db08a0f1d80d21eb585aafe30f9b0a54ecf779a69cb015f6"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:854f22fa361d1ff914c7efa347398374cc7d567bdafa48ac3aa22334650dfba2"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1e414dc32ee5c3f36544ea466b6f52f28a7af788653744b8570d0bf12ff34bc0"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6c5ad996c6fa4d8ed669cfa1e8551348729d008a2caf81489ab9ea67cfbc7498"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-win32.whl", hash = "sha256:691571f31ace1837838b7e421d3a09a8c00b4aac32efacb4fc9bd0a5c647d25a"},
|
||||
{file = "coverage-7.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:89caf4425fe88889e2973a8e9a3f6f5f9bbe5dd411d7d521e86428c08a873a4a"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:63d56165a7c76265468d7e0c5548215a5ba515fc2cba5232d17df97bffa10f6c"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f943a3b2bc520102dd3e0bb465e1286e12c9a54f58accd71b9e65324d9c7c01"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:830525361249dc4cd013652b0efad645a385707a5ae49350c894b67d23fbb07c"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd1b9c5adc066db699ccf7fa839189a649afcdd9e02cb5dc9d24e67e7922737d"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00c14720b8b3b6c23b487e70bd406abafc976ddc50490f645166f111c419c39"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6d55d840e1b8c0002fce66443e124e8581f30f9ead2e54fbf6709fb593181f2c"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:66b18c3cf8bbab0cce0d7b9e4262dc830e93588986865a8c78ab2ae324b3ed56"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:12a5aa77783d49e05439fbe6e6b427484f8a0f9f456b46a51d8aac022cfd024d"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-win32.whl", hash = "sha256:b77015d1cb8fe941be1222a5a8b4e3fbca88180cfa7e2d4a4e58aeabadef0ab7"},
|
||||
{file = "coverage-7.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb992c47cb1e5bd6a01e97182400bcc2ba2077080a17fcd7be23aaa6e572e390"},
|
||||
{file = "coverage-7.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e78e9dcbf4f3853d3ae18a8f9272111242531535ec9e1009fa8ec4a2b74557dc"},
|
||||
{file = "coverage-7.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e60bef2e2416f15fdc05772bf87db06c6a6f9870d1db08fdd019fbec98ae24a9"},
|
||||
{file = "coverage-7.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9823e4789ab70f3ec88724bba1a203f2856331986cd893dedbe3e23a6cfc1e4e"},
|
||||
{file = "coverage-7.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9158f8fb06747ac17bd237930c4372336edc85b6e13bdc778e60f9d685c3ca37"},
|
||||
{file = "coverage-7.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:486ee81fa694b4b796fc5617e376326a088f7b9729c74d9defa211813f3861e4"},
|
||||
{file = "coverage-7.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1285648428a6101b5f41a18991c84f1c3959cee359e51b8375c5882fc364a13f"},
|
||||
{file = "coverage-7.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2c44fcfb3781b41409d0f060a4ed748537557de9362a8a9282182fafb7a76ab4"},
|
||||
{file = "coverage-7.0.1-cp37-cp37m-win32.whl", hash = "sha256:d6814854c02cbcd9c873c0f3286a02e3ac1250625cca822ca6bc1018c5b19f1c"},
|
||||
{file = "coverage-7.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f66460f17c9319ea4f91c165d46840314f0a7c004720b20be58594d162a441d8"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b373c9345c584bb4b5f5b8840df7f4ab48c4cbb7934b58d52c57020d911b856"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d3022c3007d3267a880b5adcf18c2a9bf1fc64469b394a804886b401959b8742"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92651580bd46519067e36493acb394ea0607b55b45bd81dd4e26379ed1871f55"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cfc595d2af13856505631be072835c59f1acf30028d1c860b435c5fc9c15b69"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b4b3a4d9915b2be879aff6299c0a6129f3d08a775d5a061f503cf79571f73e4"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b6f22bb64cc39bcb883e5910f99a27b200fdc14cdd79df8696fa96b0005c9444"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72d1507f152abacea81f65fee38e4ef3ac3c02ff8bc16f21d935fd3a8a4ad910"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0a79137fc99815fff6a852c233628e735ec15903cfd16da0f229d9c4d45926ab"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-win32.whl", hash = "sha256:b3763e7fcade2ff6c8e62340af9277f54336920489ceb6a8cd6cc96da52fcc62"},
|
||||
{file = "coverage-7.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:09f6b5a8415b6b3e136d5fec62b552972187265cb705097bf030eb9d4ffb9b60"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:978258fec36c154b5e250d356c59af7d4c3ba02bef4b99cda90b6029441d797d"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:19ec666533f0f70a0993f88b8273057b96c07b9d26457b41863ccd021a043b9a"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfded268092a84605f1cc19e5c737f9ce630a8900a3589e9289622db161967e9"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07bcfb1d8ac94af886b54e18a88b393f6a73d5959bb31e46644a02453c36e475"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b4a923cc7566bbc7ae2dfd0ba5a039b61d19c740f1373791f2ebd11caea59"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:aec2d1515d9d39ff270059fd3afbb3b44e6ec5758af73caf18991807138c7118"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c20cfebcc149a4c212f6491a5f9ff56f41829cd4f607b5be71bb2d530ef243b1"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fd556ff16a57a070ce4f31c635953cc44e25244f91a0378c6e9bdfd40fdb249f"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-win32.whl", hash = "sha256:b9ea158775c7c2d3e54530a92da79496fb3fb577c876eec761c23e028f1e216c"},
|
||||
{file = "coverage-7.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:d1991f1dd95eba69d2cd7708ff6c2bbd2426160ffc73c2b81f617a053ebcb1a8"},
|
||||
{file = "coverage-7.0.1-pp37.pp38.pp39-none-any.whl", hash = "sha256:3dd4ee135e08037f458425b8842d24a95a0961831a33f89685ff86b77d378f89"},
|
||||
{file = "coverage-7.0.1.tar.gz", hash = "sha256:a4a574a19eeb67575a5328a5760bbbb737faa685616586a9f9da4281f940109c"},
|
||||
]
|
||||
cryptography = [
|
||||
{file = "cryptography-38.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70"},
|
||||
@@ -919,6 +902,10 @@ environs = [
|
||||
{file = "environs-9.5.0-py2.py3-none-any.whl", hash = "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124"},
|
||||
{file = "environs-9.5.0.tar.gz", hash = "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9"},
|
||||
]
|
||||
exceptiongroup = [
|
||||
{file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"},
|
||||
{file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"},
|
||||
]
|
||||
fastapi = [
|
||||
{file = "fastapi-0.83.0-py3-none-any.whl", hash = "sha256:694a2b6c2607a61029a4be1c6613f84d74019cb9f7a41c7a475dca8e715f9368"},
|
||||
{file = "fastapi-0.83.0.tar.gz", hash = "sha256:96eb692350fe13d7a9843c3c87a874f0d45102975257dd224903efd6c0fde3bd"},
|
||||
@@ -932,24 +919,24 @@ idna = [
|
||||
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
||||
]
|
||||
importlib-metadata = [
|
||||
{file = "importlib_metadata-5.0.0-py3-none-any.whl", hash = "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"},
|
||||
{file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"},
|
||||
{file = "importlib_metadata-5.2.0-py3-none-any.whl", hash = "sha256:0eafa39ba42bf225fc00e67f701d71f85aead9f878569caf13c3724f704b970f"},
|
||||
{file = "importlib_metadata-5.2.0.tar.gz", hash = "sha256:404d48d62bba0b7a77ff9d405efd91501bef2e67ff4ace0bed40a0cf28c3c7cd"},
|
||||
]
|
||||
iniconfig = [
|
||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||
]
|
||||
isort = [
|
||||
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
|
||||
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
|
||||
{file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"},
|
||||
{file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"},
|
||||
]
|
||||
loguru = [
|
||||
{file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"},
|
||||
{file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"},
|
||||
]
|
||||
marshmallow = [
|
||||
{file = "marshmallow-3.18.0-py3-none-any.whl", hash = "sha256:35e02a3a06899c9119b785c12a22f4cda361745d66a71ab691fd7610202ae104"},
|
||||
{file = "marshmallow-3.18.0.tar.gz", hash = "sha256:6804c16114f7fce1f5b4dadc31f4674af23317fcc7f075da21e35c1a35d781f7"},
|
||||
{file = "marshmallow-3.19.0-py3-none-any.whl", hash = "sha256:93f0958568da045b0021ec6aeb7ac37c81bfcccbb9a0e7ed8559885070b3a19b"},
|
||||
{file = "marshmallow-3.19.0.tar.gz", hash = "sha256:90032c0fd650ce94b6ec6dc8dfeb0e3ff50c144586462c389b81a07205bedb78"},
|
||||
]
|
||||
mypy = [
|
||||
{file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"},
|
||||
@@ -985,25 +972,21 @@ outcome = [
|
||||
{file = "outcome-1.2.0.tar.gz", hash = "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672"},
|
||||
]
|
||||
packaging = [
|
||||
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
|
||||
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
|
||||
{file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"},
|
||||
{file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"},
|
||||
]
|
||||
pathspec = [
|
||||
{file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"},
|
||||
{file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"},
|
||||
{file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"},
|
||||
{file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"},
|
||||
]
|
||||
platformdirs = [
|
||||
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
|
||||
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
|
||||
{file = "platformdirs-2.6.0-py3-none-any.whl", hash = "sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca"},
|
||||
{file = "platformdirs-2.6.0.tar.gz", hash = "sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e"},
|
||||
]
|
||||
pluggy = [
|
||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||
]
|
||||
py = [
|
||||
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
||||
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
||||
]
|
||||
pycparser = [
|
||||
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
|
||||
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
|
||||
@@ -1074,18 +1057,14 @@ pydantic = [
|
||||
{file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"},
|
||||
{file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"},
|
||||
]
|
||||
pyparsing = [
|
||||
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
|
||||
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
|
||||
]
|
||||
PySocks = [
|
||||
{file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"},
|
||||
{file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"},
|
||||
{file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"},
|
||||
]
|
||||
pytest = [
|
||||
{file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"},
|
||||
{file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"},
|
||||
{file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
|
||||
{file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
|
||||
]
|
||||
pytest-asyncio = [
|
||||
{file = "pytest-asyncio-0.19.0.tar.gz", hash = "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed"},
|
||||
@@ -1223,8 +1202,8 @@ typing-extensions = [
|
||||
{file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
|
||||
{file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
|
||||
{file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"},
|
||||
{file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"},
|
||||
]
|
||||
uvicorn = [
|
||||
{file = "uvicorn-0.18.3-py3-none-any.whl", hash = "sha256:0abd429ebb41e604ed8d2be6c60530de3408f250e8d2d84967d85ba9e86fe3af"},
|
||||
@@ -1239,6 +1218,6 @@ win32-setctime = [
|
||||
{file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"},
|
||||
]
|
||||
zipp = [
|
||||
{file = "zipp-3.9.0-py3-none-any.whl", hash = "sha256:972cfa31bc2fedd3fa838a51e9bc7e64b7fb725a8c00e7431554311f180e9980"},
|
||||
{file = "zipp-3.9.0.tar.gz", hash = "sha256:3a7af91c3db40ec72dd9d154ae18e008c69efe8ca88dde4f9a731bb82fe2f9eb"},
|
||||
{file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"},
|
||||
{file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user