diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 420721e..7492605 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -14,7 +14,7 @@ permissions: jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 strategy: matrix: python-version: [ '3.10', '3.11' ] diff --git a/plutus_bench/mock.py b/plutus_bench/mock.py index 6ec71f8..f929260 100644 --- a/plutus_bench/mock.py +++ b/plutus_bench/mock.py @@ -5,6 +5,7 @@ from collections import defaultdict from dataclasses import asdict from typing import Any, Callable, Dict, List, Optional, Union +import functools import cbor2 import pycardano @@ -41,6 +42,7 @@ default_encoder, StakeKeyPair, StakeVerificationKey, + RedeemerKey, ) from .protocol_params import ( @@ -53,6 +55,7 @@ ScriptInvocation, ) +import pickle ValidatorType = Callable[[Any, Any, Any], Any] MintingPolicyType = Callable[[Any, Any], Any] @@ -68,7 +71,14 @@ def __str__(self): return f"{super().__str__()}\n{''.join(self.logs)}" +class InvalidTransactionError(Exception): + def __init__(self, message: str): + super().__init__(message) + self.message = message + + def request_wrapper(func): + @functools.wraps(func) def error_wrapper(*args, **kwargs): request_response = func(*args, **kwargs) if "return_type" in kwargs: @@ -216,6 +226,24 @@ def remove_utxo(self, utxo: UTxO): self.remove_txi(utxo.input) def submit_tx(self, tx: Transaction): + # Check before evaluation that requested ExUnits are valid. + requested_mem, requested_cpu = 0, 0 + for redeemer in tx.transaction_witness_set.redeemer or []: + if isinstance(redeemer, RedeemerKey): + redeemer = tx.transaction_witness_set.redeemer[redeemer] + if redeemer.ex_units.mem < 0 or redeemer.ex_units.steps < 0: + raise InvalidTransactionError( + f"Negative ExUnits not allowed: {str(redeemer.ex_units)}" + ) + requested_mem += redeemer.ex_units.mem + requested_cpu += redeemer.ex_units.steps + if ( + requested_mem > self.protocol_param.max_tx_ex_mem + or requested_cpu > self.protocol_param.max_tx_ex_steps + ): + raise InvalidTransactionError( + f"Invalid ExUnits: a total of {requested_mem} bytes and {requested_cpu} steps requested across all redeemers. Protocol requires less than {self.protocol_param.max_tx_ex_mem} bytes and {self.protocol_param.max_tx_ex_steps} steps per transaction." + ) self.evaluate_tx(tx) self.submit_tx_mock(tx) @@ -316,6 +344,8 @@ def evaluate_tx(self, tx: Transaction) -> Dict[str, ExecutionUnits]: tx, input_utxos, ref_input_utxos, lambda s: self.posix_from_slot(s) ) ret = {} + ex_units_steps_budget = self.protocol_param.max_tx_ex_steps + ex_units_mem_budget = self.protocol_param.max_tx_ex_mem for invocation in script_invocations: # run opshin script if available if self.opshin_scripts.get(invocation.script) is not None: @@ -325,8 +355,8 @@ def evaluate_tx(self, tx: Transaction) -> Dict[str, ExecutionUnits]: redeemer = invocation.redeemer if redeemer.ex_units.steps <= 0 and redeemer.ex_units.mem <= 0: redeemer.ex_units = ExecutionUnits( - self.protocol_param.max_tx_ex_mem, - self.protocol_param.max_tx_ex_steps, + ex_units_mem_budget, + ex_units_steps_budget, ) res, (cpu, mem), logs = evaluate_script(invocation) @@ -334,6 +364,8 @@ def evaluate_tx(self, tx: Transaction) -> Dict[str, ExecutionUnits]: raise ExecutionException( f"Error while evaluating script: {res}", logs=logs ) + ex_units_mem_budget -= mem + ex_units_steps_budget -= cpu key = f"{redeemer.tag.name.lower()}:{redeemer.index}" ret[key] = ExecutionUnits(mem, cpu) return ret @@ -381,6 +413,27 @@ def distribute_rewards(self, rewards: int): if account["registered_stake"] and delegation["pool_id"]: delegation["rewards"] += rewards + def __getstate__(self): + state = self.__dict__ + _utxo_state = state["_utxo_state"] + for key, value in _utxo_state.items(): + _utxo_state[key] = [x.to_cbor() for x in value] + _utxo_from_txid = state["_utxo_from_txid"] + for key, value in _utxo_from_txid.items(): + _utxo_from_txid[key] = {i: utxo.to_cbor() for i, utxo in value.items()} + return state + + def __setstate__(self, state): + _utxo_state = state["_utxo_state"] + for key, value in _utxo_state.items(): + _utxo_state[key] = [UTxO.from_cbor(x) for x in value] + _utxo_from_txid = state["_utxo_from_txid"] + for key, value in _utxo_from_txid.items(): + _utxo_from_txid[key] = { + i: UTxO.from_cbor(utxo) for i, utxo in value.items() + } + self.__dict__.update(state) + # These functions are supposed to overwrite the BlockFrost API @request_wrapper @@ -550,6 +603,11 @@ def address_utxos(self, address: str, **kwargs): @request_wrapper def transaction_submit_raw(self, tx_cbor: bytes, **kwargs): + # Prevent oversized transactions being submitted, this also efectively caps plutus script size + if len(tx_cbor) > self.protocol_param.max_tx_size: + raise InvalidTransactionError( + f"Transaction size ({len(tx_cbor)} bytes) exceeds protocol limit ({self.protocol_param.max_tx_size})" + ) tx = Transaction.from_cbor(tx_cbor) self.submit_tx(tx) return tx.id.payload.hex() @@ -562,6 +620,10 @@ def transaction_submit(self, file_path: str, **kwargs): @request_wrapper def transaction_evaluate_raw(self, tx_cbor: bytes, **kwargs): try: + if len(tx_cbor) > self.protocol_param.max_tx_size: + raise InvalidTransactionError( + f"Transaction size ({len(tx_cbor)} bytes) exceeds protocol limit ({self.protocol_param.max_tx_size})" + ) res = self.evaluate_tx_cbor(tx_cbor) except Exception as e: return { diff --git a/plutus_bench/mockfrost/client.py b/plutus_bench/mockfrost/client.py index 077b861..b479684 100644 --- a/plutus_bench/mockfrost/client.py +++ b/plutus_bench/mockfrost/client.py @@ -1,6 +1,7 @@ import uuid from dataclasses import dataclass from typing import Union +import functools import requests from pycardano.pool_params import PoolId @@ -19,6 +20,22 @@ PoolKeyHash, ) from blockfrost import BlockFrostApi +from blockfrost.utils import ApiError + + +class MockfrostApiError(ApiError): + pass + + +def client_request_wrapper(method): + @functools.wraps(method) + def wrapper(self, *args, **kwargs): + response = method(self, *args, **kwargs) + if response.status_code != 200: + raise MockfrostApiError(response) + return response.json() + + return wrapper @dataclass @@ -87,17 +104,21 @@ class MockFrostClient: def __post_init__(self): self.base_url = self.base_url.rstrip("/") + @client_request_wrapper def _get(self, path: str, **kwargs): - return self.session.get(self.base_url + path, **kwargs).json() + return self.session.get(self.base_url + path, **kwargs) + @client_request_wrapper def _post(self, path: str, **kwargs): - return self.session.post(self.base_url + path, **kwargs).json() + return self.session.post(self.base_url + path, **kwargs) + @client_request_wrapper def _put(self, path: str, **kwargs): - return self.session.put(self.base_url + path, **kwargs).json() + return self.session.put(self.base_url + path, **kwargs) + @client_request_wrapper def _del(self, path: str, **kwargs): - return self.session.delete(self.base_url + path, **kwargs).json() + return self.session.delete(self.base_url + path, **kwargs) def create_session( self, protocol_parameters=None, genesis_parameters=None diff --git a/plutus_bench/mockfrost/server.py b/plutus_bench/mockfrost/server.py index a4a3e9c..6e693c3 100644 --- a/plutus_bench/mockfrost/server.py +++ b/plutus_bench/mockfrost/server.py @@ -1,15 +1,22 @@ +import os import dataclasses import datetime import tempfile import uuid +from types import MappingProxyType import fastapi +from contextlib import asynccontextmanager +import asyncio import frozendict -from typing import Dict, Optional, Annotated +from typing import Dict, Optional, Annotated, Union from multiprocessing import Manager +from collections import OrderedDict +import functools import pycardano -from fastapi import FastAPI, Body +from fastapi import FastAPI, Body, Request, HTTPException +from fastapi.responses import JSONResponse from pycardano import ( ProtocolParameters, GenesisParameters, @@ -17,22 +24,273 @@ TransactionId, ) from pydantic import BaseModel +import sqlite3 +import pickle -from plutus_bench.mock import MockFrostApi +from plutus_bench.mock import MockFrostApi, InvalidTransactionError from plutus_bench.protocol_params import ( DEFAULT_PROTOCOL_PARAMETERS, DEFAULT_GENESIS_PARAMETERS, ) +from slowapi import Limiter, _rate_limit_exceeded_handler +from slowapi.util import get_remote_address +from slowapi.middleware import SlowAPIMiddleware +from slowapi.errors import RateLimitExceeded + +from dotenv import load_dotenv + +load_dotenv() + + +def deep_freeze(obj): + if isinstance(obj, dict): + return MappingProxyType({k: deep_freeze(v) for k, v in obj.items()}) + elif isinstance(obj, list): + return tuple(deep_freeze(i) for i in obj) + elif isinstance(obj, set): + return frozenset(deep_freeze(i) for i in obj) + return obj + + +class ModifiableChainstate: + def __init__( + self, + session_id: uuid.UUID, + session: "Session", + chain_state: MockFrostApi, + session_manager=None, + ): + self.session_manager = ( + session_manager if session_manager is not None else SessionManager() + ) + self.session_id = session_id + self.session = session + assert not isinstance(chain_state, ModifiableChainstate) + self.chain_state = chain_state + + def __getattr__(self, name): + attr = getattr(self.chain_state, name) + modifying_func = { + "set_block_slot", + "add_utxo", + "add_txout", + "remove_txi", + "remove_utxo", + "submit_tx", + "submit_tx_mock", + "submit_tx_cbor", + "wait", + "add_mock_pool", + "distribute_rewards", + "transaction_submit_raw", + "transaction_submit", + } + if callable(attr) and name in modifying_func: + + @functools.wraps(attr) + def wrapped(*args, **kwargs): + result = attr(*args, **kwargs) + self.session_manager[self.session_id] = self.session + return result + + return wrapped + else: + return deep_freeze(attr) + + def __setattr__(self, name, value): + if name not in {"session_manager", "session_id", "session", "chain_state"}: + self.chain_state.__setattr__(name, value) + self.session_manager[self.session_id] = self.session + else: + super().__setattr__(name, value) + @dataclasses.dataclass class Session: - chain_state: MockFrostApi + chain_state: Union[MockFrostApi, ModifiableChainstate] creation_time: datetime.datetime last_access_time: datetime.datetime + last_modify_time: datetime.datetime + + +SERVER_CONFIG = { + "max_session_lifespan": datetime.timedelta( + hours=int(os.getenv("MOCKFROST_MAX_SESSION_LIFESPAN", 24)) + ), + "max_idle_time": datetime.timedelta( + hours=int(os.getenv("MOCKFROST_MAX_SESSION_IDLE_TIME", 1)) + ), + "database_name": os.getenv("MOCKFROST_SESSION_DATABASE_NAME", "SESSIONS.db"), + "default_request_limit": os.getenv("MOCKFROST_IP_REQUEST_LIMIT", "3600/hour"), + "global_shared_session_limit": os.getenv( + "MOCKFROST_SHARED_SESSION_LIMIT", "24000/day" + ), + "ip_session_limit": os.getenv("MOCKFROST_IP_SESSION_LIMIT", "1000/day"), + "ip_transaction_limit": os.getenv("MOCKFROST_IP_TRANSACTION_LIMIT", "1000/hour"), +} + + +class SessionManager: + SESSIONS_CACHE = OrderedDict() + + def __init__(self, prefix="Session:", database_name=SERVER_CONFIG["database_name"]): + self.conn = sqlite3.connect(database_name) + self.conn.execute("PRAGMA journal_mode=WAL;") + self.cursor = self.conn.cursor() + self.cursor.execute( + """ + CREATE TABLE IF NOT EXISTS sessions ( + key TEXT PRIMARY KEY, + session BLOB NOT NULL, + creation_time TEXT NOT NULL, + last_access_time TEXT NOT NULL, + last_modify_time TEXT NOT NULL + ) + """ + ) + self.conn.commit() + + self.prefix = prefix + + def key(self, key: uuid.UUID) -> str: + return self.prefix + str(key) + + def unkey(self, full_key: str) -> uuid.UUID: + return uuid.UUID(full_key.removeprefix(self.prefix)) + + def cleanup(self) -> datetime.datetime: + """ + Clean up timed out sessions + Return time of next expiring session + """ + now = datetime.datetime.utcnow() + next_session = now + datetime.timedelta(seconds=600) + for key in self: + self.cursor.execute( + "SELECT last_access_time, creation_time FROM sessions WHERE key = ?", + (self.key(key),), + ) + last_access_time, creation_time = ( + datetime.datetime.fromisoformat(d) for d in self.cursor.fetchone() + ) + last_access_expire = last_access_time + SERVER_CONFIG["max_idle_time"] + creation_expire = creation_time + SERVER_CONFIG["max_session_lifespan"] + if now >= last_access_expire or now >= creation_expire: + print( + f"Session {key} has expired (last accessed: {last_access_time}, created: {creation_time})" + ) + del self[key] + self.SESSIONS_CACHE.pop(key, None) + else: + next_session = min(next_session, last_access_expire, creation_expire) + return next_session + + def __setitem__(self, key: uuid.UUID, value: Session): + if isinstance(value.chain_state, ModifiableChainstate): + value.chain_state = value.chain_state.chain_state + value.last_modify_time = datetime.datetime.utcnow() + assert not isinstance(value.chain_state, ModifiableChainstate) + self.cursor.execute("BEGIN IMMEDIATE") + self.cursor.execute( + "SELECT last_modify_time FROM sessions WHERE key = ?", (self.key(key),) + ) + row = self.cursor.fetchone() + timestamp = ( + datetime.datetime.fromisoformat(row[0]) + if row is not None + else datetime.datetime.min + ) + if timestamp <= value.last_modify_time: + self.cursor.execute( + """ + REPLACE INTO sessions ( + key, + session, + creation_time, + last_access_time, + last_modify_time + ) VALUES (?, ?, ?, ?, ?) + """, + ( + self.key(key), + pickle.dumps(value), + value.creation_time.isoformat(), + value.last_access_time.isoformat(), + value.last_modify_time.isoformat(), + ), + ) + self.conn.commit() + else: + self.conn.rollback() + raise RuntimeError( + f"Session for key {self.key(key)} has been modified since last access and will not be overwritten" + ) + self.SESSIONS_CACHE[key] = (value, value.last_modify_time) + self.SESSIONS_CACHE.move_to_end(key) + while len(self.SESSIONS_CACHE) > 128: + self.SESSIONS_CACHE.popitem(last=False) + + def __getitem__(self, key: uuid.UUID): + self.cursor.execute( + "SELECT last_modify_time FROM sessions WHERE key = ?", (self.key(key),) + ) + row = self.cursor.fetchone() + if not row: + raise KeyError(f"Could not find {self.key(key)} in Session Database") + timestamp = ( + datetime.datetime.fromisoformat(row[0]) + if row is not None + else datetime.datetime.min + ) + if key in self.SESSIONS_CACHE and self.SESSIONS_CACHE[key][0] == timestamp: + session = self.SESSIONS_CACHE[key][1] + self.SESSIONS_CACHE.move_to_end(key) + else: + self.cursor.execute( + "SELECT session FROM sessions WHERE key = ?", (self.key(key),) + ) + row = self.cursor.fetchone() + session = pickle.loads(row[0]) + session.last_access_time = datetime.datetime.utcnow() + if not isinstance(session.chain_state, ModifiableChainstate): + session.chain_state = ModifiableChainstate( + key, session, session.chain_state, self + ) + self.SESSIONS_CACHE[key] = (session.last_modify_time, session) + while len(self.SESSIONS_CACHE) > 128: + self.SESSIONS_CACHE.popitem(last=False) + self.cursor.execute("BEGIN IMMEDIATE") + self.cursor.execute( + """ + UPDATE sessions + SET last_access_time = ? + WHERE key = ? + """, + (session.last_access_time.isoformat(), self.key(key)), + ) + self.conn.commit() + return session -SESSIONS: Dict[uuid.UUID, "Session"] = {} + def __delitem__(self, key: uuid.UUID): + self.cursor.execute("DELETE FROM sessions WHERE key = ?", (self.key(key),)) + self.conn.commit() + self.SESSIONS_CACHE.pop(key, None) + + def __iter__(self): + self.cursor.execute("SELECT key FROM sessions") + for row in self.cursor.fetchall(): + yield self.unkey(row[0]) + + def __len__(self): + return len([i for i in self]) + + def __contains__(self, key: uuid.UUID) -> bool: + self.cursor.execute( + "SELECT 1 FROM sessions WHERE key = ? LIMIT 1", (self.key(key),) + ) + return self.cursor.fetchone() is not None class SessionModel(BaseModel): @@ -45,6 +303,27 @@ class TransactionInputModel(BaseModel): output_index: int +@asynccontextmanager +async def lifespan(app: FastAPI): + # start up logic + async def cleanup(): + while True: + sessions = SessionManager() + next_expiring_session = sessions.cleanup() + delay = (next_expiring_session - datetime.datetime.utcnow()).total_seconds() + await asyncio.sleep(delay) + + asyncio.create_task(cleanup()) + + yield + # Shutdown logic + + +# User Rate Limiter +limiter = Limiter( + key_func=get_remote_address, default_limits=[SERVER_CONFIG["default_request_limit"]] +) + app = FastAPI( title="MockFrost API", summary="A clone of the important parts of the BlockFrost API which are used to evaluate transactions. Create your own mocked environment and execute transactions in it.", @@ -64,17 +343,42 @@ class TransactionInputModel(BaseModel): - [Swagger UI](/docs): A more interactive documentation with a UI. - [Redoc](/redoc): A more static documentation with a focus on readability. """, + lifespan=lifespan, ) from fastapi.responses import RedirectResponse +@app.exception_handler(RateLimitExceeded) +async def custom_rate_limit_handler(request: Request, exc: RateLimitExceeded): + return JSONResponse( + status_code=429, + content={ + "status_code": 422, + "error": "Too many requests", + "message": f"Please wait before making more requests. retry_after: {exc.detail}", # or exc.headers.get("Retry-After") + }, + headers=exc.headers, # Important to keep headers for clients to back off + ) + + +app.state.limiter = limiter +app.state.SERVER_CONFIG = SERVER_CONFIG +app.add_exception_handler(RateLimitExceeded, custom_rate_limit_handler) +app.add_middleware(SlowAPIMiddleware) + + @app.get("/", response_class=RedirectResponse, include_in_schema=False) async def redirect_fastapi(): return "/docs" @app.post("/session") +@limiter.limit(SERVER_CONFIG["ip_session_limit"]) +@limiter.limit( + SERVER_CONFIG["global_shared_session_limit"], key_func=lambda: "shared_key" +) def create_session( + request: Request, seed: int = 0, protocol_parameters: dict = dataclasses.asdict(DEFAULT_PROTOCOL_PARAMETERS), genesis_parameters: dict = dataclasses.asdict(DEFAULT_GENESIS_PARAMETERS), @@ -90,24 +394,27 @@ def create_session( DEFAULT_GENESIS_PARAMETERS ) session_id = uuid.uuid4() + now = datetime.datetime.utcnow() + SESSIONS = SessionManager() SESSIONS[session_id] = Session( chain_state=MockFrostApi( protocol_param=ProtocolParameters(**protocol_parameters), genesis_param=GenesisParameters(**genesis_parameters), seed=seed, ), - creation_time=datetime.datetime.now(), - last_access_time=datetime.datetime.now(), + creation_time=now, + last_access_time=now, + last_modify_time=now, ) return session_id @app.get("/session/{session_id}") -def get_session_info(session_id: uuid.UUID) -> Optional[SessionModel]: +def get_session_info(request: Request, session_id: uuid.UUID) -> Optional[SessionModel]: """ Remove a session after usage. """ - session = SESSIONS.get(session_id) + session = get_session(session_id) if not session: return None return SessionModel( @@ -117,10 +424,11 @@ def get_session_info(session_id: uuid.UUID) -> Optional[SessionModel]: @app.delete("/session/{session_id}") -def delete_session(session_id: uuid.UUID) -> bool: +def delete_session(request: Request, session_id: uuid.UUID) -> bool: """ Remove a session after usage. """ + SESSIONS = SessionManager() if session_id in SESSIONS: del SESSIONS[session_id] return True @@ -134,6 +442,7 @@ def model_from_transaction_input(tx_in: TransactionInput): def get_session(session_id): + SESSIONS = SessionManager() if session_id not in SESSIONS: raise fastapi.HTTPException(status_code=404, detail="Session not found") return SESSIONS[session_id] @@ -141,7 +450,7 @@ def get_session(session_id): @app.post("/{session_id}/ledger/txo") def add_transaction_output( - session_id: uuid.UUID, tx_cbor: Annotated[str, Body(embed=True)] + request: Request, session_id: uuid.UUID, tx_cbor: Annotated[str, Body(embed=True)] ) -> TransactionInputModel: """ Add a transaction output to the UTxO, without specifying the transaction hash and index (the "input"). @@ -154,7 +463,9 @@ def add_transaction_output( @app.put("/{session_id}/ledger/utxo") -def add_utxo(session_id: uuid.UUID, tx_cbor: bytes) -> TransactionInputModel: +def add_utxo( + request: Request, session_id: uuid.UUID, tx_cbor: bytes +) -> TransactionInputModel: """ Add a transaction output and input to the UTxO. Potentially overwrites existing inputs with the same transaction hash and index. @@ -167,7 +478,7 @@ def add_utxo(session_id: uuid.UUID, tx_cbor: bytes) -> TransactionInputModel: @app.delete("/{session_id}/ledger/txo") def delete_transaction_output( - session_id: uuid.UUID, tx_input: TransactionInputModel + request: Request, session_id: uuid.UUID, tx_input: TransactionInputModel ) -> bool: """ Delete a transaction output from the UTxO. @@ -185,7 +496,7 @@ def delete_transaction_output( @app.put("/{session_id}/ledger/slot") -def set_slot(session_id: uuid.UUID, slot: int) -> int: +def set_slot(request: Request, session_id: uuid.UUID, slot: int) -> int: """ Set the current slot of the ledger to a specified value. Essentially acts as a "time travel" tool. @@ -195,7 +506,9 @@ def set_slot(session_id: uuid.UUID, slot: int) -> int: @app.put("/{session_id}/pools/pool") -def add_pool(session_id: uuid.UUID, pool_id: Annotated[str, Body(embed=True)]) -> str: +def add_pool( + request: Request, session_id: uuid.UUID, pool_id: Annotated[str, Body(embed=True)] +) -> str: """ Add a fake staking pool. This may be delegated to mimic rewards. """ @@ -204,7 +517,7 @@ def add_pool(session_id: uuid.UUID, pool_id: Annotated[str, Body(embed=True)]) - @app.put("/{session_id}/pools/distribute") -def distribute_rewards(session_id: uuid.UUID, rewards: int) -> int: +def distribute_rewards(request: Request, session_id: uuid.UUID, rewards: int) -> int: """ Distributed rewards to staked accounts. Emulates the behaviour of reward distribution at epoch boundaries. """ @@ -213,7 +526,7 @@ def distribute_rewards(session_id: uuid.UUID, rewards: int) -> int: @app.get("/{session_id}/api/v0/epochs/latest") -def latest_epoch(session_id: uuid.UUID) -> dict: +def latest_epoch(request: Request, session_id: uuid.UUID) -> dict: """ Return the information about the latest, therefore current, epoch. @@ -224,7 +537,7 @@ def latest_epoch(session_id: uuid.UUID) -> dict: @app.get("/{session_id}/api/v0/blocks/latest") -def latest_block(session_id: uuid.UUID) -> dict: +def latest_block(request: Request, session_id: uuid.UUID) -> dict: """ Return the latest block available to the backends, also known as the tip of the blockchain. @@ -234,7 +547,7 @@ def latest_block(session_id: uuid.UUID) -> dict: @app.get("/{session_id}/api/v0/genesis") -def genesis(session_id: uuid.UUID) -> dict: +def genesis(request: Request, session_id: uuid.UUID) -> dict: """ Return the information about blockchain genesis. @@ -244,7 +557,7 @@ def genesis(session_id: uuid.UUID) -> dict: @app.get("/{session_id}/api/v0/epochs/latest/parameters") -def latest_epoch_protocol_parameters(session_id: uuid.UUID) -> dict: +def latest_epoch_protocol_parameters(request: Request, session_id: uuid.UUID) -> dict: """ Return the protocol parameters for the latest epoch. @@ -256,7 +569,7 @@ def latest_epoch_protocol_parameters(session_id: uuid.UUID) -> dict: @app.get("/{session_id}/api/v0/scripts/{script_hash}") -def specific_script(session_id: uuid.UUID, script_hash: str) -> dict: +def specific_script(request: Request, session_id: uuid.UUID, script_hash: str) -> dict: """ Information about a specific script @@ -268,7 +581,7 @@ def specific_script(session_id: uuid.UUID, script_hash: str) -> dict: @app.get("/{session_id}/api/v0/scripts/{script_hash}/cbor") -def script_cbor(session_id: uuid.UUID, script_hash: str) -> dict: +def script_cbor(request: Request, session_id: uuid.UUID, script_hash: str) -> dict: """ CBOR representation of a `plutus` script @@ -280,7 +593,7 @@ def script_cbor(session_id: uuid.UUID, script_hash: str) -> dict: @app.get("/{session_id}/api/v0/scripts/{script_hash}/json") -def script_json(session_id: uuid.UUID, script_hash: str) -> dict: +def script_json(request: Request, session_id: uuid.UUID, script_hash: str) -> dict: """ JSON representation of a `timelock` script @@ -292,7 +605,7 @@ def script_json(session_id: uuid.UUID, script_hash: str) -> dict: @app.get("/{session_id}/api/v0/addresses/{address}/utxos") -def address_utxos(session_id: uuid.UUID, address: str) -> list: +def address_utxos(request: Request, session_id: uuid.UUID, address: str) -> list: """ UTXOs of the address. @@ -304,7 +617,9 @@ def address_utxos(session_id: uuid.UUID, address: str) -> list: @app.post("/{session_id}/api/v0/tx/submit") +@limiter.limit(SERVER_CONFIG["ip_transaction_limit"]) def submit_a_transaction( + request: Request, session_id: uuid.UUID, transaction: Annotated[bytes, Body(media_type="application/cbor")], ) -> str: @@ -313,13 +628,26 @@ def submit_a_transaction( https://docs.blockfrost.io/#tag/Cardano-Transactions/paths/~1tx~1submit/post """ - return get_session(session_id).chain_state.transaction_submit_raw( - transaction, return_type="json" - ) + try: + return get_session(session_id).chain_state.transaction_submit_raw( + transaction, return_type="json" + ) + except InvalidTransactionError as e: + return JSONResponse( + status_code=422, + content={ + "status_code": 422, + "message": str(e), + "error": "Invalid Transaction.", + }, + ) + # raise HTTPException(status_code = 422, detail = {"message": str(e)}) @app.post("/{session_id}/api/v0/utils/txs/evaluate") +@limiter.limit(SERVER_CONFIG["ip_transaction_limit"]) def submit_a_transaction_for_execution_units_evaluation( + request: Request, session_id: uuid.UUID, transaction: Annotated[str, Body(media_type="application/cbor")], ) -> dict: @@ -334,7 +662,9 @@ def submit_a_transaction_for_execution_units_evaluation( @app.get("/{session_id}/api/v0/accounts/{stake_address}") -def specific_account_address(session_id: uuid.UUID, stake_address: str) -> dict: +def specific_account_address( + request: Request, session_id: uuid.UUID, stake_address: str +) -> dict: """ Obtain information about a specific stake account diff --git a/plutus_bench/tx_tools.py b/plutus_bench/tx_tools.py index 3b14353..ae536b3 100644 --- a/plutus_bench/tx_tools.py +++ b/plutus_bench/tx_tools.py @@ -1,4 +1,4 @@ -from functools import cache +from functools import lru_cache from typing import Optional, Union, List from dataclasses import dataclass @@ -334,7 +334,7 @@ def generate_script_contexts_resolved( return script_contexts -@cache +@lru_cache(maxsize=1000) def uplc_unflat(script: bytes): return uplc.unflatten(script) diff --git a/poetry.lock b/poetry.lock index 156c737..15d5e7c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "annotated-types" @@ -6,6 +6,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -17,6 +18,7 @@ version = "4.6.2.post1" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, @@ -30,7 +32,7 @@ typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21.0b1) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""] trio = ["trio (>=0.26.1)"] [[package]] @@ -39,6 +41,7 @@ version = "1.4.4" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = "*" +groups = ["main", "dev"] files = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, @@ -50,6 +53,7 @@ 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" optional = false python-versions = "*" +groups = ["main", "dev"] files = [ {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, @@ -61,18 +65,19 @@ version = "24.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, ] [package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\""] [[package]] name = "black" @@ -80,6 +85,7 @@ version = "24.10.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, @@ -126,6 +132,7 @@ version = "0.6.0" description = "The official Python SDK for Blockfrost API v0.1.37" optional = false python-versions = "<4,>=3.7" +groups = ["main", "dev"] files = [ {file = "blockfrost_python-0.6.0-py3-none-any.whl", hash = "sha256:c88840b8034b30dc06c637ccd14806e472d830d63522d2a667d9263640a354f4"}, {file = "blockfrost_python-0.6.0.tar.gz", hash = "sha256:764b795617aadfd712b2a214fa6bd26cca33f0008340e0225126d18be040b112"}, @@ -140,6 +147,7 @@ version = "5.5.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, @@ -151,6 +159,7 @@ version = "2.1.0" description = "A collection of tools to enable development in the Cardano ecosystem using the Python programming language." optional = false python-versions = ">=3.8,<4.0" +groups = ["main", "dev"] files = [ {file = "cardano_tools-2.1.0-py3-none-any.whl", hash = "sha256:c562c234b3d9a51540d41432f88aeae9a68c9441e0ea363c675dd712380ca06c"}, {file = "cardano_tools-2.1.0.tar.gz", hash = "sha256:445c8a5c769f57e5e04494ac4e3012082c3d0f1bd9a9eaed7f834d37ad7a069e"}, @@ -166,6 +175,7 @@ version = "5.6.5" description = "CBOR (de)serializer with extensive tag support" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "cbor2-5.6.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e16c4a87fc999b4926f5c8f6c696b0d251b4745bc40f6c5aee51d69b30b15ca2"}, {file = "cbor2-5.6.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87026fc838370d69f23ed8572939bd71cea2b3f6c8f8bb8283f573374b4d7f33"}, @@ -215,7 +225,7 @@ files = [ [package.extras] benchmarks = ["pytest-benchmark (==4.0.0)"] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.3.0)", "typing-extensions"] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.3.0)", "typing-extensions ; python_version < \"3.12\""] test = ["coverage (>=7)", "hypothesis", "pytest"] [[package]] @@ -224,6 +234,7 @@ version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, @@ -235,6 +246,7 @@ version = "0.11.1" description = "Validates X.509 certificates and paths" optional = false python-versions = "*" +groups = ["main", "dev"] files = [ {file = "certvalidator-0.11.1-py2.py3-none-any.whl", hash = "sha256:77520b269f516d4fb0902998d5bd0eb3727fe153b659aa1cb828dcf12ea6b8de"}, {file = "certvalidator-0.11.1.tar.gz", hash = "sha256:922d141c94393ab285ca34338e18dd4093e3ae330b1f278e96c837cb62cffaad"}, @@ -250,6 +262,7 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -329,6 +342,7 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -340,6 +354,7 @@ version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" +groups = ["main", "dev"] files = [ {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, @@ -454,6 +469,7 @@ version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, @@ -468,10 +484,12 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "coloredlogs" @@ -479,6 +497,7 @@ version = "15.0.1" description = "Colored terminal output for Python's logging module" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main", "dev"] files = [ {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, @@ -496,6 +515,7 @@ version = "0.9.dev8" description = "CBOR Object Signing and Encryption (COSE) implementation" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "cose-0.9.dev8-py3-none-any.whl", hash = "sha256:f1c3be98e50724e846e3a1d23efe19a150665a4f24917ac8bfbc8e5abb31ccb0"}, {file = "cose-0.9.dev8.tar.gz", hash = "sha256:c48d1edcf7fbc564f4f4ac9d0daa52378ea9d26216e5c4bf4b324883ae5ef880"}, @@ -514,6 +534,7 @@ version = "7.6.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"}, {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"}, @@ -583,7 +604,7 @@ files = [ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "coveralls" @@ -591,6 +612,7 @@ version = "4.0.1" description = "Show coverage stats online via coveralls.io" optional = false python-versions = "<3.13,>=3.8" +groups = ["dev"] files = [ {file = "coveralls-4.0.1-py3-none-any.whl", hash = "sha256:7a6b1fa9848332c7b2221afb20f3df90272ac0167060f41b5fe90429b30b1809"}, {file = "coveralls-4.0.1.tar.gz", hash = "sha256:7b2a0a2bcef94f295e3cf28dcc55ca40b71c77d1c2446b538e85f0f7bc21aa69"}, @@ -610,6 +632,7 @@ version = "43.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, @@ -653,12 +676,31 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] +[[package]] +name = "deprecated" +version = "1.2.18" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +groups = ["main"] +files = [ + {file = "Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec"}, + {file = "deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"] + [[package]] name = "distlib" version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, @@ -670,6 +712,7 @@ version = "7.1.0" description = "A Python library for the Docker Engine API." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"}, {file = "docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c"}, @@ -692,6 +735,7 @@ version = "0.6.2" description = "Pythonic argument parser, that will make you smile" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, ] @@ -702,6 +746,7 @@ version = "0.19.0" description = "ECDSA cryptographic signature library (pure python)" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.6" +groups = ["main", "dev"] files = [ {file = "ecdsa-0.19.0-py2.py3-none-any.whl", hash = "sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a"}, {file = "ecdsa-0.19.0.tar.gz", hash = "sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8"}, @@ -720,6 +765,7 @@ version = "1.2.5" description = "Pure Pyhton Elliptic Curve Library" optional = false python-versions = "*" +groups = ["main", "dev"] files = [ {file = "ECPy-1.2.5-py3-none-any.whl", hash = "sha256:559c92e42406d9d1a6b2b8fc26e6ad7bc985f33903b72f426a56cb1073a25ce3"}, {file = "ECPy-1.2.5.tar.gz", hash = "sha256:9635cffb9b6ecf7fd7f72aea1665829ac74a1d272006d0057d45a621aae20228"}, @@ -731,6 +777,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -745,6 +793,7 @@ version = "0.110.3" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "fastapi-0.110.3-py3-none-any.whl", hash = "sha256:fd7600612f755e4050beb74001310b5a7e1796d149c2ee363124abdfa0289d32"}, {file = "fastapi-0.110.3.tar.gz", hash = "sha256:555700b0159379e94fdbfc6bb66a0f1c43f4cf7060f25239af3d84b63a656626"}, @@ -764,6 +813,7 @@ version = "3.16.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, @@ -772,7 +822,7 @@ files = [ [package.extras] docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] -typing = ["typing-extensions (>=4.12.2)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "frozendict" @@ -780,6 +830,7 @@ version = "2.4.6" description = "A simple immutable dictionary" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "frozendict-2.4.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3a05c0a50cab96b4bb0ea25aa752efbfceed5ccb24c007612bc63e51299336f"}, {file = "frozendict-2.4.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f5b94d5b07c00986f9e37a38dd83c13f5fe3bf3f1ccc8e88edea8fe15d6cd88c"}, @@ -828,6 +879,7 @@ version = "1.5.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, @@ -929,6 +981,7 @@ version = "1.0.0" description = "An immutable list" optional = false python-versions = "*" +groups = ["main", "dev"] files = [ {file = "frozenlist2-1.0.0-py3-none-any.whl", hash = "sha256:caffe66813e42de320b10d08b8c0604c7eb3f142a8482ad3130243e084f37a2f"}, {file = "frozenlist2-1.0.0.tar.gz", hash = "sha256:33f6c6bb2c7d38524ec3c2d6f2d8a3ee2625a9e13096d8bc64db012b516a95e0"}, @@ -940,6 +993,7 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -951,6 +1005,7 @@ version = "1.0.6" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f"}, {file = "httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"}, @@ -972,6 +1027,7 @@ version = "0.27.2" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, @@ -985,7 +1041,7 @@ idna = "*" sniffio = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -997,6 +1053,7 @@ version = "10.0" description = "Human friendly output for text interfaces using Python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main", "dev"] files = [ {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, @@ -1011,6 +1068,7 @@ version = "6.118.0" description = "A library for property-based testing" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "hypothesis-6.118.0-py3-none-any.whl", hash = "sha256:40e27343570cbb65d14a4d6da5ee38286995100d4fb93d4b8038ba3669e240e5"}, {file = "hypothesis-6.118.0.tar.gz", hash = "sha256:5568bae62a2b29c92e579589befa7773f685e3ca76ca4b9ec0b2e356dbf8541e"}, @@ -1022,7 +1080,7 @@ exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""} sortedcontainers = ">=2.1.0,<3.0.0" [package.extras] -all = ["black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.74)", "django (>=4.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.16)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.19.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.2)"] +all = ["black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.74)", "django (>=4.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.16)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.19.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.2) ; sys_platform == \"win32\" or sys_platform == \"emscripten\""] cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] codemods = ["libcst (>=0.3.16)"] crosshair = ["crosshair-tool (>=0.0.74)", "hypothesis-crosshair (>=0.0.16)"] @@ -1036,7 +1094,7 @@ pandas = ["pandas (>=1.1)"] pytest = ["pytest (>=4.6)"] pytz = ["pytz (>=2014.1)"] redis = ["redis (>=3.0.0)"] -zoneinfo = ["tzdata (>=2024.2)"] +zoneinfo = ["tzdata (>=2024.2) ; sys_platform == \"win32\" or sys_platform == \"emscripten\""] [[package]] name = "identify" @@ -1044,6 +1102,7 @@ version = "2.6.1" description = "File identification library for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, @@ -1058,6 +1117,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -1072,17 +1132,48 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "limits" +version = "5.2.0" +description = "Rate limiting utilities" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "limits-5.2.0-py3-none-any.whl", hash = "sha256:e4e2cf8ccca090d2276e1c60352658c1c498e1756927272abc6ce5bfdbcc02cc"}, + {file = "limits-5.2.0.tar.gz", hash = "sha256:b6b659774f17befef2dd30a76dcd2bdecf3852e73b6627143d44ab4deda94b48"}, +] + +[package.dependencies] +deprecated = ">=1.2" +packaging = ">=21,<26" +typing_extensions = "*" + +[package.extras] +all = ["coredis (>=3.4.0,<5)", "memcachio (>=0.3)", "motor (>=3,<4)", "pymemcache (>3,<5.0.0)", "pymongo (>4.1,<5)", "redis (>3,!=4.5.2,!=4.5.3,<6.0.0)", "redis (>=4.2.0,!=4.5.2,!=4.5.3)", "valkey (>=6)", "valkey (>=6)"] +async-memcached = ["memcachio (>=0.3)"] +async-mongodb = ["motor (>=3,<4)"] +async-redis = ["coredis (>=3.4.0,<5)"] +async-valkey = ["valkey (>=6)"] +memcached = ["pymemcache (>3,<5.0.0)"] +mongodb = ["pymongo (>4.1,<5)"] +redis = ["redis (>3,!=4.5.2,!=4.5.3,<6.0.0)"] +rediscluster = ["redis (>=4.2.0,!=4.5.2,!=4.5.3)"] +valkey = ["valkey (>=6)"] + [[package]] name = "mnemonic" version = "0.21" description = "Implementation of Bitcoin BIP-0039" optional = false python-versions = ">=3.8.1" +groups = ["main", "dev"] files = [ {file = "mnemonic-0.21-py3-none-any.whl", hash = "sha256:72dc9de16ec5ef47287237b9b6943da11647a03fe7cf1f139fc3d7c4a7439288"}, {file = "mnemonic-0.21.tar.gz", hash = "sha256:1fe496356820984f45559b1540c80ff10de448368929b9c60a2b55744cc88acf"}, @@ -1094,6 +1185,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -1105,6 +1197,7 @@ version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, @@ -1116,6 +1209,7 @@ version = "1.2.1" description = "Ogmios is a lightweight bridge interface for cardano-node. It offers a WebSockets API that enables local clients to speak Ouroboros' mini-protocols via JSON/RPC. ogmios-python is an Ogmios client written in Python designed for ease of use." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "ogmios-1.2.1-py3-none-any.whl", hash = "sha256:a0e1d87f9fd7b224556b30349d8abce2fcff3ce1c9a92e292f9a5c13f1e140ce"}, {file = "ogmios-1.2.1.tar.gz", hash = "sha256:07d7086ca8ddb4ca38e9c61edda53ff4acc0d156d15ea056eac11cd05390d9ec"}, @@ -1140,6 +1234,7 @@ version = "0.23.1" description = "A simple pythonic programming language for Smart Contracts on Cardano" optional = false python-versions = "<3.12,>=3.8.1" +groups = ["dev"] files = [ {file = "opshin-0.23.1-py3-none-any.whl", hash = "sha256:e3a2eb592383f13518ed0262bb42204ef88edbdc403687576582a433422fb3be"}, {file = "opshin-0.23.1.tar.gz", hash = "sha256:19b4ba6b3a19d5b3087622850243520967258a340e59652b0c38b4ce93bd8bd9"}, @@ -1158,6 +1253,7 @@ version = "4.1.0" description = "An OrderedSet is a custom MutableSet that remembers its order, so that every" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"}, {file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"}, @@ -1172,6 +1268,7 @@ version = "3.10.11" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "orjson-3.10.11-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6dade64687f2bd7c090281652fe18f1151292d567a9302b34c2dbb92a3872f1f"}, {file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82f07c550a6ccd2b9290849b22316a609023ed851a87ea888c0456485a7d196a"}, @@ -1239,6 +1336,7 @@ version = "1.3.0" description = "TLS (SSL) sockets, key generation, encryption, decryption, signing, verification and KDFs using the OS crypto libraries. Does not require a compiler, and relies on the OS for patching. Works on Windows, OS X and Linux/BSD." optional = false python-versions = "*" +groups = ["main", "dev"] files = [ {file = "oscrypto-1.3.0-py2.py3-none-any.whl", hash = "sha256:2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085"}, {file = "oscrypto-1.3.0.tar.gz", hash = "sha256:6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4"}, @@ -1253,6 +1351,7 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -1264,6 +1363,7 @@ version = "0.9.0" description = "Parameterized testing with any Python test framework" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "parameterized-0.9.0-py2.py3-none-any.whl", hash = "sha256:4e0758e3d41bea3bbd05ec14fc2c24736723f243b28d702081aef438c9372b1b"}, {file = "parameterized-0.9.0.tar.gz", hash = "sha256:7fc905272cefa4f364c1a3429cbbe9c0f98b793988efb5bf90aac80f08db09b1"}, @@ -1278,6 +1378,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -1289,6 +1390,7 @@ version = "4.9.0" description = "Pexpect allows easy control of interactive console applications." optional = false python-versions = "*" +groups = ["main", "dev"] files = [ {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, @@ -1303,6 +1405,7 @@ version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -1319,6 +1422,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1334,6 +1438,7 @@ version = "1.0.0" description = "Pluto-like programming language for Cardano Smart Contracts in Python" optional = false python-versions = ">=3" +groups = ["dev"] files = [ {file = "pluthon-1.0.0-py3-none-any.whl", hash = "sha256:cb0bfef3b56f62551c7ccbb26a13ac9aaacb88faaa07351312739b0722d217ef"}, {file = "pluthon-1.0.0.tar.gz", hash = "sha256:57ff4e8d24adc24d33098324e984f9abb723a4847e38e7c79d7a96ce6dd434b2"}, @@ -1349,6 +1454,7 @@ version = "0.4.0" description = "A drop-in replacement for pprint that's actually pretty" optional = false python-versions = "*" +groups = ["main", "dev"] files = [ {file = "pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d"}, {file = "pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403"}, @@ -1360,6 +1466,7 @@ version = "3.8.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, @@ -1378,6 +1485,7 @@ version = "0.7.0" description = "Run a subprocess in a pseudo terminal" optional = false python-versions = "*" +groups = ["main", "dev"] files = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, @@ -1389,6 +1497,7 @@ version = "0.12.0" description = "A Cardano library in Python" optional = false python-versions = "^3.8.1" +groups = ["main", "dev"] files = [] develop = false @@ -1422,6 +1531,7 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -1433,6 +1543,7 @@ version = "2.9.2" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, @@ -1445,7 +1556,7 @@ typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and sys_platform == \"win32\""] [[package]] name = "pydantic-core" @@ -1453,6 +1564,7 @@ version = "2.23.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, @@ -1554,6 +1666,7 @@ version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, @@ -1580,6 +1693,8 @@ version = "3.5.4" description = "A python implementation of GNU readline." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, @@ -1594,6 +1709,7 @@ version = "8.3.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, @@ -1610,12 +1726,28 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "python-dotenv" +version = "1.1.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, + {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "python-secp256k1-cardano" version = "0.2.3" description = "Ctypes Python3 FFI bindings for libsecp256k1 at commit hash ac83be33" optional = false python-versions = "*" +groups = ["main", "dev"] files = [ {file = "python-secp256k1-cardano-0.2.3.tar.gz", hash = "sha256:429b33cc1815c9942f3e6e4861d03aa9dbd73dbb7cc3dbc818e121f41d9f4c39"}, {file = "python_secp256k1_cardano-0.2.3-py3-none-any.whl", hash = "sha256:b4c664a007eb339c509615ad54bd2ee05564879f17b0fef9a5cc619aedf3bfb8"}, @@ -1630,6 +1762,8 @@ version = "308" description = "Python for Window Extensions" optional = false python-versions = "*" +groups = ["main", "dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, @@ -1657,6 +1791,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -1719,6 +1854,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -1740,6 +1876,7 @@ version = "0.7.8" description = "A pure Python Lex/Yacc that works with RPython" optional = false python-versions = "*" +groups = ["main", "dev"] files = [ {file = "rply-0.7.8-py2.py3-none-any.whl", hash = "sha256:28ffd11d656c48aeb8c508eb382acd6a0bd906662624b34388751732a27807e7"}, {file = "rply-0.7.8.tar.gz", hash = "sha256:2a808ac25a4580a9991fc304d64434e299a8fc75760574492f242cbb5bb301c9"}, @@ -1754,19 +1891,20 @@ version = "75.3.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd"}, {file = "setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.5.2) ; sys_platform != \"cygwin\""] +core = ["importlib-metadata (>=6) ; python_version < \"3.10\"", "importlib-resources (>=5.10.2) ; python_version < \"3.9\"", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.12.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.12.*)", "pytest-mypy"] [[package]] name = "six" @@ -1774,17 +1912,37 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main", "dev"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "slowapi" +version = "0.1.9" +description = "A rate limiting extension for Starlette and Fastapi" +optional = false +python-versions = ">=3.7,<4.0" +groups = ["main"] +files = [ + {file = "slowapi-0.1.9-py3-none-any.whl", hash = "sha256:cfad116cfb84ad9d763ee155c1e5c5cbf00b0d47399a769b227865f5df576e36"}, + {file = "slowapi-0.1.9.tar.gz", hash = "sha256:639192d0f1ca01b1c6d95bf6c71d794c3a9ee189855337b4821f7f457dddad77"}, +] + +[package.dependencies] +limits = ">=2.3" + +[package.extras] +redis = ["redis (>=3.4.1,<4.0.0)"] + [[package]] name = "sniffio" version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -1796,6 +1954,7 @@ version = "2.4.0" description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, @@ -1807,6 +1966,7 @@ version = "0.37.2" description = "The little ASGI library that shines." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, @@ -1824,6 +1984,8 @@ version = "2.0.2" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_full_version <= \"3.11.0a6\"" files = [ {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, @@ -1835,6 +1997,7 @@ version = "4.4.1" description = "Run-time type checker for Python" optional = false python-versions = ">=3.9" +groups = ["main", "dev"] files = [ {file = "typeguard-4.4.1-py3-none-any.whl", hash = "sha256:9324ec07a27ec67fc54a9c063020ca4c0ae6abad5e9f0f9804ca59aee68c6e21"}, {file = "typeguard-4.4.1.tar.gz", hash = "sha256:0d22a89d00b453b47c49875f42b6601b961757541a2e1e0ef517b6e24213c21b"}, @@ -1845,7 +2008,7 @@ typing-extensions = ">=4.10.0" [package.extras] doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.3.0)"] -test = ["coverage[toml] (>=7)", "mypy (>=1.2.0)", "pytest (>=7)"] +test = ["coverage[toml] (>=7)", "mypy (>=1.2.0) ; platform_python_implementation != \"PyPy\"", "pytest (>=7)"] [[package]] name = "typing-extensions" @@ -1853,6 +2016,7 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -1864,6 +2028,7 @@ version = "1.0.7" description = "Python implementation of untyped plutus language core" optional = false python-versions = "<3.12,>=3.8" +groups = ["main", "dev"] files = [ {file = "uplc-1.0.7-py3-none-any.whl", hash = "sha256:ad6f08473c5b7bf9d2c9e1d083bcafe3bfd5eac6670f826ed6cd77c535b7e718"}, {file = "uplc-1.0.7.tar.gz", hash = "sha256:63b51e0ccbf87f18a1d56ae53fae4312bc989b73f20cc82ec22f2ad06bd14be0"}, @@ -1883,13 +2048,14 @@ version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -1900,6 +2066,7 @@ version = "0.29.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, {file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, @@ -1911,7 +2078,7 @@ h11 = ">=0.8" typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "virtualenv" @@ -1919,6 +2086,7 @@ version = "20.27.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, @@ -1931,7 +2099,7 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "websocket-client" @@ -1939,6 +2107,7 @@ version = "1.8.0" description = "WebSocket client for Python with low level API options" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, @@ -1955,6 +2124,7 @@ version = "13.1" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee"}, {file = "websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7"}, @@ -2044,7 +2214,96 @@ files = [ {file = "websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878"}, ] +[[package]] +name = "wrapt" +version = "1.17.2" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62"}, + {file = "wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563"}, + {file = "wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72"}, + {file = "wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317"}, + {file = "wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9"}, + {file = "wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9"}, + {file = "wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504"}, + {file = "wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a"}, + {file = "wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f"}, + {file = "wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555"}, + {file = "wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c803c401ea1c1c18de70a06a6f79fcc9c5acfc79133e9869e730ad7f8ad8ef9"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f917c1180fdb8623c2b75a99192f4025e412597c50b2ac870f156de8fb101119"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ecc840861360ba9d176d413a5489b9a0aff6d6303d7e733e2c4623cfa26904a6"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb87745b2e6dc56361bfde481d5a378dc314b252a98d7dd19a651a3fa58f24a9"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58455b79ec2661c3600e65c0a716955adc2410f7383755d537584b0de41b1d8a"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4e42a40a5e164cbfdb7b386c966a588b1047558a990981ace551ed7e12ca9c2"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:91bd7d1773e64019f9288b7a5101f3ae50d3d8e6b1de7edee9c2ccc1d32f0c0a"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bb90fb8bda722a1b9d48ac1e6c38f923ea757b3baf8ebd0c82e09c5c1a0e7a04"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:08e7ce672e35efa54c5024936e559469436f8b8096253404faeb54d2a878416f"}, + {file = "wrapt-1.17.2-cp38-cp38-win32.whl", hash = "sha256:410a92fefd2e0e10d26210e1dfb4a876ddaf8439ef60d6434f21ef8d87efc5b7"}, + {file = "wrapt-1.17.2-cp38-cp38-win_amd64.whl", hash = "sha256:95c658736ec15602da0ed73f312d410117723914a5c91a14ee4cdd72f1d790b3"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99039fa9e6306880572915728d7f6c24a86ec57b0a83f6b2491e1d8ab0235b9a"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2696993ee1eebd20b8e4ee4356483c4cb696066ddc24bd70bcbb80fa56ff9061"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:612dff5db80beef9e649c6d803a8d50c409082f1fedc9dbcdfde2983b2025b82"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c2caa1585c82b3f7a7ab56afef7b3602021d6da34fbc1cf234ff139fed3cd9"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c958bcfd59bacc2d0249dcfe575e71da54f9dcf4a8bdf89c4cb9a68a1170d73f"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc78a84e2dfbc27afe4b2bd7c80c8db9bca75cc5b85df52bfe634596a1da846b"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba0f0eb61ef00ea10e00eb53a9129501f52385c44853dbd6c4ad3f403603083f"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1e1fe0e6ab7775fd842bc39e86f6dcfc4507ab0ffe206093e76d61cde37225c8"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c86563182421896d73858e08e1db93afdd2b947a70064b813d515d66549e15f9"}, + {file = "wrapt-1.17.2-cp39-cp39-win32.whl", hash = "sha256:f393cda562f79828f38a819f4788641ac7c4085f30f1ce1a68672baa686482bb"}, + {file = "wrapt-1.17.2-cp39-cp39-win_amd64.whl", hash = "sha256:36ccae62f64235cf8ddb682073a60519426fdd4725524ae38874adf72b5f2aeb"}, + {file = "wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8"}, + {file = "wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3"}, +] + [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = ">=3.10, <3.12" -content-hash = "15a49163967af789bafd9eaaecd2488d92674e2b41ba25db871b001abbd0c5e2" +content-hash = "1b1e272d015621437ef586a7b749c518822ab5009794a30bed4a42b988a6920b" diff --git a/pyproject.toml b/pyproject.toml index abae7d3..755dc2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,8 @@ fastapi = "^0.110.1" uvicorn = "^0.29.0" starlette = "^0.37.2" httpx = "^0.27.0" +slowapi = "^0.1.9" +python-dotenv = "^1.1.0" [tool.poetry.group.dev.dependencies] opshin = "^0.23.0" diff --git a/tests/test_gift_mockfrost.py b/tests/test_gift_mockfrost.py index 8744ced..50368b6 100644 --- a/tests/test_gift_mockfrost.py +++ b/tests/test_gift_mockfrost.py @@ -92,5 +92,9 @@ def test_other_user_spend_from_gift_contract(server): if __name__ == "__main__": - test_spend_from_gift_contract() - test_other_user_spend_from_gift_contract() + # proc = Process(target = run_server, args=(), daemon=True) + # proc.start() + test_spend_from_gift_contract(False) + test_other_user_spend_from_gift_contract(False) + + # proc.kill() diff --git a/tests/test_mockfrost_server.py b/tests/test_mockfrost_server.py new file mode 100644 index 0000000..d6f5de5 --- /dev/null +++ b/tests/test_mockfrost_server.py @@ -0,0 +1,163 @@ +import pathlib +from multiprocessing import Process +from time import sleep +import cbor2 + +import pycardano +import pytest +import uvicorn +from pycardano import TransactionFailedException +from starlette.testclient import TestClient + +from plutus_bench import MockChainContext, MockUser +from plutus_bench.mock import MockFrostApi + +from tests.gift import spend_from_gift_contract +from plutus_bench.tool import address_from_script, load_contract, ScriptType +from plutus_bench.mockfrost.client import ( + MockFrostClient, + MockFrostUser, + MockfrostApiError, +) +from plutus_bench.mockfrost.server import app +from blockfrost.utils import ApiError + +own_path = pathlib.Path(__file__) + + +def run_server(): + uvicorn.run(app, port=8000) + + +@pytest.fixture +def server(): + proc = Process(target=run_server, args=(), daemon=True) + proc.start() + sleep(1) # Wait for server to start + yield + proc.kill() # Cleanup after test + + +def test_rate_limiting_requests(server): + client = MockFrostClient(base_url="http://127.0.0.1:8000") + session = client.create_session() + context = session.chain_context() + for i in range(3600): + context.last_block_slot + with pytest.raises(ApiError, match="429"): + print(context.last_block_slot) + + +def test_rate_limiting_sessions(server): + client = MockFrostClient(base_url="http://127.0.0.1:8000") + for i in range(1000): + session = client.create_session() + with pytest.raises(MockfrostApiError, match="Too many requests"): + session = client.create_session() + + +def test_max_transaction_size(server): + client = MockFrostClient(base_url="http://127.0.0.1:8000") + session = client.create_session() + context = session.chain_context() + payment_key = MockFrostUser(session) + payment_key.fund(100_000_000) + # This should exceed max contract size + gift_contract = pycardano.PlutusV2Script(b"0" * context.protocol_param.max_tx_size) + gift_address = address_from_script(gift_contract, network=context.network) + session.add_txout( + pycardano.TransactionOutput( + address=gift_address, + amount=pycardano.Value(coin=1000000), + datum=payment_key.verification_key.hash().payload, + ), + ) + + payment_vkey_hash = payment_key.verification_key.hash() + utxos = context.utxos(gift_address) + spend_utxo = None + for u in utxos: + datum = cbor2.loads(u.output.datum.cbor) + if datum != payment_vkey_hash.payload: + continue + spend_utxo = u + break + assert spend_utxo is not None, "No UTxO found" + + txbuilder = pycardano.TransactionBuilder(context=context) + txbuilder.add_input_address(payment_key.address) + txbuilder.add_script_input(spend_utxo, gift_contract, None, pycardano.Redeemer(0)) + with pytest.raises( + pycardano.exception.InvalidTransactionException, match="Transaction size" + ): + tx = txbuilder.build_and_sign( + signing_keys=[payment_key.signing_key], + change_address=payment_key.address, + auto_required_signers=True, + ) + context.submit_tx(tx) + + +def test_max_resource_limits(server): + client = MockFrostClient(base_url="http://127.0.0.1:8000") + session = client.create_session() + context = session.chain_context() + payment_key = MockFrostUser(session) + payment_key.fund(100_000_000) + # This should exceed max contract size + gift_contract_path = own_path.parent / "build" / "gift" / "script.plutus" + gift_contract = load_contract(gift_contract_path, ScriptType.PlutusV2) + gift_address = address_from_script(gift_contract, network=context.network) + session.add_txout( + pycardano.TransactionOutput( + address=gift_address, + amount=pycardano.Value(coin=1000000), + datum=payment_key.verification_key.hash().payload, + ), + ) + + payment_vkey_hash = payment_key.verification_key.hash() + utxos = context.utxos(gift_address) + spend_utxo = None + for u in utxos: + datum = cbor2.loads(u.output.datum.cbor) + if datum != payment_vkey_hash.payload: + continue + spend_utxo = u + break + assert spend_utxo is not None, "No UTxO found" + + txbuilder = pycardano.TransactionBuilder(context=context) + txbuilder.add_input_address(payment_key.address) + txbuilder.add_script_input(spend_utxo, gift_contract, None, pycardano.Redeemer(0)) + tx = txbuilder.build_and_sign( + signing_keys=[payment_key.signing_key], + change_address=payment_key.address, + auto_required_signers=True, + ) + redeemers = tx.transaction_witness_set.redeemer + redeemer_key = next(iter(redeemers)) + redeemer_value = redeemers[redeemer_key] + old_steps = redeemer_value.ex_units.steps + redeemer_value.ex_units.steps = context.protocol_param.max_tx_ex_steps + 1 + + with pytest.raises( + pycardano.exception.TransactionFailedException, match="Invalid ExUnits:" + ): + context.submit_tx(tx) + + redeemer_value.ex_units.steps = old_steps + redeemer_value.ex_units.mem = context.protocol_param.max_tx_ex_mem + 1 + with pytest.raises( + pycardano.exception.TransactionFailedException, match="Invalid ExUnits:" + ): + context.submit_tx(tx) + + +if __name__ == "__main__": + # proc = Process(target = run_server, args=(), daemon=True) + # proc.start() + # test_max_transaction_size_ + # test_rate_limiting_requests(False) + test_max_resource_limits(False) + # proc.kill()