diff --git a/.env.example b/.env.example index 5ae391c..0de8074 100644 --- a/.env.example +++ b/.env.example @@ -29,4 +29,9 @@ LIGHTNING_FEE_PERCENT=1.0 LIGHTNING_RESERVE_FEE_MIN=4000 LNBITS_ENDPOINT=https://legend.lnbits.com -LNBITS_KEY=yourkeyasdasdasd \ No newline at end of file +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 \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..eaa87cc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "cashu/nostr"] + path = cashu/nostr + url = https://github.com/callebtc/python-nostr/ diff --git a/cashu/core/settings.py b/cashu/core/settings.py index effd384..b35077a 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -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" diff --git a/cashu/nostr b/cashu/nostr new file mode 160000 index 0000000..dfca8bf --- /dev/null +++ b/cashu/nostr @@ -0,0 +1 @@ +Subproject commit dfca8bfdc59732fca4c04d3ce762dedb00b2598f diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index d454181..06a0863 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -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() diff --git a/nostr/__init__.py b/nostr/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nostr/bech32.py b/nostr/bech32.py deleted file mode 100644 index b068de7..0000000 --- a/nostr/bech32.py +++ /dev/null @@ -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 diff --git a/nostr/client/__init__.py b/nostr/client/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nostr/client/aes.py b/nostr/client/aes.py deleted file mode 100644 index 9d20224..0000000 --- a/nostr/client/aes.py +++ /dev/null @@ -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) diff --git a/nostr/client/cbc.py b/nostr/client/cbc.py deleted file mode 100644 index a41dbc0..0000000 --- a/nostr/client/cbc.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/nostr/client/client.py b/nostr/client/client.py deleted file mode 100644 index 74aed9d..0000000 --- a/nostr/client/client.py +++ /dev/null @@ -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) - diff --git a/nostr/event.py b/nostr/event.py deleted file mode 100644 index 450893e..0000000 --- a/nostr/event.py +++ /dev/null @@ -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 - } diff --git a/nostr/filter.py b/nostr/filter.py deleted file mode 100644 index 32b94a3..0000000 --- a/nostr/filter.py +++ /dev/null @@ -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] diff --git a/nostr/key.py b/nostr/key.py deleted file mode 100644 index 9449c00..0000000 --- a/nostr/key.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/nostr/message_pool.py b/nostr/message_pool.py deleted file mode 100644 index 472e31e..0000000 --- a/nostr/message_pool.py +++ /dev/null @@ -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)) diff --git a/nostr/message_type.py b/nostr/message_type.py deleted file mode 100644 index 3f5206b..0000000 --- a/nostr/message_type.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/nostr/nostr/__init__.py b/nostr/nostr/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nostr/nostr/bech32.py b/nostr/nostr/bech32.py deleted file mode 100644 index b068de7..0000000 --- a/nostr/nostr/bech32.py +++ /dev/null @@ -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 diff --git a/nostr/nostr/client/__init__.py b/nostr/nostr/client/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nostr/nostr/client/aes.py b/nostr/nostr/client/aes.py deleted file mode 100644 index 9d20224..0000000 --- a/nostr/nostr/client/aes.py +++ /dev/null @@ -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) diff --git a/nostr/nostr/client/cbc.py b/nostr/nostr/client/cbc.py deleted file mode 100644 index a41dbc0..0000000 --- a/nostr/nostr/client/cbc.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/nostr/nostr/client/client.py b/nostr/nostr/client/client.py deleted file mode 100644 index 68ec69d..0000000 --- a/nostr/nostr/client/client.py +++ /dev/null @@ -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) diff --git a/nostr/nostr/event.py b/nostr/nostr/event.py deleted file mode 100644 index 450893e..0000000 --- a/nostr/nostr/event.py +++ /dev/null @@ -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 - } diff --git a/nostr/nostr/filter.py b/nostr/nostr/filter.py deleted file mode 100644 index 32b94a3..0000000 --- a/nostr/nostr/filter.py +++ /dev/null @@ -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] diff --git a/nostr/nostr/key.py b/nostr/nostr/key.py deleted file mode 100644 index 9449c00..0000000 --- a/nostr/nostr/key.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/nostr/nostr/message_pool.py b/nostr/nostr/message_pool.py deleted file mode 100644 index 472e31e..0000000 --- a/nostr/nostr/message_pool.py +++ /dev/null @@ -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)) diff --git a/nostr/nostr/message_type.py b/nostr/nostr/message_type.py deleted file mode 100644 index 3f5206b..0000000 --- a/nostr/nostr/message_type.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/nostr/nostr/relay.py b/nostr/nostr/relay.py deleted file mode 100644 index ad01ff6..0000000 --- a/nostr/nostr/relay.py +++ /dev/null @@ -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 diff --git a/nostr/nostr/relay_manager.py b/nostr/nostr/relay_manager.py deleted file mode 100644 index e4d177e..0000000 --- a/nostr/nostr/relay_manager.py +++ /dev/null @@ -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) - diff --git a/nostr/nostr/subscription.py b/nostr/nostr/subscription.py deleted file mode 100644 index 7afba20..0000000 --- a/nostr/nostr/subscription.py +++ /dev/null @@ -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() - } diff --git a/nostr/relay.py b/nostr/relay.py deleted file mode 100644 index 16667eb..0000000 --- a/nostr/relay.py +++ /dev/null @@ -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 diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py deleted file mode 100644 index e4d177e..0000000 --- a/nostr/relay_manager.py +++ /dev/null @@ -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) - diff --git a/nostr/subscription.py b/nostr/subscription.py deleted file mode 100644 index 7afba20..0000000 --- a/nostr/subscription.py +++ /dev/null @@ -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() - } diff --git a/poetry.lock b/poetry.lock index c3cbd9f..f334f30 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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"}, ]