diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07eb7208..52c48d99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,8 +12,9 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ["3.9", "3.10"] + python-version: ["3.10"] poetry-version: ["1.5.1"] + mint-cache-secrets: ["true", "false"] # db-url: ["", "postgres://cashu:cashu@localhost:5432/test"] # TODO: Postgres test not working db-url: [""] backend-wallet-class: ["FakeWallet"] @@ -21,6 +22,7 @@ jobs: with: python-version: ${{ matrix.python-version }} poetry-version: ${{ matrix.poetry-version }} + mint-cache-secrets: ${{ matrix.mint-cache-secrets }} regtest: uses: ./.github/workflows/regtest.yml strategy: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2889b52e..927f5674 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,6 +15,9 @@ on: os: default: "ubuntu-latest" type: string + mint-cache-secrets: + default: "false" + type: string jobs: poetry: @@ -47,6 +50,7 @@ jobs: MINT_HOST: localhost MINT_PORT: 3337 MINT_DATABASE: ${{ inputs.db-url }} + MINT_CACHE_SECRETS: ${{ inputs.mint-cache-secrets }} TOR: false run: | make test diff --git a/cashu/core/base.py b/cashu/core/base.py index 76492cf7..b5e14f8d 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -348,6 +348,11 @@ def __init__( self.public_keys = public_keys # overwrite id by deriving it from the public keys self.id = derive_keyset_id(self.public_keys) + logger.trace(f"Derived keyset id {self.id} from public keys.") + if id and id != self.id: + logger.warning( + f"WARNING: Keyset id {self.id} does not match the given id {id}." + ) def serialize(self): return json.dumps( @@ -356,9 +361,9 @@ def serialize(self): @classmethod def from_row(cls, row: Row): - def deserialize(serialized: str): + def deserialize(serialized: str) -> Dict[int, PublicKey]: return { - amount: PublicKey(bytes.fromhex(hex_key), raw=True) + int(amount): PublicKey(bytes.fromhex(hex_key), raw=True) for amount, hex_key in dict(json.loads(serialized)).items() } diff --git a/cashu/core/db.py b/cashu/core/db.py index e2f99d88..b4337cca 100644 --- a/cashu/core/db.py +++ b/cashu/core/db.py @@ -5,6 +5,7 @@ from contextlib import asynccontextmanager from typing import Optional, Union +from loguru import logger from sqlalchemy import create_engine from sqlalchemy_aio.base import AsyncConnection from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY # type: ignore @@ -130,7 +131,7 @@ def _parse_timestamp(value, _): # ) else: if not os.path.exists(self.db_location): - print(f"Creating database directory: {self.db_location}") + logger.info(f"Creating database directory: {self.db_location}") os.makedirs(self.db_location) self.path = os.path.join(self.db_location, f"{self.name}.sqlite3") database_uri = f"sqlite:///{self.path}" diff --git a/cashu/core/settings.py b/cashu/core/settings.py index 274e1098..d4ff75b7 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -44,6 +44,7 @@ class EnvSettings(CashuSettings): debug: bool = Field(default=False) log_level: str = Field(default="INFO") cashu_dir: str = Field(default=os.path.join(str(Path.home()), ".cashu")) + debug_profiling: bool = Field(default=False) class MintSettings(CashuSettings): @@ -56,10 +57,13 @@ class MintSettings(CashuSettings): mint_peg_out_only: bool = Field(default=False) mint_max_peg_in: int = Field(default=None) mint_max_peg_out: int = Field(default=None) + mint_max_balance: int = Field(default=None) mint_lnbits_endpoint: str = Field(default=None) mint_lnbits_key: str = Field(default=None) + mint_cache_secrets: bool = Field(default=True) + class MintInformation(CashuSettings): mint_info_name: str = Field(default="Cashu mint") diff --git a/cashu/mint/app.py b/cashu/mint/app.py index 895945e8..0de2d096 100644 --- a/cashu/mint/app.py +++ b/cashu/mint/app.py @@ -8,8 +8,6 @@ ) from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse - -# from fastapi_profiler import PyInstrumentProfilerMiddleware from loguru import logger from starlette.middleware import Middleware from starlette.middleware.cors import CORSMiddleware @@ -20,6 +18,9 @@ from .router import router from .startup import start_mint_init +if settings.debug_profiling: + from fastapi_profiler import PyInstrumentProfilerMiddleware + # from starlette_context import context # from starlette_context.middleware import RawContextMiddleware @@ -108,7 +109,9 @@ def emit(self, record): middleware=middleware, ) - # app.add_middleware(PyInstrumentProfilerMiddleware) + if settings.debug_profiling: + assert PyInstrumentProfilerMiddleware is not None + app.add_middleware(PyInstrumentProfilerMiddleware) return app diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py index 5a189710..8797dc20 100644 --- a/cashu/mint/crud.py +++ b/cashu/mint/crud.py @@ -31,7 +31,7 @@ async def get_lightning_invoice( db: Database, id: str, conn: Optional[Connection] = None, - ): + ) -> Optional[Invoice]: return await get_lightning_invoice( db=db, id=id, @@ -42,8 +42,23 @@ async def get_secrets_used( self, db: Database, conn: Optional[Connection] = None, - ): - return await get_secrets_used(db=db, conn=conn) + ) -> List[str]: + return await get_secrets_used( + db=db, + conn=conn, + ) + + async def get_proof_used( + self, + db: Database, + proof: Proof, + conn: Optional[Connection] = None, + ) -> Optional[Proof]: + return await get_proof_used( + db=db, + proof=proof, + conn=conn, + ) async def invalidate_proof( self, @@ -158,6 +173,16 @@ async def update_lightning_invoice( conn=conn, ) + async def get_balance( + self, + db: Database, + conn: Optional[Connection] = None, + ) -> int: + return await get_balance( + db=db, + conn=conn, + ) + async def store_promise( *, @@ -205,7 +230,7 @@ async def get_promise( async def get_secrets_used( db: Database, conn: Optional[Connection] = None, -): +) -> List[str]: rows = await (conn or db).fetchall(f""" SELECT secret from {table_with_schema(db, 'proofs_used')} """) @@ -243,6 +268,21 @@ async def get_proofs_pending( return [Proof(**r) for r in rows] +async def get_proof_used( + db: Database, + proof: Proof, + conn: Optional[Connection] = None, +) -> Optional[Proof]: + row = await (conn or db).fetchone( + f""" + SELECT 1 from {table_with_schema(db, 'proofs_used')} + WHERE secret = ? + """, + (str(proof.secret),), + ) + return Proof(**row) if row else None + + async def set_proof_pending( db: Database, proof: Proof, @@ -394,3 +434,14 @@ async def get_keyset( tuple(values), ) return [MintKeyset(**row) for row in rows] + + +async def get_balance( + db: Database, + conn: Optional[Connection] = None, +) -> int: + row = await (conn or db).fetchone(f""" + SELECT * from {table_with_schema(db, 'balance')} + """) + assert row, "Balance not found" + return int(row[0]) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 371da7dd..a0402481 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -1,6 +1,6 @@ import asyncio import math -from typing import Dict, List, Optional, Set, Tuple +from typing import Dict, List, Optional, Tuple import bolt11 from loguru import logger @@ -49,7 +49,6 @@ def __init__( crud: LedgerCrud, derivation_path="", ): - self.secrets_used: Set[str] = set() self.master_key = seed self.derivation_path = derivation_path @@ -146,6 +145,10 @@ def get_keyset(self, keyset_id: Optional[str] = None) -> Dict[int, str]: assert keyset.public_keys, KeysetError("no public keys for this keyset") return {a: p.serialize().hex() for a, p in keyset.public_keys.items()} + async def get_balance(self) -> int: + """Returns the balance of the mint.""" + return await self.crud.get_balance(db=self.db) + # ------- ECASH ------- async def _invalidate_proofs(self, proofs: List[Proof]) -> None: @@ -158,9 +161,10 @@ async def _invalidate_proofs(self, proofs: List[Proof]) -> None: # Mark proofs as used and prepare new promises secrets = set([p.secret for p in proofs]) self.secrets_used |= secrets - # store in db - for p in proofs: - await self.crud.invalidate_proof(proof=p, db=self.db) + async with self.db.connect() as conn: + # store in db + for p in proofs: + await self.crud.invalidate_proof(proof=p, db=self.db, conn=conn) async def _generate_change_promises( self, @@ -245,6 +249,10 @@ async def request_mint(self, amount: int) -> Tuple[str, str]: ) if settings.mint_peg_out_only: raise NotAllowedError("Mint does not allow minting new tokens.") + if settings.mint_max_balance: + balance = await self.get_balance() + if balance + amount > settings.mint_max_balance: + raise NotAllowedError("Mint has reached maximum balance.") logger.trace(f"requesting invoice for {amount} satoshis") invoice_response = await self._request_lightning_invoice(amount) @@ -530,70 +538,59 @@ async def restore( async def _generate_promises( self, B_s: List[BlindedMessage], keyset: Optional[MintKeyset] = None ) -> list[BlindedSignature]: - """Generates promises that sum to the given amount. - - Args: - B_s (List[BlindedMessage]): _description_ - keyset (Optional[MintKeyset], optional): _description_. Defaults to None. - - Returns: - list[BlindedSignature]: _description_ - """ - return [ - await self._generate_promise( - b.amount, PublicKey(bytes.fromhex(b.B_), raw=True), keyset - ) - for b in B_s - ] - - async def _generate_promise( - self, amount: int, B_: PublicKey, keyset: Optional[MintKeyset] = None - ) -> BlindedSignature: - """Generates a promise (Blind signature) for given amount and returns a pair (amount, C'). + """Generates a promises (Blind signatures) for given amount and returns a pair (amount, C'). Args: - amount (int): Amount of the promise. - B_ (PublicKey): Blinded secret (point on curve) + B_s (List[BlindedMessage]): Blinded secret (point on curve) keyset (Optional[MintKeyset], optional): Which keyset to use. Private keys will be taken from this keyset. Defaults to None. Returns: - BlindedSignature: Generated promise. + list[BlindedSignature]: Generated BlindedSignatures. """ keyset = keyset if keyset else self.keyset - logger.trace(f"Generating promise with keyset {keyset.id}.") - private_key_amount = keyset.private_keys[amount] - C_, e, s = b_dhke.step2_bob(B_, private_key_amount) - logger.trace(f"crud: _generate_promise storing promise for {amount}") - await self.crud.store_promise( - amount=amount, - B_=B_.serialize().hex(), - C_=C_.serialize().hex(), - e=e.serialize(), - s=s.serialize(), - db=self.db, - id=keyset.id, - ) - logger.trace(f"crud: _generate_promise stored promise for {amount}") - return BlindedSignature( - id=keyset.id, - amount=amount, - C_=C_.serialize().hex(), - dleq=DLEQ(e=e.serialize(), s=s.serialize()), - ) + promises = [] + for b in B_s: + amount = b.amount + B_ = PublicKey(bytes.fromhex(b.B_), raw=True) + logger.trace(f"Generating promise with keyset {keyset.id}.") + private_key_amount = keyset.private_keys[amount] + C_, e, s = b_dhke.step2_bob(B_, private_key_amount) + promises.append((B_, amount, C_, e, s)) + + signatures = [] + async with self.db.connect() as conn: + for promise in promises: + B_, amount, C_, e, s = promise + logger.trace(f"crud: _generate_promise storing promise for {amount}") + await self.crud.store_promise( + amount=amount, + id=keyset.id, + B_=B_.serialize().hex(), + C_=C_.serialize().hex(), + e=e.serialize(), + s=s.serialize(), + db=self.db, + conn=conn, + ) + logger.trace(f"crud: _generate_promise stored promise for {amount}") + signature = BlindedSignature( + id=keyset.id, + amount=amount, + C_=C_.serialize().hex(), + dleq=DLEQ(e=e.serialize(), s=s.serialize()), + ) + signatures.append(signature) + return signatures # ------- PROOFS ------- async def load_used_proofs(self) -> None: """Load all used proofs from database.""" - logger.trace("crud: loading used proofs") + logger.debug("Loading used proofs into memory") secrets_used = await self.crud.get_secrets_used(db=self.db) - logger.trace(f"crud: loaded {len(secrets_used)} used proofs") + logger.debug(f"Loaded {len(secrets_used)} used proofs") self.secrets_used = set(secrets_used) - def _check_spendable(self, proof: Proof) -> bool: - """Checks whether the proof was already spent.""" - return proof.secret not in self.secrets_used - async def _check_pending(self, proofs: List[Proof]) -> List[bool]: """Checks whether the proof is still pending.""" proofs_pending = await self.crud.get_proofs_pending(db=self.db) @@ -620,13 +617,12 @@ async def check_proof_state( List[bool]: List of which proof is still spendable (True if still spendable, else False) List[bool]: List of which proof are pending (True if pending, else False) """ - spendable = [self._check_spendable(p) for p in proofs] + + spendable = await self._check_proofs_spendable(proofs) pending = await self._check_pending(proofs) return spendable, pending - async def _set_proofs_pending( - self, proofs: List[Proof], conn: Optional[Connection] = None - ) -> None: + async def _set_proofs_pending(self, proofs: List[Proof]) -> None: """If none of the proofs is in the pending table (_validate_proofs_pending), adds proofs to the list of pending proofs or removes them. Used as a mutex for proofs. @@ -638,24 +634,26 @@ async def _set_proofs_pending( """ # first we check whether these proofs are pending already async with self.proofs_pending_lock: - await self._validate_proofs_pending(proofs, conn) - for p in proofs: - try: - await self.crud.set_proof_pending(proof=p, db=self.db, conn=conn) - except Exception: - raise TransactionError("proofs already pending.") - - async def _unset_proofs_pending( - self, proofs: List[Proof], conn: Optional[Connection] = None - ) -> None: + async with self.db.connect() as conn: + await self._validate_proofs_pending(proofs, conn) + for p in proofs: + try: + await self.crud.set_proof_pending( + proof=p, db=self.db, conn=conn + ) + except Exception: + raise TransactionError("proofs already pending.") + + async def _unset_proofs_pending(self, proofs: List[Proof]) -> None: """Deletes proofs from pending table. Args: proofs (List[Proof]): Proofs to delete. """ async with self.proofs_pending_lock: - for p in proofs: - await self.crud.unset_proof_pending(proof=p, db=self.db, conn=conn) + async with self.db.connect() as conn: + for p in proofs: + await self.crud.unset_proof_pending(proof=p, db=self.db, conn=conn) async def _validate_proofs_pending( self, proofs: List[Proof], conn: Optional[Connection] = None diff --git a/cashu/mint/migrations.py b/cashu/mint/migrations.py index 866e3ca2..67a3d0fe 100644 --- a/cashu/mint/migrations.py +++ b/cashu/mint/migrations.py @@ -204,3 +204,13 @@ async def m009_add_out_to_invoices(db: Database): await conn.execute( f"ALTER TABLE {table_with_schema(db, 'invoices')} ADD COLUMN out BOOL" ) + + +async def m010_add_index_to_proofs_used(db: Database): + # create index on proofs_used table for secret + async with db.connect() as conn: + await conn.execute( + "CREATE INDEX IF NOT EXISTS" + f" {table_with_schema(db, 'proofs_used')}_secret_idx ON" + f" {table_with_schema(db, 'proofs_used')} (secret)" + ) diff --git a/cashu/mint/startup.py b/cashu/mint/startup.py index 162c853f..833cc883 100644 --- a/cashu/mint/startup.py +++ b/cashu/mint/startup.py @@ -47,7 +47,8 @@ async def rotate_keys(n_seconds=10): async def start_mint_init(): await migrate_databases(ledger.db, migrations) - await ledger.load_used_proofs() + if settings.mint_cache_secrets: + await ledger.load_used_proofs() await ledger.init_keysets() if settings.lightning: diff --git a/cashu/mint/verification.py b/cashu/mint/verification.py index a50ba5ee..1cbd76b8 100644 --- a/cashu/mint/verification.py +++ b/cashu/mint/verification.py @@ -11,6 +11,7 @@ ) from ..core.crypto import b_dhke from ..core.crypto.secp import PublicKey +from ..core.db import Database from ..core.errors import ( NoSecretInProofsError, NotAllowedError, @@ -19,16 +20,19 @@ TransactionError, ) from ..core.settings import settings +from ..mint.crud import LedgerCrud from .conditions import LedgerSpendingConditions -from .protocols import SupportsKeysets +from .protocols import SupportsDb, SupportsKeysets -class LedgerVerification(LedgerSpendingConditions, SupportsKeysets): +class LedgerVerification(LedgerSpendingConditions, SupportsKeysets, SupportsDb): """Verification functions for the ledger.""" keyset: MintKeyset keysets: MintKeysets - secrets_used: Set[str] + secrets_used: Set[str] = set() + crud: LedgerCrud + db: Database async def verify_inputs_and_outputs( self, proofs: List[Proof], outputs: Optional[List[BlindedMessage]] = None @@ -48,7 +52,9 @@ async def verify_inputs_and_outputs( """ # Verify inputs # Verify proofs are spendable - self._check_proofs_spendable(proofs) + spendable = await self._check_proofs_spendable(proofs) + if not all(spendable): + raise TokenAlreadySpentError() # Verify amounts of inputs if not all([self._verify_amount(p.amount) for p in proofs]): raise TransactionError("invalid amount.") @@ -87,10 +93,24 @@ def _verify_outputs(self, outputs: List[BlindedMessage]): if not self._verify_no_duplicate_outputs(outputs): raise TransactionError("duplicate outputs.") - def _check_proofs_spendable(self, proofs: List[Proof]): - """Checks whether the proofs were already spent.""" - if not all([p.secret not in self.secrets_used for p in proofs]): - raise TokenAlreadySpentError() + async def _check_proofs_spendable(self, proofs: List[Proof]) -> List[bool]: + """Checks whether the proof was already spent.""" + spendable_states = [] + if settings.mint_cache_secrets: + # check used secrets in memory + for p in proofs: + spendable_state = p.secret not in self.secrets_used + spendable_states.append(spendable_state) + else: + # check used secrets in database + async with self.db.connect() as conn: + for p in proofs: + spendable_state = ( + await self.crud.get_proof_used(db=self.db, proof=p, conn=conn) + is None + ) + spendable_states.append(spendable_state) + return spendable_states def _verify_secret_criteria(self, proof: Proof) -> Literal[True]: """Verifies that a secret is present and is not too long (DOS prevention).""" diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 7c20ff81..b3c8baf4 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -94,6 +94,7 @@ async def wrapper(self, *args, **kwargs): proxies=proxies_dict, # type: ignore headers=headers_dict, base_url=self.url, + timeout=None if settings.debug else 5, ) return await func(self, *args, **kwargs) @@ -171,21 +172,24 @@ async def _load_mint_keys(self, keyset_id: Optional[str] = None) -> None: keyset_local: Union[WalletKeyset, None] = None if keyset_id: # check if current keyset is in db + logger.trace(f"Checking if keyset {keyset_id} is in database.") keyset_local = await get_keyset(keyset_id, db=self.db) if keyset_local: - logger.debug(f"Found keyset {keyset_id} in database.") + logger.trace(f"Found keyset {keyset_id} in database.") else: - logger.debug( - f"Cannot find keyset {keyset_id} in database. Loading keyset from" - " mint." + logger.trace( + f"Could not find keyset {keyset_id} in database. Loading keyset" + " from mint." ) keyset = keyset_local if keyset_local is None and keyset_id: # get requested keyset from mint + logger.trace(f"Getting keyset {keyset_id} from mint.") keyset = await self._get_keys_of_keyset(self.url, keyset_id) else: # get current keyset + logger.trace("Getting current keyset from mint.") keyset = await self._get_keys(self.url) assert keyset diff --git a/poetry.lock b/poetry.lock index 95e82158..40405c08 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "anyio" version = "4.0.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -26,7 +25,6 @@ trio = ["trio (>=0.22)"] name = "asn1crypto" version = "1.5.1" description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" -category = "main" optional = false python-versions = "*" files = [ @@ -38,7 +36,6 @@ files = [ name = "attrs" version = "23.1.0" description = "Classes Without Boilerplate" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -57,7 +54,6 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte name = "base58" version = "2.1.1" description = "Base58 and Base58Check implementation." -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -72,7 +68,6 @@ tests = ["PyHamcrest (>=2.0.2)", "mypy", "pytest (>=4.6)", "pytest-benchmark", " name = "bech32" version = "1.2.0" description = "Reference implementation for Bech32 and segwit addresses." -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -84,7 +79,6 @@ files = [ name = "bip32" version = "3.4" description = "Minimalistic implementation of the BIP32 key derivation scheme" -category = "main" optional = false python-versions = "*" files = [ @@ -100,7 +94,6 @@ coincurve = ">=15.0,<19" name = "bitstring" version = "3.1.9" description = "Simple construction, analysis and modification of binary data." -category = "main" optional = false python-versions = "*" files = [ @@ -113,7 +106,6 @@ files = [ name = "black" version = "23.9.1" description = "The uncompromising code formatter." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -160,7 +152,6 @@ uvloop = ["uvloop (>=0.15.2)"] name = "bolt11" version = "2.0.5" description = "A library for encoding and decoding BOLT11 payment requests." -category = "main" optional = false python-versions = ">=3.8.1" files = [ @@ -180,7 +171,6 @@ secp256k1 = "*" name = "certifi" version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -192,7 +182,6 @@ files = [ name = "cffi" version = "1.16.0" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -257,7 +246,6 @@ pycparser = "*" name = "cfgv" version = "3.4.0" description = "Validate configuration and produce human readable error messages." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -269,7 +257,6 @@ files = [ name = "click" version = "8.1.7" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -284,7 +271,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "coincurve" version = "18.0.0" description = "Cross-platform Python CFFI bindings for libsecp256k1" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -340,7 +326,6 @@ cffi = ">=1.3.0" name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -352,7 +337,6 @@ files = [ name = "coverage" version = "7.3.2" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -420,7 +404,6 @@ toml = ["tomli"] name = "cryptography" version = "41.0.4" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -466,7 +449,6 @@ test-randomorder = ["pytest-randomly"] name = "distlib" version = "0.3.7" description = "Distribution utilities" -category = "dev" optional = false python-versions = "*" files = [ @@ -478,7 +460,6 @@ files = [ name = "ecdsa" version = "0.18.0" description = "ECDSA cryptographic signature library (pure python)" -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -497,7 +478,6 @@ gmpy2 = ["gmpy2"] name = "environs" version = "9.5.0" description = "simplified environment variable parsing" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -519,7 +499,6 @@ tests = ["dj-database-url", "dj-email-url", "django-cache-url", "pytest"] name = "exceptiongroup" version = "1.1.3" description = "Backport of PEP 654 (exception groups)" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -534,7 +513,6 @@ test = ["pytest (>=6)"] name = "fastapi" version = "0.103.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -550,11 +528,24 @@ typing-extensions = ">=4.5.0" [package.extras] all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +[[package]] +name = "fastapi-profiler" +version = "1.2.0" +description = "A FastAPI Middleware of pyinstrument to check your service performance." +optional = false +python-versions = "*" +files = [ + {file = "fastapi_profiler-1.2.0-py3-none-any.whl", hash = "sha256:71615f815c5ff4fe193c14b1ecf6bfc250502c6adfca64a219340df5eb0a7a9b"}, +] + +[package.dependencies] +fastapi = "*" +pyinstrument = ">=4.4.0" + [[package]] name = "filelock" version = "3.12.4" description = "A platform independent file lock." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -571,7 +562,6 @@ typing = ["typing-extensions (>=4.7.1)"] name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -583,7 +573,6 @@ files = [ name = "httpcore" version = "0.18.0" description = "A minimal low-level HTTP client." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -595,17 +584,16 @@ files = [ anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" -sniffio = ">=1.0.0,<2.0.0" +sniffio = "==1.*" [package.extras] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [[package]] name = "httpx" version = "0.25.1" description = "The next generation HTTP client." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -619,19 +607,18 @@ certifi = "*" httpcore = "*" idna = "*" sniffio = "*" -socksio = {version = ">=1.0.0,<2.0.0", optional = true, markers = "extra == \"socks\""} +socksio = {version = "==1.*", optional = true, markers = "extra == \"socks\""} [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [[package]] name = "identify" version = "2.5.30" description = "File identification library for Python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -646,7 +633,6 @@ license = ["ukkonen"] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -658,7 +644,6 @@ files = [ name = "importlib-metadata" version = "6.8.0" description = "Read metadata from Python packages" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -678,7 +663,6 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -690,7 +674,6 @@ files = [ name = "loguru" version = "0.7.2" description = "Python logging made (stupidly) simple" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -709,7 +692,6 @@ dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptio name = "marshmallow" version = "3.20.1" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -730,7 +712,6 @@ tests = ["pytest", "pytz", "simplejson"] name = "mnemonic" version = "0.20" description = "Implementation of Bitcoin BIP-0039" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -742,7 +723,6 @@ files = [ name = "mypy" version = "1.6.0" description = "Optional static typing for Python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -789,7 +769,6 @@ reports = ["lxml"] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -801,7 +780,6 @@ files = [ name = "nodeenv" version = "1.8.0" description = "Node.js virtual environment builder" -category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ @@ -816,7 +794,6 @@ setuptools = "*" name = "outcome" version = "1.2.0" description = "Capture the outcome of Python function calls." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -831,7 +808,6 @@ attrs = ">=19.2.0" name = "packaging" version = "23.2" description = "Core utilities for Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -843,7 +819,6 @@ files = [ name = "pathspec" version = "0.11.2" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -855,7 +830,6 @@ files = [ name = "platformdirs" version = "3.11.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" files = [ @@ -871,7 +845,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co name = "pluggy" version = "1.3.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -887,7 +860,6 @@ testing = ["pytest", "pytest-benchmark"] name = "pre-commit" version = "3.5.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -906,7 +878,6 @@ virtualenv = ">=20.10.0" name = "psycopg2-binary" version = "2.9.9" description = "psycopg2 - Python-PostgreSQL Database Adapter" -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -985,7 +956,6 @@ files = [ name = "pycparser" version = "2.21" description = "C parser in Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -997,7 +967,6 @@ files = [ name = "pycryptodomex" version = "3.19.0" description = "Cryptographic library for Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1039,7 +1008,6 @@ files = [ name = "pydantic" version = "1.10.13" description = "Data validation and settings management using python type hints" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1088,11 +1056,86 @@ typing-extensions = ">=4.2.0" dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] +[[package]] +name = "pyinstrument" +version = "4.6.1" +description = "Call stack profiler for Python. Shows you why your code is slow!" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyinstrument-4.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:73476e4bc6e467ac1b2c3c0dd1f0b71c9061d4de14626676adfdfbb14aa342b4"}, + {file = "pyinstrument-4.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4d1da8efd974cf9df52ee03edaee2d3875105ddd00de35aa542760f7c612bdf7"}, + {file = "pyinstrument-4.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:507be1ee2f2b0c9fba74d622a272640dd6d1b0c9ec3388b2cdeb97ad1e77125f"}, + {file = "pyinstrument-4.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95cee6de08eb45754ef4f602ce52b640d1c535d934a6a8733a974daa095def37"}, + {file = "pyinstrument-4.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7873e8cec92321251fdf894a72b3c78f4c5c20afdd1fef0baf9042ec843bb04"}, + {file = "pyinstrument-4.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a242f6cac40bc83e1f3002b6b53681846dfba007f366971db0bf21e02dbb1903"}, + {file = "pyinstrument-4.6.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:97c9660cdb4bd2a43cf4f3ab52cffd22f3ac9a748d913b750178fb34e5e39e64"}, + {file = "pyinstrument-4.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e304cd0723e2b18ada5e63c187abf6d777949454c734f5974d64a0865859f0f4"}, + {file = "pyinstrument-4.6.1-cp310-cp310-win32.whl", hash = "sha256:cee21a2d78187dd8a80f72f5d0f1ddb767b2d9800f8bb4d94b6d11f217c22cdb"}, + {file = "pyinstrument-4.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:2000712f71d693fed2f8a1c1638d37b7919124f367b37976d07128d49f1445eb"}, + {file = "pyinstrument-4.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a366c6f3dfb11f1739bdc1dee75a01c1563ad0bf4047071e5e77598087df457f"}, + {file = "pyinstrument-4.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c6be327be65d934796558aa9cb0f75ce62ebd207d49ad1854610c97b0579ad47"}, + {file = "pyinstrument-4.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e160d9c5d20d3e4ef82269e4e8b246ff09bdf37af5fb8cb8ccca97936d95ad6"}, + {file = "pyinstrument-4.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ffbf56605ef21c2fcb60de2fa74ff81f417d8be0c5002a407e414d6ef6dee43"}, + {file = "pyinstrument-4.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c92cc4924596d6e8f30a16182bbe90893b1572d847ae12652f72b34a9a17c24a"}, + {file = "pyinstrument-4.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f4b48a94d938cae981f6948d9ec603bab2087b178d2095d042d5a48aabaecaab"}, + {file = "pyinstrument-4.6.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e7a386392275bdef4a1849712dc5b74f0023483fca14ef93d0ca27d453548982"}, + {file = "pyinstrument-4.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:871b131b83e9b1122f2325061c68ed1e861eebcb568c934d2fb193652f077f77"}, + {file = "pyinstrument-4.6.1-cp311-cp311-win32.whl", hash = "sha256:8d8515156dd91f5652d13b5fcc87e634f8fe1c07b68d1d0840348cdd50bf5ace"}, + {file = "pyinstrument-4.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb868fbe089036e9f32525a249f4c78b8dc46967612393f204b8234f439c9cc4"}, + {file = "pyinstrument-4.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a18cd234cce4f230f1733807f17a134e64a1f1acabf74a14d27f583cf2b183df"}, + {file = "pyinstrument-4.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:574cfca69150be4ce4461fb224712fbc0722a49b0dc02fa204d02807adf6b5a0"}, + {file = "pyinstrument-4.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e02cf505e932eb8ccf561b7527550a67ec14fcae1fe0e25319b09c9c166e914"}, + {file = "pyinstrument-4.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832fb2acef9d53701c1ab546564c45fb70a8770c816374f8dd11420d399103c9"}, + {file = "pyinstrument-4.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13cb57e9607545623ebe462345b3d0c4caee0125d2d02267043ece8aca8f4ea0"}, + {file = "pyinstrument-4.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9be89e7419bcfe8dd6abb0d959d6d9c439c613a4a873514c43d16b48dae697c9"}, + {file = "pyinstrument-4.6.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:476785cfbc44e8e1b1ad447398aa3deae81a8df4d37eb2d8bbb0c404eff979cd"}, + {file = "pyinstrument-4.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e9cebd90128a3d2fee36d3ccb665c1b9dce75261061b2046203e45c4a8012d54"}, + {file = "pyinstrument-4.6.1-cp312-cp312-win32.whl", hash = "sha256:1d0b76683df2ad5c40eff73607dc5c13828c92fbca36aff1ddf869a3c5a55fa6"}, + {file = "pyinstrument-4.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:c4b7af1d9d6a523cfbfedebcb69202242d5bd0cb89c4e094cc73d5d6e38279bd"}, + {file = "pyinstrument-4.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:79ae152f8c6a680a188fb3be5e0f360ac05db5bbf410169a6c40851dfaebcce9"}, + {file = "pyinstrument-4.6.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07cad2745964c174c65aa75f1bf68a4394d1b4d28f33894837cfd315d1e836f0"}, + {file = "pyinstrument-4.6.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb81f66f7f94045d723069cf317453d42375de9ff3c69089cf6466b078ac1db4"}, + {file = "pyinstrument-4.6.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ab30ae75969da99e9a529e21ff497c18fdf958e822753db4ae7ed1e67094040"}, + {file = "pyinstrument-4.6.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f36cb5b644762fb3c86289324bbef17e95f91cd710603ac19444a47f638e8e96"}, + {file = "pyinstrument-4.6.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8b45075d9dbbc977dbc7007fb22bb0054c6990fbe91bf48dd80c0b96c6307ba7"}, + {file = "pyinstrument-4.6.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:475ac31477f6302e092463896d6a2055f3e6abcd293bad16ff94fc9185308a88"}, + {file = "pyinstrument-4.6.1-cp37-cp37m-win32.whl", hash = "sha256:29172ab3d8609fdf821c3f2562dc61e14f1a8ff5306607c32ca743582d3a760e"}, + {file = "pyinstrument-4.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:bd176f297c99035127b264369d2bb97a65255f65f8d4e843836baf55ebb3cee4"}, + {file = "pyinstrument-4.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:23e9b4526978432e9999021da9a545992cf2ac3df5ee82db7beb6908fc4c978c"}, + {file = "pyinstrument-4.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2dbcaccc9f456ef95557ec501caeb292119c24446d768cb4fb43578b0f3d572c"}, + {file = "pyinstrument-4.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2097f63c66c2bc9678c826b9ff0c25acde3ed455590d9dcac21220673fe74fbf"}, + {file = "pyinstrument-4.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:205ac2e76bd65d61b9611a9ce03d5f6393e34ec5b41dd38808f25d54e6b3e067"}, + {file = "pyinstrument-4.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f414ddf1161976a40fc0a333000e6a4ad612719eac0b8c9bb73f47153187148"}, + {file = "pyinstrument-4.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:65e62ebfa2cd8fb57eda90006f4505ac4c70da00fc2f05b6d8337d776ea76d41"}, + {file = "pyinstrument-4.6.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d96309df4df10be7b4885797c5f69bb3a89414680ebaec0722d8156fde5268c3"}, + {file = "pyinstrument-4.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f3d1ad3bc8ebb4db925afa706aa865c4bfb40d52509f143491ac0df2440ee5d2"}, + {file = "pyinstrument-4.6.1-cp38-cp38-win32.whl", hash = "sha256:dc37cb988c8854eb42bda2e438aaf553536566657d157c4473cc8aad5692a779"}, + {file = "pyinstrument-4.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:2cd4ce750c34a0318fc2d6c727cc255e9658d12a5cf3f2d0473f1c27157bdaeb"}, + {file = "pyinstrument-4.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6ca95b21f022e995e062b371d1f42d901452bcbedd2c02f036de677119503355"}, + {file = "pyinstrument-4.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ac1e1d7e1f1b64054c4eb04eb4869a7a5eef2261440e73943cc1b1bc3c828c18"}, + {file = "pyinstrument-4.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0711845e953fce6ab781221aacffa2a66dbc3289f8343e5babd7b2ea34da6c90"}, + {file = "pyinstrument-4.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b7d28582017de35cb64eb4e4fa603e753095108ca03745f5d17295970ee631f"}, + {file = "pyinstrument-4.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7be57db08bd366a37db3aa3a6187941ee21196e8b14975db337ddc7d1490649d"}, + {file = "pyinstrument-4.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9a0ac0f56860398d2628ce389826ce83fb3a557d0c9a2351e8a2eac6eb869983"}, + {file = "pyinstrument-4.6.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a9045186ff13bc826fef16be53736a85029aae3c6adfe52e666cad00d7ca623b"}, + {file = "pyinstrument-4.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6c4c56b6eab9004e92ad8a48bb54913fdd71fc8a748ae42a27b9e26041646f8b"}, + {file = "pyinstrument-4.6.1-cp39-cp39-win32.whl", hash = "sha256:37e989c44b51839d0c97466fa2b623638b9470d56d79e329f359f0e8fa6d83db"}, + {file = "pyinstrument-4.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:5494c5a84fee4309d7d973366ca6b8b9f8ba1d6b254e93b7c506264ef74f2cef"}, + {file = "pyinstrument-4.6.1.tar.gz", hash = "sha256:f4731b27121350f5a983d358d2272fe3df2f538aed058f57217eef7801a89288"}, +] + +[package.extras] +bin = ["click", "nox"] +docs = ["furo (==2021.6.18b36)", "myst-parser (==0.15.1)", "sphinx (==4.2.0)", "sphinxcontrib-programoutput (==0.17)"] +examples = ["django", "numpy"] +test = ["flaky", "greenlet (>=3.0.0a1)", "ipython", "pytest", "pytest-asyncio (==0.12.0)", "sphinx-autobuild (==2021.3.14)", "trio"] +types = ["typing-extensions"] + [[package]] name = "pytest" version = "7.4.2" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1115,7 +1158,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-asyncio" version = "0.21.1" description = "Pytest support for asyncio" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1134,7 +1176,6 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy name = "pytest-cov" version = "4.1.0" description = "Pytest plugin for measuring coverage." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1153,7 +1194,6 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "python-dotenv" version = "1.0.0" description = "Read key-value pairs from a .env file and set them as environment variables" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1168,7 +1208,6 @@ cli = ["click (>=5.0)"] name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1218,7 +1257,6 @@ files = [ name = "represent" version = "1.6.0.post0" description = "Create __repr__ automatically or declaratively." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1236,7 +1274,6 @@ test = ["ipython", "mock", "pytest (>=3.0.5)"] name = "ruff" version = "0.0.284" description = "An extremely fast Python linter, written in Rust." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1263,7 +1300,6 @@ files = [ name = "secp256k1" version = "0.14.0" description = "FFI bindings to libsecp256k1" -category = "main" optional = false python-versions = "*" files = [ @@ -1299,7 +1335,6 @@ cffi = ">=1.3.0" name = "setuptools" version = "68.2.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1316,7 +1351,6 @@ testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jar name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1328,7 +1362,6 @@ files = [ name = "sniffio" version = "1.3.0" description = "Sniff out which async library your code is running under" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1340,7 +1373,6 @@ files = [ name = "socksio" version = "1.0.0" description = "Sans-I/O implementation of SOCKS4, SOCKS4A, and SOCKS5." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1352,7 +1384,6 @@ files = [ name = "sqlalchemy" version = "1.3.24" description = "Database Abstraction Library" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1408,7 +1439,6 @@ pymysql = ["pymysql", "pymysql (<1)"] name = "sqlalchemy-aio" version = "0.17.0" description = "Async support for SQLAlchemy." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1430,7 +1460,6 @@ trio = ["trio (>=0.15)"] name = "starlette" version = "0.27.0" description = "The little ASGI library that shines." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1449,7 +1478,6 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1461,7 +1489,6 @@ files = [ name = "typing-extensions" version = "4.8.0" description = "Backported and Experimental Type Hints for Python 3.8+" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1473,7 +1500,6 @@ files = [ name = "uvicorn" version = "0.23.2" description = "The lightning-fast ASGI server." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1493,7 +1519,6 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", name = "virtualenv" version = "20.24.5" description = "Virtual Python Environment builder" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1514,7 +1539,6 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess name = "websocket-client" version = "1.6.4" description = "WebSocket client for Python with low level API options" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1531,7 +1555,6 @@ test = ["websockets"] name = "wheel" version = "0.41.2" description = "A built-package format for Python" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1546,7 +1569,6 @@ test = ["pytest (>=6.0.0)", "setuptools (>=65)"] name = "win32-setctime" version = "1.1.0" description = "A small Python utility to set file creation time on Windows" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1561,7 +1583,6 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] name = "zipp" version = "3.17.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1579,4 +1600,4 @@ pgsql = ["psycopg2-binary"] [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "fd3495f7158b86ad25037517a1dfe033cfd367d75857b4db5c9db10f233bb842" +content-hash = "b2c312fd906aa18a26712039f700322c2c20889a95e1cd9af787df54d700b2ca" diff --git a/pyproject.toml b/pyproject.toml index 608bbc9b..98b725a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ pytest-cov = "^4.0.0" pytest = "^7.4.0" ruff = "^0.0.284" pre-commit = "^3.3.3" +fastapi-profiler = "^1.2.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/conftest.py b/tests/conftest.py index 9906b832..90562137 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,7 @@ SERVER_PORT = 3337 SERVER_ENDPOINT = f"http://localhost:{SERVER_PORT}" +settings.debug = True settings.cashu_dir = "./test_data/" settings.mint_host = "localhost" settings.mint_port = SERVER_PORT @@ -32,6 +33,7 @@ settings.mint_database = "./test_data/test_mint" settings.mint_derivation_path = "0/0/0/0" settings.mint_private_key = "TEST_PRIVATE_KEY" +settings.mint_max_balance = 0 shutil.rmtree(settings.cashu_dir, ignore_errors=True) Path(settings.cashu_dir).mkdir(parents=True, exist_ok=True) @@ -54,7 +56,8 @@ def run(self, *args, **kwargs): async def ledger(): async def start_mint_init(ledger: Ledger): await migrate_databases(ledger.db, migrations_mint) - await ledger.load_used_proofs() + if settings.mint_cache_secrets: + await ledger.load_used_proofs() await ledger.init_keysets() database_name = "test" diff --git a/tests/test_cli.py b/tests/test_cli.py index 3ec460e3..64119ae4 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,5 @@ import asyncio +from typing import Tuple import pytest from click.testing import CliRunner @@ -15,7 +16,7 @@ def cli_prefix(): yield ["--wallet", "test_cli_wallet", "--host", settings.mint_url, "--tests"] -def get_bolt11_and_invoice_id_from_invoice_command(output: str) -> (str, str): +def get_bolt11_and_invoice_id_from_invoice_command(output: str) -> Tuple[str, str]: invoice = [ line.split(" ")[1] for line in output.split("\n") if line.startswith("Invoice") ][0] diff --git a/tests/test_mint.py b/tests/test_mint.py index 404f42ab..c646bbba 100644 --- a/tests/test_mint.py +++ b/tests/test_mint.py @@ -182,3 +182,20 @@ async def test_generate_change_promises_returns_empty_if_no_outputs(ledger: Ledg total_provided, invoice_amount, actual_fee_msat, outputs ) assert len(promises) == 0 + + +@pytest.mark.asyncio +async def test_get_balance(ledger: Ledger): + balance = await ledger.get_balance() + assert balance == 0 + + +@pytest.mark.asyncio +async def test_maximum_balance(ledger: Ledger): + settings.mint_max_balance = 1000 + invoice, id = await ledger.request_mint(8) + await assert_err( + ledger.request_mint(8000), + "Mint has reached maximum balance.", + ) + settings.mint_max_balance = 0 diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 6433009c..d719ce94 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -1,3 +1,4 @@ +import copy import shutil from pathlib import Path from typing import List, Union @@ -9,7 +10,7 @@ from cashu.core.errors import CashuError, KeysetNotFoundError from cashu.core.helpers import sum_proofs from cashu.core.settings import settings -from cashu.wallet.crud import get_lightning_invoice, get_proofs +from cashu.wallet.crud import get_keyset, get_lightning_invoice, get_proofs from cashu.wallet.wallet import Wallet from cashu.wallet.wallet import Wallet as Wallet1 from cashu.wallet.wallet import Wallet as Wallet2 @@ -114,6 +115,27 @@ async def test_get_keyset(wallet1: Wallet): assert len(keys1.public_keys) == len(keys2.public_keys) +@pytest.mark.asyncio +async def test_get_keyset_from_db(wallet1: Wallet): + # first load it from the mint + # await wallet1._load_mint_keys() + # NOTE: conftest already called wallet.load_mint() which got the keys from the mint + keyset1 = copy.copy(wallet1.keysets[wallet1.keyset_id]) + + # then load it from the db + await wallet1._load_mint_keys() + keyset2 = copy.copy(wallet1.keysets[wallet1.keyset_id]) + + assert keyset1.public_keys == keyset2.public_keys + assert keyset1.id == keyset2.id + + # load it directly from the db + keyset3 = await get_keyset(db=wallet1.db, id=keyset1.id) + assert keyset3 + assert keyset1.public_keys == keyset3.public_keys + assert keyset1.id == keyset3.id + + @pytest.mark.asyncio async def test_get_info(wallet1: Wallet): info = await wallet1._get_info(wallet1.url)