diff --git a/.github/workflows/pr-preprod-tests.yaml b/.github/workflows/pr-preprod-tests.yaml index 61fa829d5b..7533b1a631 100644 --- a/.github/workflows/pr-preprod-tests.yaml +++ b/.github/workflows/pr-preprod-tests.yaml @@ -210,9 +210,8 @@ jobs: # Output test result echo "construction_result=${CONSTRUCTION_RESULT:-0}" >> $GITHUB_OUTPUT - # Don't fail the whole job if construction tests fail - # These are informational for now - exit 0 + # Fail if construction tests failed + exit ${CONSTRUCTION_RESULT:-0} env: ROSETTA_URL: http://localhost:8082 CARDANO_NETWORK: preprod diff --git a/.gitignore b/.gitignore index 7499c9a66d..d694ec1251 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ hs_err_pid* target settings.xml .env* +!.env.example /data/ /node/ diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/account/mapper/AccountMapperUtil.java b/api/src/main/java/org/cardanofoundation/rosetta/api/account/mapper/AccountMapperUtil.java index 8be317722f..b3ca33e166 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/account/mapper/AccountMapperUtil.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/account/mapper/AccountMapperUtil.java @@ -18,6 +18,9 @@ import java.math.BigInteger; import java.util.*; +import static org.cardanofoundation.rosetta.common.util.Constants.ADA; +import static org.cardanofoundation.rosetta.common.util.Constants.ADA_DECIMALS; + @Component @RequiredArgsConstructor public class AccountMapperUtil { @@ -76,7 +79,7 @@ public List mapUtxosToCoins(List utxos, Amt adaAsset = utxo.getAmounts().stream() .filter(amt -> Constants.LOVELACE.equals(amt.getUnit())) .findFirst() - .orElseGet(() -> new Amt(null, Constants.ADA, BigInteger.ZERO)); + .orElseGet(() -> new Amt(null, ADA, BigInteger.ZERO)); String coinIdentifier = "%s:%d".formatted(utxo.getTxHash(), utxo.getOutputIndex()); @@ -144,8 +147,8 @@ private static int getDecimalsWithFallback(@NotNull TokenRegistryCurrencyData me private CurrencyResponse getAdaCurrency() { return CurrencyResponse.builder() - .symbol(Constants.ADA) - .decimals(Constants.ADA_DECIMALS) + .symbol(ADA) + .decimals(ADA_DECIMALS) .build(); } diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/TxnEntity.java b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/TxnEntity.java index 8e63236324..b4c731d2d5 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/TxnEntity.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/TxnEntity.java @@ -2,6 +2,7 @@ import java.math.BigInteger; import java.util.List; +import jakarta.annotation.Nullable; import jakarta.persistence.Column; import jakarta.persistence.ConstraintMode; import jakarta.persistence.Entity; @@ -50,6 +51,10 @@ public class TxnEntity { @Column(name = "fee") private BigInteger fee; + @Column(name = "tx_index") + @Nullable + private Integer txIndex; + @OneToMany(mappedBy = "txHash") private List script; diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/TxRepositoryCustomBase.java b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/TxRepositoryCustomBase.java index 58983c1a92..6f3f18b49f 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/TxRepositoryCustomBase.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/TxRepositoryCustomBase.java @@ -123,6 +123,7 @@ public List findTransactionsByBlockHash(String blockHash) { TRANSACTION.OUTPUTS, TRANSACTION.FEE, TRANSACTION.SLOT, + TRANSACTION.TX_INDEX, BLOCK.HASH.as("joined_block_hash"), BLOCK.NUMBER.as("joined_block_number"), BLOCK.SLOT.as("joined_block_slot"), @@ -133,6 +134,7 @@ public List findTransactionsByBlockHash(String blockHash) { .leftJoin(BLOCK).on(TRANSACTION.BLOCK_HASH.eq(BLOCK.HASH)) .leftJoin(TRANSACTION_SIZE).on(TRANSACTION.TX_HASH.eq(TRANSACTION_SIZE.TX_HASH)) .where(TRANSACTION.BLOCK_HASH.eq(blockHash)) + .orderBy(TRANSACTION.TX_INDEX.asc()) .fetch(queryBuilder::mapRecordToTxnEntity); } @@ -182,12 +184,12 @@ protected int executeCountQuery(Condition conditions, @Nullable Boolean isSucces * Ensures count and results queries use identical conditions and JOINs. * Currency conditions use EXISTS subqueries - no JOIN needed. */ - protected List executeResultsQuery(Condition conditions, - @Nullable Boolean isSuccess, + protected List executeResultsQuery(Condition conditions, + @Nullable Boolean isSuccess, OffsetBasedPageRequest offsetBasedPageRequest) { return buildBaseResultsQuery(isSuccess) .where(conditions) - .orderBy(TRANSACTION.SLOT.desc()) + .orderBy(TRANSACTION.SLOT.desc(), TRANSACTION.TX_INDEX.desc()) .limit(offsetBasedPageRequest.getLimit()) .offset(offsetBasedPageRequest.getOffset()) .fetch(); diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/util/TxRepositoryQueryBuilder.java b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/util/TxRepositoryQueryBuilder.java index 5f0d9e129a..06780da5e7 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/util/TxRepositoryQueryBuilder.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/util/TxRepositoryQueryBuilder.java @@ -36,7 +36,7 @@ public interface CurrencyConditionBuilder { Condition buildCurrencyCondition(Currency currency); } - public SelectJoinStep> buildTransactionSelectQuery(DSLContext dsl) { + public SelectJoinStep> buildTransactionSelectQuery(DSLContext dsl) { return dsl.select( TRANSACTION.TX_HASH, TRANSACTION.BLOCK_HASH, @@ -44,6 +44,7 @@ public SelectJoinStep operations = operationService.getOperationsFromTransactionData(convertedTr, network); + List operations = transactionOperationParser.getOperationsFromTransactionData(convertedTr, network); List accountIdentifierSigners = new ArrayList<>(); + if (signed) { log.info("[parseSignedTransaction] About to get signatures from parsed transaction"); List accumulator = convertedTr.transactionExtraData().operations().stream() - .map(o -> operationService.getSignerFromOperation(network, o)) + .map(o -> transactionOperationParser.getSignerFromOperation(network, o)) .flatMap(List::stream) .toList(); + accountIdentifierSigners = getUniqueAccountIdentifiers(accumulator); } + return new TransactionParsed(operations, accountIdentifierSigners); } catch (CborException | CborDeserializationException | CborSerializationException error) { log.error("{} [parseTransaction] Cant instantiate transaction from transaction bytes", error.getMessage(), error); + throw ExceptionFactory.invalidTransactionError(); } } @@ -624,7 +629,7 @@ private TransactionBody deserializeTransactionBody(String unsignedTransaction) { private List getUniqueAccountIdentifiers(List addresses) { return new HashSet<>(addresses) .stream() - .map(s -> new AccountIdentifier(s, null, null)) + .map(address -> new AccountIdentifier(address, null, null)) .toList(); } diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/construction/service/OperationService.java b/api/src/main/java/org/cardanofoundation/rosetta/api/construction/service/OperationService.java deleted file mode 100644 index 9ffaf933c2..0000000000 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/construction/service/OperationService.java +++ /dev/null @@ -1,192 +0,0 @@ -package org.cardanofoundation.rosetta.api.construction.service; - -import co.nstant.in.cbor.CborException; -import com.bloxbean.cardano.client.common.model.Network; -import com.bloxbean.cardano.client.crypto.bip32.key.HdPublicKey; -import com.bloxbean.cardano.client.exception.CborDeserializationException; -import com.bloxbean.cardano.client.exception.CborSerializationException; -import com.bloxbean.cardano.client.transaction.spec.TransactionBody; -import com.bloxbean.cardano.client.transaction.spec.TransactionInput; -import com.bloxbean.cardano.client.transaction.spec.TransactionOutput; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.ObjectUtils; -import org.cardanofoundation.rosetta.common.enumeration.OperationType; -import org.cardanofoundation.rosetta.common.exception.ExceptionFactory; -import org.cardanofoundation.rosetta.common.model.cardano.pool.PoolRegistrationCertReturn; -import org.cardanofoundation.rosetta.common.model.cardano.transaction.TransactionData; -import org.cardanofoundation.rosetta.common.model.cardano.transaction.TransactionExtraData; -import org.cardanofoundation.rosetta.common.util.CardanoAddressUtils; -import org.cardanofoundation.rosetta.common.util.Constants; -import org.cardanofoundation.rosetta.common.util.ParseConstructionUtil; -import org.cardanofoundation.rosetta.common.util.ValidateParseUtil; -import org.openapitools.client.model.Operation; -import org.openapitools.client.model.OperationIdentifier; -import org.openapitools.client.model.OperationMetadata; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import static org.cardanofoundation.rosetta.common.util.Constants.OPERATION_TYPE_POOL_REGISTRATION; -import static org.cardanofoundation.rosetta.common.util.Constants.OPERATION_TYPE_POOL_REGISTRATION_WITH_CERT; - -/** - * Service class to operate on Operations - */ -@Slf4j -@Service -public class OperationService { - - public List getOperationsFromTransactionData(TransactionData data, Network network) - throws CborDeserializationException, CborException, CborSerializationException { - TransactionBody transactionBody = data.transactionBody(); - TransactionExtraData extraData = data.transactionExtraData(); - - List operations = new ArrayList<>(); - fillInputOperations(transactionBody, extraData, operations); - fillOutputOperations(transactionBody, operations); - fillCertOperations(transactionBody, extraData, network, operations); - fillWithdrawalOperations(transactionBody, extraData, network, operations); - fillGovOperations(extraData, operations, network); - - return operations; - } - - private void fillGovOperations(TransactionExtraData transactionBody, List operations, Network network) { - List governanceOperations = transactionBody.operations().stream() - .filter(o -> Constants.GOVERNANCE_OPERATIONS.contains(o.getType()) - ).toList(); - - operations.addAll(governanceOperations); - } - - public List getSignerFromOperation(Network network, Operation operation) { - if (Constants.POOL_OPERATIONS.contains(operation.getType())) { - return getPoolSigners(network, operation); - } - if (operation.getAccount() != null) { - // org.openapitools.client.model.AccountIdentifier.getAddress() is always not null - return Collections.singletonList(operation.getAccount().getAddress()); - } - validateMetadataForStakingCredential(operation); - HdPublicKey hdPublicKey = - CardanoAddressUtils.publicKeyToHdPublicKey(operation.getMetadata().getStakingCredential()); - - return Collections.singletonList( - CardanoAddressUtils.generateRewardAddress(network, hdPublicKey)); - } - - private void fillInputOperations(TransactionBody transactionBody, - TransactionExtraData extraData, - List operations) { - List inputs = transactionBody.getInputs(); - log.info("[fillInputOperations] About to parse {} inputs", inputs.size()); - List extraDataInputOperations = extraData.operations().stream() - .filter(o -> o.getType().equals(OperationType.INPUT.getValue())) - .toList(); - - for (int i = 0; i < inputs.size(); i++) { - if (!extraDataInputOperations.isEmpty() && extraDataInputOperations.size() <= inputs.size()) { - operations.add(extraDataInputOperations.get(i)); - } else { // fallback in case of no extra data input operations - TransactionInput input = inputs.get(i); - - Operation inputParsed = ParseConstructionUtil.transactionInputToOperation(input, - (long) operations.size()); - - operations.add(inputParsed); - } - } - } - - private void fillOutputOperations(TransactionBody transactionBody, List operations) { - List outputs = transactionBody.getOutputs(); - List relatedOperations = ParseConstructionUtil.getRelatedOperationsFromInputs( - operations); - - log.info("[parseOperationsFromTransactionBody] About to parse {} outputs", outputs.size()); - - for (TransactionOutput output : outputs) { - Operation outputParsed = ParseConstructionUtil.transActionOutputToOperation(output, - (long) operations.size(), - relatedOperations); - operations.add(outputParsed); - } - } - - private void fillCertOperations(TransactionBody transactionBody, TransactionExtraData extraData, - Network network, List operations) - throws CborException, CborSerializationException { - List certOps = extraData.operations().stream() - .filter(o -> { - return Constants.STAKE_POOL_OPERATIONS.contains(o.getType()) - || OperationType.VOTE_DREP_DELEGATION.getValue().equals(o.getType()); - } - ).toList(); - - List parsedCertOperations = ParseConstructionUtil.parseCertsToOperations( - transactionBody, certOps, network); - - operations.addAll(parsedCertOperations); - } - - private void fillWithdrawalOperations(TransactionBody transactionBody, - TransactionExtraData extraData, - Network network, List operations) { - List withdrawalOps = extraData.operations().stream() - .filter(o -> o.getType().equals(OperationType.WITHDRAWAL.getValue())) - .toList(); - int withdrawalsCount = ObjectUtils.isEmpty(transactionBody.getWithdrawals()) ? 0 - : transactionBody.getWithdrawals().size(); - List withdrawalsOperations = ParseConstructionUtil.parseWithdrawalsToOperations( - withdrawalOps, withdrawalsCount, network); - operations.addAll(withdrawalsOperations); - } - - private List getPoolSigners(Network network, Operation operation) { - List signers = new ArrayList<>(); - switch (operation.getType()) { - case OPERATION_TYPE_POOL_REGISTRATION -> { - if (ValidateParseUtil.validateAddressPresence(operation)) { - signers.add(operation.getAccount().getAddress()); - } - Optional.ofNullable(operation.getMetadata()) - .map(OperationMetadata::getPoolRegistrationParams) - .ifPresent(poolRegistrationParameters -> { - signers.add(poolRegistrationParameters.getRewardAddress()); - signers.addAll(poolRegistrationParameters.getPoolOwners()); - }); - } - case OPERATION_TYPE_POOL_REGISTRATION_WITH_CERT -> { - String poolCertAsHex = Optional.ofNullable(operation.getMetadata()) - .map(OperationMetadata::getPoolRegistrationCert) - .orElse(null); - PoolRegistrationCertReturn dto = ValidateParseUtil.validateAndParsePoolRegistrationCert( - network, - poolCertAsHex, - operation.getAccount() == null ? null : operation.getAccount().getAddress() - ); - signers.addAll(dto.getAddress()); - } - - // pool retirement case - default -> { - if (ValidateParseUtil.validateAddressPresence(operation)) { - signers.add(operation.getAccount().getAddress()); - } - } - } - log.info("[getPoolSigners] About to return {} signers for {} operation", signers.size(), - operation.getType()); - - return signers; - } - - private void validateMetadataForStakingCredential(Operation operation) { - if (operation.getMetadata() == null) { - throw ExceptionFactory.missingStakingKeyError(); - } - } -} diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/construction/service/TransactionOperationParser.java b/api/src/main/java/org/cardanofoundation/rosetta/api/construction/service/TransactionOperationParser.java new file mode 100644 index 0000000000..c6afcc74f8 --- /dev/null +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/construction/service/TransactionOperationParser.java @@ -0,0 +1,61 @@ +package org.cardanofoundation.rosetta.api.construction.service; + +import co.nstant.in.cbor.CborException; +import com.bloxbean.cardano.client.common.model.Network; +import com.bloxbean.cardano.client.exception.CborDeserializationException; +import com.bloxbean.cardano.client.exception.CborSerializationException; +import org.cardanofoundation.rosetta.common.model.cardano.transaction.TransactionData; +import org.openapitools.client.model.Operation; + +import java.util.List; + +/** + * Parser interface for extracting Rosetta operations from Cardano transactions. + *

+ * This parser handles the conversion between Cardano transaction structures and Rosetta API + * operations, including inputs, outputs, certificates (staking, pool, governance), and withdrawals. + *

+ */ +public interface TransactionOperationParser { + + /** + * Extracts and constructs a complete list of Rosetta operations from transaction data. + *

+ * This method processes all operation types from a Cardano transaction, including: + *

    + *
  • Input operations - UTxO consumption
  • + *
  • Output operations - UTxO creation
  • + *
  • Certificate operations - Staking registrations, delegations, pool operations
  • + *
  • Governance operations - DRep vote delegations, pool governance votes
  • + *
  • Withdrawal operations - Staking reward withdrawals
  • + *
+ *

+ * + * @param data the transaction data containing the transaction body and extra metadata + * @param network the Cardano network (mainnet/testnet) for address generation + * @return a list of Rosetta operations representing all transaction activities + * @throws CborDeserializationException if CBOR deserialization fails + * @throws CborException if CBOR processing encounters an error + * @throws CborSerializationException if CBOR serialization fails + */ + List getOperationsFromTransactionData(TransactionData data, Network network) + throws CborDeserializationException, CborException, CborSerializationException; + + /** + * Determines the required signers for a given operation. + *

+ * Different operation types require different signers: + *

    + *
  • Pool operations - Pool owners, reward address, and optionally payment address
  • + *
  • Staking operations - Stake address derived from staking credential
  • + *
  • Regular operations - Account address from the operation
  • + *
+ *

+ * + * @param network the Cardano network for address generation + * @param operation the operation to extract signers from + * @return list of addresses that must sign this operation + */ + List getSignerFromOperation(Network network, Operation operation); + +} diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/construction/service/TransactionOperationParserImpl.java b/api/src/main/java/org/cardanofoundation/rosetta/api/construction/service/TransactionOperationParserImpl.java new file mode 100644 index 0000000000..93c0d9e74d --- /dev/null +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/construction/service/TransactionOperationParserImpl.java @@ -0,0 +1,436 @@ +package org.cardanofoundation.rosetta.api.construction.service; + +import co.nstant.in.cbor.CborException; +import com.bloxbean.cardano.client.common.model.Network; +import com.bloxbean.cardano.client.crypto.bip32.key.HdPublicKey; +import com.bloxbean.cardano.client.exception.CborSerializationException; +import com.bloxbean.cardano.client.transaction.spec.TransactionBody; +import com.bloxbean.cardano.client.transaction.spec.TransactionInput; +import com.bloxbean.cardano.client.transaction.spec.TransactionOutput; +import com.bloxbean.cardano.client.transaction.spec.cert.Certificate; +import com.bloxbean.cardano.client.util.HexUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.cardanofoundation.rosetta.common.enumeration.OperationType; +import org.cardanofoundation.rosetta.common.exception.ExceptionFactory; +import org.cardanofoundation.rosetta.common.model.cardano.pool.PoolRegistrationCertReturn; +import org.cardanofoundation.rosetta.common.model.cardano.transaction.TransactionData; +import org.cardanofoundation.rosetta.common.model.cardano.transaction.TransactionExtraData; +import org.cardanofoundation.rosetta.common.util.CardanoAddressUtils; +import org.cardanofoundation.rosetta.common.util.ParseConstructionUtil; +import org.cardanofoundation.rosetta.common.util.ValidateParseUtil; +import org.openapitools.client.model.Operation; +import org.openapitools.client.model.OperationIdentifier; +import org.openapitools.client.model.OperationMetadata; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.cardanofoundation.rosetta.common.enumeration.OperationType.POOL_GOVERNANCE_VOTE; +import static org.cardanofoundation.rosetta.common.enumeration.OperationType.VOTE_DREP_DELEGATION; +import static org.cardanofoundation.rosetta.common.util.Constants.*; +import static org.cardanofoundation.rosetta.common.util.OperationTypes.*; +import static org.cardanofoundation.rosetta.common.util.ParseConstructionUtil.*; + +/** + * Implementation of {@link TransactionOperationParser} for parsing Rosetta operations. + *

+ * This parser orchestrates the conversion of Cardano transaction structures into + * Rosetta API operations. It handles multiple operation types and ensures proper + * ordering and relationships between operations. + *

+ */ +@Slf4j +@Service +public class TransactionOperationParserImpl implements TransactionOperationParser { + + @Override + public List getOperationsFromTransactionData(TransactionData data, Network network) throws CborException, CborSerializationException { + TransactionBody transactionBody = data.transactionBody(); + TransactionExtraData extraData = data.transactionExtraData(); + + List operations = new ArrayList<>(); + + fillInputOperations(transactionBody, extraData, operations); + fillOutputOperations(transactionBody, operations); + fillCertOperations(transactionBody, extraData, network, operations); + fillWithdrawalOperations(transactionBody, extraData, network, operations); + fillSpoVotingOperations(extraData, operations); + + return operations; + } + + /** + * {@inheritDoc} + *

+ * Signer determination logic: + *

    + *
  • Pool operations: Delegates to {@link #getPoolSigners(Network, Operation)} + * which extracts pool owners and reward addresses
  • + *
  • Operations with account: Returns the account address directly
  • + *
  • Staking operations: Derives reward address from staking credential
  • + *
+ *

+ */ + @Override + public List getSignerFromOperation(Network network, Operation operation) { + // Pool operations require special handling for multiple signers + if (POOL_OPERATIONS.contains(operation.getType())) { + return getPoolSigners(network, operation); + } + + // If account is present, use its address directly + if (operation.getAccount() != null) { + // org.openapitools.client.model.AccountIdentifier.getAddress() is always not null + return Collections.singletonList(operation.getAccount().getAddress()); + } + + // For staking operations, derive reward address from credential + validateMetadataForStakingCredential(operation); + + HdPublicKey hdPublicKey = + CardanoAddressUtils.publicKeyToHdPublicKey(operation.getMetadata().getStakingCredential()); + + return Collections.singletonList( + CardanoAddressUtils.generateRewardAddress(network, hdPublicKey)); + } + + /** + * Processes transaction inputs and converts them to Rosetta input operations. + *

+ * Input operations represent UTxO consumption. This method attempts to use + * pre-processed input operations from extra data if available, otherwise + * falls back to parsing directly from transaction inputs. + *

+ * + * @param transactionBody the transaction body containing inputs + * @param extraData extra transaction data that may contain pre-processed input operations + * @param operations the list to append input operations to + */ + private void fillInputOperations(TransactionBody transactionBody, + TransactionExtraData extraData, + List operations) { + List inputs = transactionBody.getInputs(); + log.info("[fillInputOperations] About to parse {} inputs", inputs.size()); + + // Try to use pre-processed input operations from extra data + List extraDataInputOperations = extraData.operations().stream() + .filter(o -> o.getType().equals(OperationType.INPUT.getValue())) + .toList(); + + for (int i = 0; i < inputs.size(); i++) { + if (i < extraDataInputOperations.size()) { + // Use pre-processed operation from extra data + operations.add(extraDataInputOperations.get(i)); + } else { + // Fallback: parse directly from transaction input + TransactionInput input = inputs.get(i); + Operation inputParsed = ParseConstructionUtil.transactionInputToOperation(input, + (long) operations.size()); + + operations.add(inputParsed); + } + } + } + + /** + * Processes transaction outputs and converts them to Rosetta output operations. + *

+ * Output operations represent UTxO creation. Each output includes related operations + * linking it to the corresponding input operations for proper transaction flow visualization. + *

+ * + * @param transactionBody the transaction body containing outputs + * @param operations the list to append output operations to + */ + private void fillOutputOperations(TransactionBody transactionBody, + List operations) { + List outputs = transactionBody.getOutputs(); + + if (outputs == null) { + log.warn("[fillOutputOperations] Transaction outputs list is null, skipping output operations"); + return; + } + + List relatedOperations = ParseConstructionUtil.getRelatedOperationsFromInputs( + operations); + + log.info("[fillOutputOperations] About to parse {} outputs", outputs.size()); + + for (TransactionOutput output : outputs) { + Operation outputParsed = ParseConstructionUtil.transActionOutputToOperation(output, + (long) operations.size(), + relatedOperations); + + operations.add(outputParsed); + } + } + + /** + * Processes transaction certificates and converts them to Rosetta certificate operations. + *

+ * This method handles ALL certificate types in a single pass, maintaining the order + * of certificates as they appear in the transaction body. Certificate types include: + *

    + *
  • Staking certificates: Registration, deregistration, delegation
  • + *
  • Pool certificates: Pool registration, retirement
  • + *
  • Governance certificates: DRep vote delegation (VoteDelegCert)
  • + *
+ *

+ *

+ * Important: The order of operations in extraData must match the order of + * certificates in the transaction body's certs[] array. + *

+ * + * @param transactionBody the transaction body containing certificates + * @param extraData extra transaction data containing certificate operation metadata + * @param network the Cardano network for address generation + * @param operations the list to append certificate operations to + * @throws CborException if CBOR processing fails + * @throws CborSerializationException if certificate serialization fails + */ + private void fillCertOperations(TransactionBody transactionBody, + TransactionExtraData extraData, + Network network, + List operations) + throws CborException, CborSerializationException { + + // Filter ALL certificate operations from extra data + List certOps = extraData.operations().stream() + .filter(o -> CERTIFICATE_OPERATIONS.contains(o.getType())) + .toList(); + + List certs = transactionBody.getCerts(); + int certsCount = getCertSize(certs); + + log.info("[fillCertOperations] About to parse {} certificates", certsCount); + + // Process all certificates in order + for (int i = 0; i < certsCount; i++) { + Operation certOperation = certOps.get(i); + Certificate cert = ValidateParseUtil.validateCert(certs, i); + + if (ObjectUtils.isEmpty(cert)) { + continue; + } + + + // Handle staking operations (registration, deregistration, delegation) + if (STAKING_OPERATIONS.contains(certOperation.getType())) { + String hex = getStakingCredentialHex(certOperation); + HdPublicKey hdPublicKey = new HdPublicKey(); + hdPublicKey.setKeyData(HexUtil.decodeHexString(hex)); + String address = CardanoAddressUtils.generateRewardAddress(network, hdPublicKey); + + Operation parsedOperation = parseStakingCertToOperation( + cert, + certOperation.getOperationIdentifier().getIndex(), + hex, + certOperation.getType(), + address + ); + + parsedOperation.setAccount(certOperation.getAccount()); + operations.add(parsedOperation); + } + + // Handle DRep vote delegation (governance certificate) + if (VOTE_DREP_DELEGATION.getValue().equals(certOperation.getType())) { + String hex = getStakingCredentialHex(certOperation); + HdPublicKey hdPublicKey = new HdPublicKey(); + hdPublicKey.setKeyData(HexUtil.decodeHexString(hex)); + String address = CardanoAddressUtils.generateRewardAddress(network, hdPublicKey); + + Operation parsedOperation = parseDRepVoteDelegation( + cert, + certOperation.getOperationIdentifier().getIndex(), + hex, + certOperation.getType(), + address + ); + + parsedOperation.setAccount(certOperation.getAccount()); + operations.add(parsedOperation); + } + + // Handle pool operations (registration, retirement) + if (POOL_OPERATIONS.contains(certOperation.getType())) { + Operation parsedOperation = parsePoolCertToOperation( + network, + cert, + certOperation.getOperationIdentifier().getIndex(), + certOperation.getType() + ); + parsedOperation.setAccount(certOperation.getAccount()); + operations.add(parsedOperation); + } + + } + } + + /** + * Processes staking reward withdrawals and converts them to Rosetta withdrawal operations. + *

+ * Withdrawal operations represent the claiming of staking rewards accumulated in + * reward accounts. The method validates that the number of operations matches the + * number of withdrawals in the transaction body. + *

+ * + * @param transactionBody the transaction body containing withdrawals + * @param extraData extra transaction data containing withdrawal operation metadata + * @param network the Cardano network for address validation + * @param operations the list to append withdrawal operations to + */ + private void fillWithdrawalOperations(TransactionBody transactionBody, + TransactionExtraData extraData, + Network network, + List operations) { + List withdrawalOps = extraData.operations().stream() + .filter(o -> o.getType().equals(OperationType.WITHDRAWAL.getValue())) + .toList(); + + int withdrawalsCount = ObjectUtils.isEmpty(transactionBody.getWithdrawals()) ? 0 + : transactionBody.getWithdrawals().size(); + + log.info("[parseWithdrawalsToOperations] About to parse {} withdrawals", withdrawalsCount); + + for (int i = 0; i < withdrawalsCount; i++) { + Operation withdrawalOperation = withdrawalOps.get(i); + String stakingCredentialHex = getStakingCredentialHex(withdrawalOperation); + HdPublicKey hdPublicKey = new HdPublicKey(); + hdPublicKey.setKeyData(HexUtil.decodeHexString(stakingCredentialHex)); + String address = CardanoAddressUtils.generateRewardAddress(network, hdPublicKey); + Operation parsedOperation = parseWithdrawalToOperation( + withdrawalOperation.getAmount().getValue(), + stakingCredentialHex, + withdrawalOperation.getOperationIdentifier().getIndex(), + address + ); + operations.add(parsedOperation); + } + } + + /** + * Processes SPO (Stake Pool Operator) governance voting operations from transaction extra data. + *

+ * This method handles pool governance votes which are voting procedures (NOT certificates). + * These operations represent stake pool operators voting on governance actions through + * the Conway era voting mechanism. + *

+ *

+ * Important distinction: + *

    + *
  • Pool Governance Votes (this method): Voting procedures in the voting_procedures + * field of the transaction body. Operation type: {@code poolGovernanceVote}
  • + *
  • DRep Vote Delegation: Certificate-based operations (VoteDelegCert) processed + * in {@link #fillCertOperations(TransactionBody, TransactionExtraData, Network, List)}. + * Operation type: {@code dRepVoteDelegation}
  • + *
+ *

+ *

+ * Unlike certificates, SPO voting operations come pre-processed in extra data with all + * necessary metadata already populated, including: + *

    + *
  • governance_action_hash - The hash of the governance action being voted on
  • + *
  • pool_credential - The stake pool's credential (public key)
  • + *
  • vote - The vote choice (yes, no, abstain)
  • + *
+ * Therefore, this method simply filters and adds these fully-formed operations to the list. + *

+ * + * @param extraData extra transaction metadata containing pre-processed SPO voting operations + * @param operations the list to append SPO voting operations to + */ + private void fillSpoVotingOperations(TransactionExtraData extraData, + List operations) { + // Filter only SPO governance voting operations from extra data + // These are voting procedures, not certificates + List spoVotingOps = extraData.operations().stream() + .filter(o -> o.getType().equals(POOL_GOVERNANCE_VOTE.getValue())) + .toList(); + + log.info("[fillSpoVotingOperations] About to add {} SPO voting operations", spoVotingOps.size()); + + // SPO voting operations are already fully formed in extra data, just add them + operations.addAll(spoVotingOps); + } + + /** + * Extracts all required signers for pool-related operations. + *

+ * Pool operations may require multiple signers depending on the operation type: + *

    + *
  • Pool registration: Pool owners, reward address, optional payment address
  • + *
  • Pool registration with cert: Extracted from the certificate hex
  • + *
  • Pool retirement: Payment address if present
  • + *
+ *

+ * + * @param network the Cardano network for address validation + * @param operation the pool operation to extract signers from + * @return list of addresses that must sign the pool operation + */ + private List getPoolSigners(Network network, Operation operation) { + List signers = new ArrayList<>(); + + switch (operation.getType()) { + case OPERATION_TYPE_POOL_REGISTRATION -> { + // Add payment address if present + if (ValidateParseUtil.validateAddressPresence(operation)) { + signers.add(operation.getAccount().getAddress()); + } + // Extract pool owners and reward address from registration params + Optional.ofNullable(operation.getMetadata()) + .map(OperationMetadata::getPoolRegistrationParams) + .ifPresent(poolRegistrationParameters -> { + signers.add(poolRegistrationParameters.getRewardAddress()); + signers.addAll(poolRegistrationParameters.getPoolOwners()); + }); + } + case OPERATION_TYPE_POOL_REGISTRATION_WITH_CERT -> { + // Parse signers from the pool registration certificate hex + String poolCertAsHex = Optional.ofNullable(operation.getMetadata()) + .map(OperationMetadata::getPoolRegistrationCert) + .orElse(null); + + PoolRegistrationCertReturn dto = ValidateParseUtil.validateAndParsePoolRegistrationCert( + network, + poolCertAsHex, + operation.getAccount() == null ? null : operation.getAccount().getAddress() + ); + signers.addAll(dto.getAddress()); + } + case OPERATION_TYPE_POOL_RETIREMENT -> { + // Add payment address if present for pool retirement + if (ValidateParseUtil.validateAddressPresence(operation)) { + signers.add(operation.getAccount().getAddress()); + } + } + + default -> throw new IllegalStateException("pool operation not supported, operation:" + operation.getType()); + } + + log.info("[getPoolSigners] About to return {} signers for {} operation", signers.size(), operation.getType()); + + return signers; + } + + /** + * Validates that the operation metadata contains a staking credential. + *

+ * This validation is required for operations that need to derive a reward address + * from the staking credential (e.g., stake registration, delegation). + *

+ * + * @param operation the operation to validate + * @throws org.cardanofoundation.rosetta.common.exception.ApiException if metadata is null + */ + private void validateMetadataForStakingCredential(Operation operation) { + if (operation.getMetadata() == null) { + throw ExceptionFactory.missingStakingKeyError(); + } + } +} diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/jooq/tables/Transaction.java b/api/src/main/java/org/cardanofoundation/rosetta/api/jooq/tables/Transaction.java index aa4cc446d3..11c2abd7d1 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/jooq/tables/Transaction.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/jooq/tables/Transaction.java @@ -144,6 +144,11 @@ public Class getRecordType() { */ public final TableField COLLATERAL_RETURN_JSON = createField(DSL.name("collateral_return_json"), SQLDataType.JSONB, this, ""); + /** + * The column public.transaction.block. + */ + public final TableField TX_INDEX = createField(DSL.name("tx_index"), SQLDataType.INTEGER, this, ""); + /** * The column public.transaction.block. */ diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/jooq/tables/records/TransactionRecord.java b/api/src/main/java/org/cardanofoundation/rosetta/api/jooq/tables/records/TransactionRecord.java index 3868d3f4ad..ce77b75618 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/jooq/tables/records/TransactionRecord.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/jooq/tables/records/TransactionRecord.java @@ -272,46 +272,60 @@ public JSONB getCollateralReturnJson() { return (JSONB) get(17); } + /** + * Setter for public.transaction.tx_index. + */ + public void setTxIndex(Integer value) { + set(18, value); + } + + /** + * Getter for public.transaction.tx_index. + */ + public Integer getTxIndex() { + return (Integer) get(18); + } + /** * Setter for public.transaction.block. */ public void setBlock(Long value) { - set(18, value); + set(19, value); } /** * Getter for public.transaction.block. */ public Long getBlock() { - return (Long) get(18); + return (Long) get(19); } /** * Setter for public.transaction.block_time. */ public void setBlockTime(Long value) { - set(19, value); + set(20, value); } /** * Getter for public.transaction.block_time. */ public Long getBlockTime() { - return (Long) get(19); + return (Long) get(20); } /** * Setter for public.transaction.update_datetime. */ public void setUpdateDatetime(LocalDateTime value) { - set(20, value); + set(21, value); } /** * Getter for public.transaction.update_datetime. */ public LocalDateTime getUpdateDatetime() { - return (LocalDateTime) get(20); + return (LocalDateTime) get(21); } // ------------------------------------------------------------------------- @@ -337,7 +351,7 @@ public TransactionRecord() { /** * Create a detached, initialised TransactionRecord */ - public TransactionRecord(String txHash, String auxiliaryDatahash, String blockHash, JSONB collateralInputs, JSONB collateralReturn, Long fee, JSONB inputs, Boolean invalid, Short networkId, JSONB outputs, JSONB referenceInputs, JSONB requiredSigners, String scriptDatahash, Long slot, Long totalCollateral, Long ttl, Long validityIntervalStart, JSONB collateralReturnJson, Long block, Long blockTime, LocalDateTime updateDatetime) { + public TransactionRecord(String txHash, String auxiliaryDatahash, String blockHash, JSONB collateralInputs, JSONB collateralReturn, Long fee, JSONB inputs, Boolean invalid, Short networkId, JSONB outputs, JSONB referenceInputs, JSONB requiredSigners, String scriptDatahash, Long slot, Long totalCollateral, Long ttl, Long validityIntervalStart, JSONB collateralReturnJson, Integer txIndex, Long block, Long blockTime, LocalDateTime updateDatetime) { super(Transaction.TRANSACTION); setTxHash(txHash); @@ -358,6 +372,7 @@ public TransactionRecord(String txHash, String auxiliaryDatahash, String blockHa setTtl(ttl); setValidityIntervalStart(validityIntervalStart); setCollateralReturnJson(collateralReturnJson); + setTxIndex(txIndex); setBlock(block); setBlockTime(blockTime); setUpdateDatetime(updateDatetime); diff --git a/api/src/main/java/org/cardanofoundation/rosetta/common/enumeration/OperationType.java b/api/src/main/java/org/cardanofoundation/rosetta/common/enumeration/OperationType.java index fcfc6a1422..2aa0115e3b 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/common/enumeration/OperationType.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/common/enumeration/OperationType.java @@ -15,6 +15,7 @@ public enum OperationType { STAKE_DELEGATION(OPERATION_TYPE_STAKE_DELEGATION), WITHDRAWAL(OPERATION_TYPE_WITHDRAWAL), STAKE_KEY_DEREGISTRATION(OPERATION_TYPE_STAKE_KEY_DEREGISTRATION), + POOL_REGISTRATION(OPERATION_TYPE_POOL_REGISTRATION), POOL_REGISTRATION_WITH_CERT(OPERATION_TYPE_POOL_REGISTRATION_WITH_CERT), POOL_RETIREMENT(OPERATION_TYPE_POOL_RETIREMENT), diff --git a/api/src/main/java/org/cardanofoundation/rosetta/common/model/cardano/transaction/TransactionParsed.java b/api/src/main/java/org/cardanofoundation/rosetta/common/model/cardano/transaction/TransactionParsed.java index 597e95877e..c9391847d0 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/common/model/cardano/transaction/TransactionParsed.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/common/model/cardano/transaction/TransactionParsed.java @@ -5,4 +5,5 @@ import org.openapitools.client.model.AccountIdentifier; import org.openapitools.client.model.Operation; -public record TransactionParsed (List operations, List accountIdentifierSigners) {} +public record TransactionParsed (List operations, + List accountIdentifierSigners) {} diff --git a/api/src/main/java/org/cardanofoundation/rosetta/common/util/Constants.java b/api/src/main/java/org/cardanofoundation/rosetta/common/util/Constants.java index b344d62c98..2a2d83a486 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/common/util/Constants.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/common/util/Constants.java @@ -1,10 +1,7 @@ package org.cardanofoundation.rosetta.common.util; -import org.cardanofoundation.rosetta.common.enumeration.OperationType; import org.openapitools.client.model.CurveType; -import java.util.List; - public class Constants { public static final String SINGLE_HOST_ADDR = "single_host_addr"; @@ -93,25 +90,6 @@ private Constants() { public static final String CHAIN_CODE_DUMMY = new String(new char[CHAIN_CODE_LENGTH]).replace( "\0", "0"); - public static final List STAKING_OPERATIONS = - List.of(OperationType.STAKE_DELEGATION.getValue(), - OperationType.STAKE_KEY_REGISTRATION.getValue(), - OperationType.STAKE_KEY_DEREGISTRATION.getValue(), - OperationType.WITHDRAWAL.getValue()); - - public static final List POOL_OPERATIONS = - List.of(OperationType.POOL_RETIREMENT.getValue(), - OperationType.POOL_REGISTRATION.getValue(), - OperationType.POOL_REGISTRATION_WITH_CERT.getValue()); - - public static final List STAKE_POOL_OPERATIONS = - List.of(OperationType.STAKE_DELEGATION.getValue(), - OperationType.STAKE_KEY_REGISTRATION.getValue(), - OperationType.STAKE_KEY_DEREGISTRATION.getValue(), - OperationType.POOL_RETIREMENT.getValue(), - OperationType.POOL_REGISTRATION.getValue(), - OperationType.POOL_REGISTRATION_WITH_CERT.getValue()); - public static final String OPERATION_TYPE_INPUT = "input"; public static final String OPERATION_TYPE_OUTPUT = "output"; public static final String OPERATION_TYPE_STAKE_KEY_REGISTRATION = "stakeKeyRegistration"; @@ -126,11 +104,6 @@ private Constants() { public static final String OPERATION_TYPE_DREP_VOTE_DELEGATION = "dRepVoteDelegation"; public static final String OPERATION_TYPE_POOL_GOVERNANCE_VOTE = "poolGovernanceVote"; - public static final List GOVERNANCE_OPERATIONS = List.of( - OperationType.VOTE_DREP_DELEGATION.getValue(), - OperationType.POOL_GOVERNANCE_VOTE.getValue() - ); - public static final Integer ED_25519_KEY_SIGNATURE_BYTE_LENGTH = 64; public static final String CERTIFICATE = "certificate"; diff --git a/api/src/main/java/org/cardanofoundation/rosetta/common/util/OperationTypes.java b/api/src/main/java/org/cardanofoundation/rosetta/common/util/OperationTypes.java new file mode 100644 index 0000000000..f204fe1454 --- /dev/null +++ b/api/src/main/java/org/cardanofoundation/rosetta/common/util/OperationTypes.java @@ -0,0 +1,41 @@ +package org.cardanofoundation.rosetta.common.util; + +import com.google.common.collect.ImmutableList; +import org.cardanofoundation.rosetta.common.enumeration.OperationType; + +import java.util.List; + +import static org.cardanofoundation.rosetta.common.enumeration.OperationType.STAKE_DELEGATION; +import static org.cardanofoundation.rosetta.common.util.Constants.OPERATION_TYPE_DREP_VOTE_DELEGATION; + +public final class OperationTypes { + + public static final List STAKING_OPERATIONS = + List.of(STAKE_DELEGATION.getValue(), + OperationType.STAKE_KEY_REGISTRATION.getValue(), + OperationType.STAKE_KEY_DEREGISTRATION.getValue() + ); + + public static final List POOL_OPERATIONS = + List.of(OperationType.POOL_RETIREMENT.getValue(), + OperationType.POOL_REGISTRATION.getValue(), + OperationType.POOL_REGISTRATION_WITH_CERT.getValue()); + + /** + * All certificate-based operations that appear in the transaction body's certs[] array. + *

+ * Includes staking operations (registration, deregistration, delegation), + * pool operations (registration, retirement), and DRep vote delegation. + *

+ *

+ * Note: Withdrawals are NOT certificates. SPO governance votes are voting procedures, not certificates. + *

+ */ + public static final List CERTIFICATE_OPERATIONS = + ImmutableList.builder() + .addAll(STAKING_OPERATIONS) + .addAll(POOL_OPERATIONS) + .add(OPERATION_TYPE_DREP_VOTE_DELEGATION) + .build(); + +} diff --git a/api/src/main/java/org/cardanofoundation/rosetta/common/util/ParseConstructionUtil.java b/api/src/main/java/org/cardanofoundation/rosetta/common/util/ParseConstructionUtil.java index 8c53e97dc7..1dcb596d3e 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/common/util/ParseConstructionUtil.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/common/util/ParseConstructionUtil.java @@ -3,9 +3,11 @@ import co.nstant.in.cbor.CborException; import com.bloxbean.cardano.client.address.Address; import com.bloxbean.cardano.client.common.model.Network; -import com.bloxbean.cardano.client.crypto.bip32.key.HdPublicKey; import com.bloxbean.cardano.client.exception.CborSerializationException; -import com.bloxbean.cardano.client.transaction.spec.*; +import com.bloxbean.cardano.client.transaction.spec.Asset; +import com.bloxbean.cardano.client.transaction.spec.MultiAsset; +import com.bloxbean.cardano.client.transaction.spec.TransactionInput; +import com.bloxbean.cardano.client.transaction.spec.TransactionOutput; import com.bloxbean.cardano.client.transaction.spec.cert.*; import com.bloxbean.cardano.client.transaction.spec.governance.DRep; import com.bloxbean.cardano.client.util.HexUtil; @@ -24,31 +26,30 @@ import java.util.concurrent.atomic.AtomicLong; import static com.bloxbean.cardano.client.address.AddressType.Reward; -import static org.cardanofoundation.rosetta.common.enumeration.OperationType.VOTE_DREP_DELEGATION; import static org.openapitools.client.model.CoinAction.SPENT; @Slf4j -public class ParseConstructionUtil { +public final class ParseConstructionUtil { private ParseConstructionUtil() { } - public static List getOwnerAddressesFromPoolRegistrations(Network network, - PoolRegistration poolRegistration) { - List poolOwners = new ArrayList<>(); - Set owners = poolRegistration.getPoolOwners(); - if (network != null) { - for (String owner : owners) { - Address address = CardanoAddressUtils.getAddress( - null, - HexUtil.decodeHexString(owner), - Constants.STAKE_KEY_HASH_HEADER_KIND, - network, - Reward); - - poolOwners.add(address.getAddress()); - } - } + public static List getOwnerAddressesFromPoolRegistrations(Network network, + PoolRegistration poolRegistration) { + List poolOwners = new ArrayList<>(); + Set owners = poolRegistration.getPoolOwners(); + if (network != null) { + for (String owner : owners) { + Address address = CardanoAddressUtils.getAddress( + null, + HexUtil.decodeHexString(owner), + Constants.STAKE_KEY_HASH_HEADER_KIND, + network, + Reward); + + poolOwners.add(address.getAddress()); + } + } return poolOwners; } @@ -195,135 +196,60 @@ public static Amount parseAsset(List assets, String key) throws CborExcep .build(); } - public static List parseCertsToOperations(TransactionBody transactionBody, - List certOps, - Network network) - throws CborException, CborSerializationException { - List parsedOperations = new ArrayList<>(); - List certs = transactionBody.getCerts(); - int certsCount = getCertSize(certs); - log.info("[parseCertsToOperations] About to parse {} certs", certsCount); - - for (int i = 0; i < certsCount; i++) { - Operation certOperation = certOps.get(i); + public static String getStakingCredentialHex(Operation certOperation) { + String hex = null; + if (checkStakeCredential(certOperation)) { + hex = certOperation.getMetadata().getStakingCredential().getHexBytes(); + } + if (hex == null) { + log.error("[parseCertsToOperations] Missing staking key"); + throw ExceptionFactory.missingStakingKeyError(); + } - Certificate cert = ValidateParseUtil.validateCert(certs, i); + return hex; + } - if (ObjectUtils.isEmpty(cert)) { - continue; - } + // TODO split this into 3 separate parseOperations? + public static Operation parsePoolCertToOperation(Network network, + Certificate cert, + Long index, + String type) + throws CborSerializationException, CborException { - if (Constants.STAKING_OPERATIONS.contains(certOperation.getType())) { - String hex = getStakingCredentialHex(certOperation); - HdPublicKey hdPublicKey = new HdPublicKey(); - hdPublicKey.setKeyData(HexUtil.decodeHexString(hex)); - String address = CardanoAddressUtils.generateRewardAddress(network, hdPublicKey); - - Operation parsedOperation = parseStakingCertToOperation( - cert, - certOperation.getOperationIdentifier().getIndex(), - hex, - certOperation.getType(), - address - ); - - parsedOperation.setAccount(certOperation.getAccount()); - parsedOperations.add(parsedOperation); - } + Operation operation = Operation.builder() + .operationIdentifier(new OperationIdentifier(index, null)) + .type(type) + .status("") + .metadata(new OperationMetadata()) + .build(); - // all certificates related to pool operations - if (Constants.POOL_OPERATIONS.contains(certOperation.getType())) { - if (!ObjectUtils.isEmpty(cert)) { - Operation parsedOperation = parsePoolCertToOperation( - network, - cert, - certOperation.getOperationIdentifier().getIndex(), - certOperation.getType() - ); - parsedOperation.setAccount(certOperation.getAccount()); - parsedOperations.add(parsedOperation); - } - } + if (type.equals(OperationType.POOL_RETIREMENT.getValue())) { + PoolRetirement poolRetirementCert = (PoolRetirement) cert; + operation.getMetadata().setEpoch((int) poolRetirementCert.getEpoch()); + } - // extra type of certificate for vote delegation - if (VOTE_DREP_DELEGATION.getValue().equals(certOperation.getType())) { - String hex = getStakingCredentialHex(certOperation); - HdPublicKey hdPublicKey = new HdPublicKey(); - hdPublicKey.setKeyData(HexUtil.decodeHexString(hex)); - String address = CardanoAddressUtils.generateRewardAddress(network, hdPublicKey); - - Operation parsedOperation = parseDRepVoteDelegation( - cert, - certOperation.getOperationIdentifier().getIndex(), - hex, - certOperation.getType(), - address - ); - parsedOperation.setAccount(certOperation.getAccount()); - parsedOperations.add(parsedOperation); - } + if (type.equals(OperationType.POOL_REGISTRATION.getValue())) { + PoolRegistration poolRegistrationCert = (PoolRegistration) cert; + PoolRegistrationParams poolRegistrationParams = parsePoolRegistration(network, poolRegistrationCert); + operation.getMetadata().setPoolRegistrationParams(poolRegistrationParams); } - return parsedOperations; - } + if (type.equals(OperationType.POOL_REGISTRATION_WITH_CERT.getValue())) { + PoolRegistration poolRegistrationCert = (PoolRegistration) cert; - private static String getStakingCredentialHex(Operation certOperation) { - String hex = null; - if (checkStakeCredential(certOperation)) { - hex = certOperation.getMetadata().getStakingCredential().getHexBytes(); - } - if (hex == null) { - log.error("[parseCertsToOperations] Missing staking key"); - throw ExceptionFactory.missingStakingKeyError(); - } + String parsedPoolCert = HexUtil.encodeHexString( + com.bloxbean.cardano.client.common.cbor.CborSerializationUtil.serialize( + poolRegistrationCert.serialize())); - return hex; - } - - public static Operation parsePoolCertToOperation(Network network, - Certificate cert, - Long index, - String type) - throws CborSerializationException, CborException { - Operation operation = Operation.builder() - .operationIdentifier(new OperationIdentifier(index, null)) - .type(type) - .status("") - .metadata(new OperationMetadata()) - .build(); - - if (type.equals(OperationType.POOL_RETIREMENT.getValue())) { - PoolRetirement poolRetirementCert = (PoolRetirement) cert; - if (!ObjectUtils.isEmpty(poolRetirementCert)) { - operation.getMetadata().setEpoch((int) poolRetirementCert.getEpoch()); - } - } else { - PoolRegistration poolRegistrationCert = null; - try { - poolRegistrationCert = (PoolRegistration) cert; - } catch (Exception e) { - log.info("Not a PoolRegistration"); - } - if (!ObjectUtils.isEmpty(poolRegistrationCert)) { - if (type.equals(OperationType.POOL_REGISTRATION.getValue())) { - PoolRegistrationParams poolRegistrationParams = parsePoolRegistration(network, poolRegistrationCert); - operation.getMetadata().setPoolRegistrationParams(poolRegistrationParams); - } else { - String parsedPoolCert = HexUtil.encodeHexString( - com.bloxbean.cardano.client.common.cbor.CborSerializationUtil.serialize( - poolRegistrationCert.serialize())); - - operation.getMetadata().setPoolRegistrationCert(parsedPoolCert); + operation.getMetadata().setPoolRegistrationCert(parsedPoolCert); } - } - } - return operation; - } + return operation; + } - public static PoolRegistrationParams parsePoolRegistration(Network network, - PoolRegistration poolRegistration) { + static PoolRegistrationParams parsePoolRegistration(Network network, + PoolRegistration poolRegistration) { return new PoolRegistrationParams( HexUtil.encodeHexString(poolRegistration.getVrfKeyHash()), parsePoolRewardAccount(network, poolRegistration), @@ -336,28 +262,6 @@ public static PoolRegistrationParams parsePoolRegistration(Network network, ); } - public static List parseWithdrawalsToOperations(List withdrawalOps, - Integer withdrawalsCount, Network network) { - - log.info("[parseWithdrawalsToOperations] About to parse {} withdrawals", withdrawalsCount); - List withdrawalOperations = new ArrayList<>(); - for (int i = 0; i < withdrawalsCount; i++) { - Operation withdrawalOperation = withdrawalOps.get(i); - String stakingCredentialHex = getStakingCredentialHex(withdrawalOperation); - HdPublicKey hdPublicKey = new HdPublicKey(); - hdPublicKey.setKeyData(HexUtil.decodeHexString(stakingCredentialHex)); - String address = CardanoAddressUtils.generateRewardAddress(network, hdPublicKey); - Operation parsedOperation = parseWithdrawalToOperation( - withdrawalOperation.getAmount().getValue(), - stakingCredentialHex, - withdrawalOperation.getOperationIdentifier().getIndex(), - address - ); - withdrawalOperations.add(parsedOperation); - } - return withdrawalOperations; - } - public static Operation parseWithdrawalToOperation(String value, String hex, Long index, String address) { return Operation.builder() @@ -377,7 +281,7 @@ public static Operation parseWithdrawalToOperation(String value, String hex, Lon .build(); } - public static List parsePoolOwners(Network network, PoolRegistration poolRegistration) { + static List parsePoolOwners(Network network, PoolRegistration poolRegistration) { List poolOwners = new ArrayList<>(); Set owners = poolRegistration.getPoolOwners(); for (String owner : owners) { @@ -392,25 +296,26 @@ public static List parsePoolOwners(Network network, PoolRegistration poo return poolOwners; } - public static String parsePoolRewardAccount(Network network, PoolRegistration poolRegistration) { - String cutRewardAccount = poolRegistration.getRewardAccount(); - if (poolRegistration.getRewardAccount().length() == Constants.HEX_PREFIX_AND_REWARD_ACCOUNT_LENGTH) { - cutRewardAccount = poolRegistration.getRewardAccount().substring(2); - } + static String parsePoolRewardAccount(Network network, PoolRegistration poolRegistration) { + String cutRewardAccount = poolRegistration.getRewardAccount(); + if (poolRegistration.getRewardAccount().length() == Constants.HEX_PREFIX_AND_REWARD_ACCOUNT_LENGTH) { + cutRewardAccount = poolRegistration.getRewardAccount().substring(2); + } - return CardanoAddressUtils.getAddress( - null, - HexUtil.decodeHexString(cutRewardAccount), - (byte) -32, - network, - com.bloxbean.cardano.client.address.AddressType.Reward).getAddress(); - } + return CardanoAddressUtils.getAddress( + null, + HexUtil.decodeHexString(cutRewardAccount), + (byte) -32, + network, + com.bloxbean.cardano.client.address.AddressType.Reward).getAddress(); + } - public static PoolMetadata parsePoolMetadata(PoolRegistration poolRegistration) { + static PoolMetadata parsePoolMetadata(PoolRegistration poolRegistration) { if (poolRegistration.getPoolMetadataUrl() != null || poolRegistration.getPoolMetadataHash() != null) { return new PoolMetadata(poolRegistration.getPoolMetadataUrl(), poolRegistration.getPoolMetadataHash()); } + return null; } @@ -445,36 +350,36 @@ public static void addRelayToPoolReLayOfTypeSingleHostAddr(List poolRelay } } - public static MultiHostName getMultiHostRelay( - com.bloxbean.cardano.client.transaction.spec.cert.Relay relay) { - if (relay instanceof MultiHostName multiHostName) { - return multiHostName; + public static MultiHostName getMultiHostRelay( + com.bloxbean.cardano.client.transaction.spec.cert.Relay relay) { + if (relay instanceof MultiHostName multiHostName) { + return multiHostName; + } + + log.info("not a MultiHostName"); + return null; } - log.info("not a MultiHostName"); - return null; - } + public static SingleHostName getSingleHostName( + com.bloxbean.cardano.client.transaction.spec.cert.Relay relay) { + if (relay instanceof SingleHostName singleHostName) { + return singleHostName; + } - public static SingleHostName getSingleHostName( - com.bloxbean.cardano.client.transaction.spec.cert.Relay relay) { - if (relay instanceof SingleHostName singleHostName) { - return singleHostName; + log.info("not a SingleHostName"); + return null; } - log.info("not a SingleHostName"); - return null; - } + public static SingleHostAddr getSingleHostAddr( + com.bloxbean.cardano.client.transaction.spec.cert.Relay relay) { + if (relay instanceof SingleHostAddr singleHostAddr) { + return singleHostAddr; + } - public static SingleHostAddr getSingleHostAddr( - com.bloxbean.cardano.client.transaction.spec.cert.Relay relay) { - if (relay instanceof SingleHostAddr singleHostAddr) { - return singleHostAddr; + log.info("not a SingleHostAddr"); + return null; } - log.info("not a SingleHostAddr"); - return null; - } - public static void addRelayToPoolRelayOfTypeMultiHost(List poolRelays, MultiHostName multiHostRelay) { if (!ObjectUtils.isEmpty(multiHostRelay)) { @@ -508,55 +413,56 @@ public static boolean checkStakeCredential(Operation certOperation) { && certOperation.getMetadata().getStakingCredential() != null; } - public static Operation parseStakingCertToOperation(Certificate cert, - Long index, - String hash, - String type, - String address) { - Operation operation = new Operation(new OperationIdentifier(index, null), null, type, "", - new AccountIdentifier(address, null, null), null, null, - OperationMetadata.builder().stakingCredential(new PublicKey(hash, CurveType.EDWARDS25519)) - .build()); - - if (cert instanceof StakeDelegation delegationCert) { - operation.getMetadata().setPoolKeyHash( - HexUtil.encodeHexString(delegationCert.getStakePoolId().getPoolKeyHash())); - } + public static Operation parseStakingCertToOperation(Certificate cert, + Long index, + String hash, + String type, + String address) { + Operation operation = new Operation(new OperationIdentifier(index, null), null, type, "", + new AccountIdentifier(address, null, null), null, null, + OperationMetadata.builder().stakingCredential(new PublicKey(hash, CurveType.EDWARDS25519)) + .build()); + + // TODO why only StakeDelegation here , is it because there is additionally pool data there comparing to REG and DE_REG? + if (cert instanceof StakeDelegation delegationCert) { + operation.getMetadata().setPoolKeyHash( + HexUtil.encodeHexString(delegationCert.getStakePoolId().getPoolKeyHash())); + } - return operation; - } - - public static Operation parseDRepVoteDelegation(Certificate cert, - Long index, - String hash, - String type, - String address) { - Operation operation = new Operation(new OperationIdentifier(index, null), null, type, "", - new AccountIdentifier(address, null, null), null, null, - OperationMetadata.builder().stakingCredential(new PublicKey(hash, CurveType.EDWARDS25519)) - .build()); - - VoteDelegCert voteDelegCert = (VoteDelegCert) cert; - - if (!ObjectUtils.isEmpty(voteDelegCert)) { - DRep drep = voteDelegCert.getDrep(); - - DRepTypeParams dRepTypeParams = switch (drep.getType()) { - case ADDR_KEYHASH -> DRepTypeParams.KEY_HASH; - case SCRIPTHASH -> DRepTypeParams.SCRIPT_HASH; - case ABSTAIN -> DRepTypeParams.ABSTAIN; - case NO_CONFIDENCE -> DRepTypeParams.NO_CONFIDENCE; - }; - - DRepParams dRepParams = DRepParams.builder() - .type(dRepTypeParams) - .id(drep.getHash()) - .build(); - - operation.getMetadata().setDrep(dRepParams); + return operation; } - return operation; - } + public static Operation parseDRepVoteDelegation(Certificate cert, + Long index, + String hash, + String type, + String address) { + Operation operation = new Operation(new OperationIdentifier(index, null), null, type, "", + new AccountIdentifier(address, null, null), null, null, + OperationMetadata.builder().stakingCredential(new PublicKey(hash, CurveType.EDWARDS25519)) + .build()); + + VoteDelegCert voteDelegCert = (VoteDelegCert) cert; + + if (!ObjectUtils.isEmpty(voteDelegCert)) { + DRep drep = voteDelegCert.getDrep(); + + DRepTypeParams dRepTypeParams = switch (drep.getType()) { + case ADDR_KEYHASH -> DRepTypeParams.KEY_HASH; + case SCRIPTHASH -> DRepTypeParams.SCRIPT_HASH; + case ABSTAIN -> DRepTypeParams.ABSTAIN; + case NO_CONFIDENCE -> DRepTypeParams.NO_CONFIDENCE; + }; + + DRepParams dRepParams = DRepParams.builder() + .type(dRepTypeParams) + .id(drep.getHash()) + .build(); + + operation.getMetadata().setDrep(dRepParams); + } + + return operation; + } } diff --git a/api/src/test/java/org/cardanofoundation/rosetta/api/block/model/repository/util/TxRepositoryQueryBuilderTest.java b/api/src/test/java/org/cardanofoundation/rosetta/api/block/model/repository/util/TxRepositoryQueryBuilderTest.java new file mode 100644 index 0000000000..a71e85bcfe --- /dev/null +++ b/api/src/test/java/org/cardanofoundation/rosetta/api/block/model/repository/util/TxRepositoryQueryBuilderTest.java @@ -0,0 +1,387 @@ +package org.cardanofoundation.rosetta.api.block.model.repository.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.cardanofoundation.rosetta.api.block.model.entity.BlockEntity; +import org.cardanofoundation.rosetta.api.block.model.entity.TransactionSizeEntity; +import org.cardanofoundation.rosetta.api.block.model.entity.TxnEntity; +import org.cardanofoundation.rosetta.api.block.model.entity.UtxoKey; +import org.jooq.JSONB; +import org.jooq.Record; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.annotation.Nullable; +import java.math.BigInteger; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.cardanofoundation.rosetta.api.jooq.Tables.*; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TxRepositoryQueryBuilderTest { + + @Mock + private Record mockRecord; + + private ObjectMapper objectMapper; + private TxRepositoryQueryBuilder queryBuilder; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + queryBuilder = new TxRepositoryQueryBuilder(objectMapper); + } + + @Nested + class MapRecordToTxnEntityTests { + + @Test + void shouldMapRecordWithTxIndex() throws JsonProcessingException { + // given + String txHash = "tx123"; + String blockHash = "block456"; + Long blockNumber = 100L; + Long blockSlot = 1000L; + Long fee = 170000L; + Integer txIndex = 5; + Integer txSize = 300; + Integer scriptSize = 50; + + List inputKeys = List.of( + new UtxoKey("prevtx1", 0) + ); + List outputKeys = List.of( + new UtxoKey(txHash, 0) + ); + + JSONB inputsJsonb = JSONB.jsonb(objectMapper.writeValueAsString(inputKeys)); + JSONB outputsJsonb = JSONB.jsonb(objectMapper.writeValueAsString(outputKeys)); + + when(mockRecord.get(TRANSACTION.TX_HASH)).thenReturn(txHash); + when(mockRecord.get(TRANSACTION.INPUTS)).thenReturn(inputsJsonb); + when(mockRecord.get(TRANSACTION.OUTPUTS)).thenReturn(outputsJsonb); + when(mockRecord.get(TRANSACTION.FEE)).thenReturn(fee); + when(mockRecord.get(TRANSACTION.TX_INDEX)).thenReturn(txIndex); + when(mockRecord.get("joined_block_hash", String.class)).thenReturn(blockHash); + when(mockRecord.get("joined_block_number", Long.class)).thenReturn(blockNumber); + when(mockRecord.get("joined_block_slot", Long.class)).thenReturn(blockSlot); + when(mockRecord.get("joined_tx_size", Integer.class)).thenReturn(txSize); + when(mockRecord.get("joined_tx_script_size", Integer.class)).thenReturn(scriptSize); + + // when + TxnEntity result = queryBuilder.mapRecordToTxnEntity(mockRecord); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTxHash()).isEqualTo(txHash); + assertThat(result.getFee()).isEqualTo(BigInteger.valueOf(fee)); + assertThat(result.getTxIndex()).isEqualTo(txIndex); + assertThat(result.getInputKeys()).hasSize(1); + assertThat(result.getOutputKeys()).hasSize(1); + + assertThat(result.getBlock()).isNotNull(); + assertThat(result.getBlock().getHash()).isEqualTo(blockHash); + assertThat(result.getBlock().getNumber()).isEqualTo(blockNumber); + assertThat(result.getBlock().getSlot()).isEqualTo(blockSlot); + + assertThat(result.getSizeEntity()).isNotNull(); + assertThat(result.getSizeEntity().getTxHash()).isEqualTo(txHash); + assertThat(result.getSizeEntity().getSize()).isEqualTo(txSize); + assertThat(result.getSizeEntity().getScriptSize()).isEqualTo(scriptSize); + } + + @Test + void shouldHandleNullTxIndex() throws JsonProcessingException { + // given - simulating old data before tx_index was added + String txHash = "tx789"; + Long fee = 150000L; + + List inputKeys = List.of(); + List outputKeys = List.of( + new UtxoKey(txHash, 0) + ); + + JSONB inputsJsonb = JSONB.jsonb(objectMapper.writeValueAsString(inputKeys)); + JSONB outputsJsonb = JSONB.jsonb(objectMapper.writeValueAsString(outputKeys)); + + when(mockRecord.get(TRANSACTION.TX_HASH)).thenReturn(txHash); + when(mockRecord.get(TRANSACTION.INPUTS)).thenReturn(inputsJsonb); + when(mockRecord.get(TRANSACTION.OUTPUTS)).thenReturn(outputsJsonb); + when(mockRecord.get(TRANSACTION.FEE)).thenReturn(fee); + when(mockRecord.get(TRANSACTION.TX_INDEX)).thenReturn(null); + when(mockRecord.get("joined_block_hash", String.class)).thenReturn(null); + + // when + TxnEntity result = queryBuilder.mapRecordToTxnEntity(mockRecord); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTxHash()).isEqualTo(txHash); + assertThat(result.getTxIndex()).isNull(); + assertThat(result.getFee()).isEqualTo(BigInteger.valueOf(fee)); + } + + @Test + void shouldHandleTxIndexZero() throws JsonProcessingException { + // given - first transaction in a block has tx_index = 0 + String txHash = "first_tx_in_block"; + Integer txIndex = 0; + + List inputKeys = List.of(); + List outputKeys = List.of(); + + JSONB inputsJsonb = JSONB.jsonb(objectMapper.writeValueAsString(inputKeys)); + JSONB outputsJsonb = JSONB.jsonb(objectMapper.writeValueAsString(outputKeys)); + + when(mockRecord.get(TRANSACTION.TX_HASH)).thenReturn(txHash); + when(mockRecord.get(TRANSACTION.INPUTS)).thenReturn(inputsJsonb); + when(mockRecord.get(TRANSACTION.OUTPUTS)).thenReturn(outputsJsonb); + when(mockRecord.get(TRANSACTION.FEE)).thenReturn(null); + when(mockRecord.get(TRANSACTION.TX_INDEX)).thenReturn(txIndex); + when(mockRecord.get("joined_block_hash", String.class)).thenReturn(null); + + // when + TxnEntity result = queryBuilder.mapRecordToTxnEntity(mockRecord); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTxIndex()).isEqualTo(0); + } + + @Test + void shouldHandleHighTxIndex() throws JsonProcessingException { + // given - block with many transactions + String txHash = "last_tx_in_large_block"; + Integer txIndex = 9999; + + List inputKeys = List.of(); + List outputKeys = List.of(); + + JSONB inputsJsonb = JSONB.jsonb(objectMapper.writeValueAsString(inputKeys)); + JSONB outputsJsonb = JSONB.jsonb(objectMapper.writeValueAsString(outputKeys)); + + when(mockRecord.get(TRANSACTION.TX_HASH)).thenReturn(txHash); + when(mockRecord.get(TRANSACTION.INPUTS)).thenReturn(inputsJsonb); + when(mockRecord.get(TRANSACTION.OUTPUTS)).thenReturn(outputsJsonb); + when(mockRecord.get(TRANSACTION.FEE)).thenReturn(null); + when(mockRecord.get(TRANSACTION.TX_INDEX)).thenReturn(txIndex); + when(mockRecord.get("joined_block_hash", String.class)).thenReturn(null); + + // when + TxnEntity result = queryBuilder.mapRecordToTxnEntity(mockRecord); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTxIndex()).isEqualTo(9999); + } + + @Test + void shouldMapWithoutBlockData() throws JsonProcessingException { + // given - transaction without joined block data + String txHash = "tx_without_block"; + Integer txIndex = 3; + + List inputKeys = List.of(); + List outputKeys = List.of(); + + JSONB inputsJsonb = JSONB.jsonb(objectMapper.writeValueAsString(inputKeys)); + JSONB outputsJsonb = JSONB.jsonb(objectMapper.writeValueAsString(outputKeys)); + + when(mockRecord.get(TRANSACTION.TX_HASH)).thenReturn(txHash); + when(mockRecord.get(TRANSACTION.INPUTS)).thenReturn(inputsJsonb); + when(mockRecord.get(TRANSACTION.OUTPUTS)).thenReturn(outputsJsonb); + when(mockRecord.get(TRANSACTION.FEE)).thenReturn(null); + when(mockRecord.get(TRANSACTION.TX_INDEX)).thenReturn(txIndex); + when(mockRecord.get("joined_block_hash", String.class)).thenReturn(null); + // No need to mock block_number, block_slot, tx_size, script_size when block_hash is null + + // when + TxnEntity result = queryBuilder.mapRecordToTxnEntity(mockRecord); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTxHash()).isEqualTo(txHash); + assertThat(result.getTxIndex()).isEqualTo(txIndex); + assertThat(result.getBlock()).isNull(); + assertThat(result.getSizeEntity()).isNull(); + } + + @Test + void shouldHandleNullInputsAndOutputs() { + // given + String txHash = "tx_with_null_io"; + Integer txIndex = 1; + + when(mockRecord.get(TRANSACTION.TX_HASH)).thenReturn(txHash); + when(mockRecord.get(TRANSACTION.INPUTS)).thenReturn(null); + when(mockRecord.get(TRANSACTION.OUTPUTS)).thenReturn(null); + when(mockRecord.get(TRANSACTION.FEE)).thenReturn(null); + when(mockRecord.get(TRANSACTION.TX_INDEX)).thenReturn(txIndex); + when(mockRecord.get("joined_block_hash", String.class)).thenReturn(null); + + // when + TxnEntity result = queryBuilder.mapRecordToTxnEntity(mockRecord); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTxHash()).isEqualTo(txHash); + assertThat(result.getTxIndex()).isEqualTo(txIndex); + assertThat(result.getInputKeys()).isEmpty(); + assertThat(result.getOutputKeys()).isEmpty(); + } + + @Test + void shouldHandleComplexUtxoKeysWithTxIndex() throws JsonProcessingException { + // given + String txHash = "complex_tx"; + Integer txIndex = 42; + + List inputKeys = List.of( + new UtxoKey("input1", 0), + new UtxoKey("input2", 1), + new UtxoKey("input3", 2) + ); + List outputKeys = List.of( + new UtxoKey(txHash, 0), + new UtxoKey(txHash, 1) + ); + + JSONB inputsJsonb = JSONB.jsonb(objectMapper.writeValueAsString(inputKeys)); + JSONB outputsJsonb = JSONB.jsonb(objectMapper.writeValueAsString(outputKeys)); + + when(mockRecord.get(TRANSACTION.TX_HASH)).thenReturn(txHash); + when(mockRecord.get(TRANSACTION.INPUTS)).thenReturn(inputsJsonb); + when(mockRecord.get(TRANSACTION.OUTPUTS)).thenReturn(outputsJsonb); + when(mockRecord.get(TRANSACTION.FEE)).thenReturn(200000L); + when(mockRecord.get(TRANSACTION.TX_INDEX)).thenReturn(txIndex); + when(mockRecord.get("joined_block_hash", String.class)).thenReturn(null); + + // when + TxnEntity result = queryBuilder.mapRecordToTxnEntity(mockRecord); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTxHash()).isEqualTo(txHash); + assertThat(result.getTxIndex()).isEqualTo(42); + assertThat(result.getInputKeys()).hasSize(3); + assertThat(result.getOutputKeys()).hasSize(2); + assertThat(result.getInputKeys().get(0).getTxHash()).isEqualTo("input1"); + assertThat(result.getInputKeys().get(1).getTxHash()).isEqualTo("input2"); + assertThat(result.getInputKeys().get(2).getTxHash()).isEqualTo("input3"); + } + } + + @Nested + class BackwardCompatibilityTests { + + @Test + void shouldHandleLegacyRecordsWithoutTxIndex() throws JsonProcessingException { + // given - simulating records from database before tx_index column was added + String txHash = "legacy_tx"; + String blockHash = "legacy_block"; + Long blockNumber = 50L; + Long blockSlot = 500L; + + List inputKeys = List.of(); + List outputKeys = List.of(); + + JSONB inputsJsonb = JSONB.jsonb(objectMapper.writeValueAsString(inputKeys)); + JSONB outputsJsonb = JSONB.jsonb(objectMapper.writeValueAsString(outputKeys)); + + when(mockRecord.get(TRANSACTION.TX_HASH)).thenReturn(txHash); + when(mockRecord.get(TRANSACTION.INPUTS)).thenReturn(inputsJsonb); + when(mockRecord.get(TRANSACTION.OUTPUTS)).thenReturn(outputsJsonb); + when(mockRecord.get(TRANSACTION.FEE)).thenReturn(null); + when(mockRecord.get(TRANSACTION.TX_INDEX)).thenReturn(null); // No tx_index + when(mockRecord.get("joined_block_hash", String.class)).thenReturn(blockHash); + when(mockRecord.get("joined_block_number", Long.class)).thenReturn(blockNumber); + when(mockRecord.get("joined_block_slot", Long.class)).thenReturn(blockSlot); + when(mockRecord.get("joined_tx_size", Integer.class)).thenReturn(null); + when(mockRecord.get("joined_tx_script_size", Integer.class)).thenReturn(null); + + // when + TxnEntity result = queryBuilder.mapRecordToTxnEntity(mockRecord); + + // then - should work fine with null txIndex + assertThat(result).isNotNull(); + assertThat(result.getTxHash()).isEqualTo(txHash); + assertThat(result.getTxIndex()).isNull(); + assertThat(result.getBlock()).isNotNull(); + assertThat(result.getBlock().getHash()).isEqualTo(blockHash); + } + } + + @Nested + class EdgeCaseTests { + + @Test + void shouldHandleTransactionSizeWithoutScriptSize() throws JsonProcessingException { + // given + String txHash = "tx_no_script"; + Integer txIndex = 1; + Integer txSize = 250; + + List inputKeys = List.of(); + List outputKeys = List.of(); + + JSONB inputsJsonb = JSONB.jsonb(objectMapper.writeValueAsString(inputKeys)); + JSONB outputsJsonb = JSONB.jsonb(objectMapper.writeValueAsString(outputKeys)); + + when(mockRecord.get(TRANSACTION.TX_HASH)).thenReturn(txHash); + when(mockRecord.get(TRANSACTION.INPUTS)).thenReturn(inputsJsonb); + when(mockRecord.get(TRANSACTION.OUTPUTS)).thenReturn(outputsJsonb); + when(mockRecord.get(TRANSACTION.FEE)).thenReturn(null); + when(mockRecord.get(TRANSACTION.TX_INDEX)).thenReturn(txIndex); + when(mockRecord.get("joined_block_hash", String.class)).thenReturn("block1"); + when(mockRecord.get("joined_block_number", Long.class)).thenReturn(10L); + when(mockRecord.get("joined_block_slot", Long.class)).thenReturn(100L); + when(mockRecord.get("joined_tx_size", Integer.class)).thenReturn(txSize); + when(mockRecord.get("joined_tx_script_size", Integer.class)).thenReturn(null); + + // when + TxnEntity result = queryBuilder.mapRecordToTxnEntity(mockRecord); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTxIndex()).isEqualTo(txIndex); + assertThat(result.getSizeEntity()).isNotNull(); + assertThat(result.getSizeEntity().getSize()).isEqualTo(txSize); + assertThat(result.getSizeEntity().getScriptSize()).isEqualTo(0); // Default value + } + + @Test + void shouldHandleNullFee() throws JsonProcessingException { + // given + String txHash = "tx_no_fee"; + Integer txIndex = 2; + + List inputKeys = List.of(); + List outputKeys = List.of(); + + JSONB inputsJsonb = JSONB.jsonb(objectMapper.writeValueAsString(inputKeys)); + JSONB outputsJsonb = JSONB.jsonb(objectMapper.writeValueAsString(outputKeys)); + + when(mockRecord.get(TRANSACTION.TX_HASH)).thenReturn(txHash); + when(mockRecord.get(TRANSACTION.INPUTS)).thenReturn(inputsJsonb); + when(mockRecord.get(TRANSACTION.OUTPUTS)).thenReturn(outputsJsonb); + when(mockRecord.get(TRANSACTION.FEE)).thenReturn(null); + when(mockRecord.get(TRANSACTION.TX_INDEX)).thenReturn(txIndex); + when(mockRecord.get("joined_block_hash", String.class)).thenReturn(null); + + // when + TxnEntity result = queryBuilder.mapRecordToTxnEntity(mockRecord); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTxHash()).isEqualTo(txHash); + assertThat(result.getTxIndex()).isEqualTo(txIndex); + assertThat(result.getFee()).isNull(); + } + } +} diff --git a/api/src/test/java/org/cardanofoundation/rosetta/api/construction/service/CardanoConstructionServiceImplTest.java b/api/src/test/java/org/cardanofoundation/rosetta/api/construction/service/CardanoConstructionServiceImplTest.java index 15085c8219..6abad73877 100644 --- a/api/src/test/java/org/cardanofoundation/rosetta/api/construction/service/CardanoConstructionServiceImplTest.java +++ b/api/src/test/java/org/cardanofoundation/rosetta/api/construction/service/CardanoConstructionServiceImplTest.java @@ -78,7 +78,7 @@ class CardanoConstructionServiceImplTest { @BeforeEach void setup() { cardanoService = new CardanoConstructionServiceImpl(null, null, - new OperationService(), restTemplate, offlineSlotService); + new TransactionOperationParserImpl(), restTemplate, offlineSlotService); completableFutureMock = Mockito.mockStatic(CompletableFuture.class, invocation -> { if (invocation.getMethod().getName().equals("supplyAsync")) { Supplier supplier = invocation.getArgument(0); diff --git a/api/src/test/java/org/cardanofoundation/rosetta/common/mapper/OperationServiceTest.java b/api/src/test/java/org/cardanofoundation/rosetta/common/mapper/OperationServiceTest.java deleted file mode 100644 index f32ae60b18..0000000000 --- a/api/src/test/java/org/cardanofoundation/rosetta/common/mapper/OperationServiceTest.java +++ /dev/null @@ -1,210 +0,0 @@ -package org.cardanofoundation.rosetta.common.mapper; - -import co.nstant.in.cbor.CborException; -import com.bloxbean.cardano.client.exception.CborDeserializationException; -import com.bloxbean.cardano.client.exception.CborSerializationException; -import com.bloxbean.cardano.client.transaction.spec.TransactionBody; -import com.bloxbean.cardano.client.transaction.spec.TransactionInput; -import com.bloxbean.cardano.client.transaction.spec.Withdrawal; -import org.cardanofoundation.rosetta.api.construction.service.OperationService; -import org.cardanofoundation.rosetta.common.enumeration.NetworkEnum; -import org.cardanofoundation.rosetta.common.enumeration.OperationType; -import org.cardanofoundation.rosetta.common.exception.ApiException; -import org.cardanofoundation.rosetta.common.model.cardano.transaction.TransactionData; -import org.cardanofoundation.rosetta.common.model.cardano.transaction.TransactionExtraData; -import org.cardanofoundation.rosetta.common.util.RosettaConstants.RosettaErrorType; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; -import org.openapitools.client.model.*; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@ExtendWith(MockitoExtension.class) -class OperationServiceTest { - - OperationService operationService = new OperationService(); - - @Test - void getSignerFromOperation_poolOperationType_test() { - Operation operation = creteTestPoolRegistrationOperation("addr1", "rewardAddress", - List.of("poolOwner1", "poolOwner2")); - - List poolSigners = operationService.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation); - - assertThat(poolSigners) - .hasSize(4) - .contains("addr1", "rewardAddress", "poolOwner1", "poolOwner2"); - } - - @Test - void getSignerFromOperation_poolOperationRetirementType_test() { - Operation operation = creteTestPoolRegistrationOperation("addr1", "rewardAddress", - List.of("poolOwner1", "poolOwner2")); - operation.setType(OperationType.POOL_RETIREMENT.getValue()); - - List poolSigners = operationService.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation); - - assertThat(poolSigners).hasSize(1).contains("addr1"); - } - - @Test - void getSignerFromOperation_poolOperationTypeWithCertificate_test() { - Operation operation = creteTestPoolRegistrationOperation("addr1", "", null); - operation.setType(OperationType.POOL_REGISTRATION_WITH_CERT.getValue()); - operation.getMetadata().setPoolRegistrationCert( - "8a03581c1b268f4cba3faa7e36d8a0cc4adca2096fb856119412ee7330f692b558208dd154228946bd12967c12bedb1cb6038b78f8b84a1760b1a788fa72a4af3db01a004c4b401a002dc6c0d81e820101581de1bb40f1a647bc88c1bd6b738db8eb66357d926474ea5ffd6baa76c9fb81581c7a9a4d5a6ac7a9d8702818fa3ea533e56c4f1de16da611a730ee3f008184001820445820f5d9505820f5d9ea167fd2e0b19647f18dd1e0826f706f6f6c4d6574616461746155726c58209ac2217288d1ae0b4e15c41b58d3e05a13206fd9ab81cb15943e4174bf30c90b"); - - List poolSigners = operationService.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation); - - assertThat(poolSigners) - .hasSize(3) - .contains("stake1uxa5pudxg77g3sdaddecmw8tvc6hmynywn49lltt4fmvn7caek7a5", "addr1", - "stake1u9af5n26dtr6nkrs9qv05049x0jkcncau9k6vyd8xrhr7qq8tez5p"); - } - - @SuppressWarnings("java:S5778") - @Test - void getSignerFromOperation_poolOperationTypeWithCertificateNullable_test() { - Operation operation = creteTestPoolRegistrationOperation("addr1", null, null); - operation.setType(OperationType.POOL_REGISTRATION_WITH_CERT.getValue()); - operation.getMetadata().setPoolRegistrationCert(null); - - ApiException actualException = assertThrows(ApiException.class, - () -> operationService.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation)); - - assertThat(actualException.getError().getMessage()) - .isEqualTo(RosettaErrorType.POOL_CERT_MISSING.getMessage()); - assertThat(actualException.getError().getCode()) - .isEqualTo(RosettaErrorType.POOL_CERT_MISSING.getCode()); - - operation.setMetadata(null); - actualException = assertThrows(ApiException.class, - () -> operationService.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation)); - - assertThat(actualException.getError().getMessage()) - .isEqualTo(RosettaErrorType.POOL_CERT_MISSING.getMessage()); - assertThat(actualException.getError().getCode()) - .isEqualTo(RosettaErrorType.POOL_CERT_MISSING.getCode()); - } - - @Test - void getSignerFromOperation_poolOperationTypeNullable_test() { - Operation operation = creteTestPoolRegistrationOperation(null, "rewardAddress", - List.of("poolOwner1", "poolOwner2")); - - List poolSigners = operationService.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation); - - assertThat(poolSigners) - .hasSize(3) - .contains("rewardAddress", "poolOwner1", "poolOwner2"); - - operation.setAccount(null); - poolSigners = operationService.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation); - assertThat(poolSigners) - .hasSize(3) - .contains("rewardAddress", "poolOwner1", "poolOwner2"); - - operation.getMetadata().setPoolRegistrationParams(null); - poolSigners = operationService.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation); - assertThat(poolSigners).isEmpty(); - - operation.setMetadata(null); - poolSigners = operationService.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation); - assertThat(poolSigners).isEmpty(); - } - - @Test - void getSignerFromOperation_stakingCredentials_test() { - Operation operation = Operation - .builder() - .type("someType") - .metadata(OperationMetadata.builder() - .stakingCredential(PublicKey.builder() - .curveType(CurveType.EDWARDS25519) - .hexBytes("1B400D60AAF34EAF6DCBAB9BBA46001A23497886CF11066F7846933D30E5AD3F") - .build()) - .build()) - .build(); - - List poolSigners = operationService.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation); - - assertThat(poolSigners) - .hasSize(1) - .contains("stake1uxa5pudxg77g3sdaddecmw8tvc6hmynywn49lltt4fmvn7caek7a5"); - } - - @SuppressWarnings("java:S5778") - @Test - void getSignerFromOperation_negative_test() { - Operation operation = Operation.builder().metadata(null).type("invalidType").build(); - - ApiException actualException = assertThrows(ApiException.class, - () -> operationService.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation)); - - assertThat(actualException.getError().getMessage()) - .isEqualTo(RosettaErrorType.STAKING_KEY_MISSING.getMessage()); - assertThat(actualException.getError().getCode()) - .isEqualTo(RosettaErrorType.STAKING_KEY_MISSING.getCode()); - } - - @Test - void getOperationsFromTransactionData() - throws CborException, CborDeserializationException, CborSerializationException { - TransactionData transactionData = getPoolTransactionData1(); - transactionData.transactionBody().setInputs(List.of(new TransactionInput())); - transactionData.transactionBody().setWithdrawals(List.of(new Withdrawal())); - Operation withdrawalOperation = transactionData.transactionExtraData().operations().getFirst(); - withdrawalOperation.setType(OperationType.WITHDRAWAL.getValue()); - withdrawalOperation.setMetadata(OperationMetadata.builder() - .stakingCredential( - PublicKey.builder() - .curveType(CurveType.EDWARDS25519) - .hexBytes("1B400D60AAF34EAF6DCBAB9BBA46001A23497886CF11066F7846933D30E5AD3F") - .build()) - .build()); - withdrawalOperation.setOperationIdentifier(OperationIdentifier.builder().index(22L).build()); - withdrawalOperation.setAmount(Amount.builder().value("value").build()); - - List operations = operationService - .getOperationsFromTransactionData(transactionData, NetworkEnum.MAINNET.getNetwork()); - - assertThat(operations).hasSize(2); - assertThat(operations.get(1).getType()).isEqualTo(OperationType.WITHDRAWAL.getValue()); - assertThat(operations.get(1).getOperationIdentifier().getIndex()).isEqualTo(22L); - assertThat(operations.get(1).getAmount().getValue()).isEqualTo("value"); - assertThat(operations.get(1).getMetadata().getStakingCredential().getHexBytes()) - .isEqualTo("1B400D60AAF34EAF6DCBAB9BBA46001A23497886CF11066F7846933D30E5AD3F"); - } - - private static Operation creteTestPoolRegistrationOperation(String accountAddress, String rewardAddress, - List poolOwners) { - return Operation.builder() - .type(OperationType.POOL_REGISTRATION.getValue()) - .account(AccountIdentifier.builder() - .address(accountAddress) - .build()) - .metadata(OperationMetadata.builder() - .poolRegistrationParams(PoolRegistrationParams.builder() - .rewardAddress(rewardAddress) - .poolOwners(poolOwners) - .build()) - .build()) - .build(); - } - - private static TransactionData getPoolTransactionData1() { - Operation operation1 = creteTestPoolRegistrationOperation("addr1", "rewardAddress", List.of("poolOwner1", "poolOwner2")); - Operation operation2 = creteTestPoolRegistrationOperation("addr2", "rewardAddress", List.of("poolOwner3", "poolOwner4")); - TransactionExtraData transactionExtraData = new TransactionExtraData(List.of(operation1, operation2)); - - return new TransactionData( - TransactionBody.builder().build(), - transactionExtraData - ); - } - -} diff --git a/api/src/test/java/org/cardanofoundation/rosetta/common/mapper/TransactionOperationParserImplTest.java b/api/src/test/java/org/cardanofoundation/rosetta/common/mapper/TransactionOperationParserImplTest.java new file mode 100644 index 0000000000..093b382f07 --- /dev/null +++ b/api/src/test/java/org/cardanofoundation/rosetta/common/mapper/TransactionOperationParserImplTest.java @@ -0,0 +1,1025 @@ +package org.cardanofoundation.rosetta.common.mapper; + +import co.nstant.in.cbor.CborException; +import com.bloxbean.cardano.client.exception.CborDeserializationException; +import com.bloxbean.cardano.client.exception.CborSerializationException; +import com.bloxbean.cardano.client.transaction.spec.TransactionBody; +import com.bloxbean.cardano.client.transaction.spec.TransactionInput; +import com.bloxbean.cardano.client.transaction.spec.Withdrawal; +import org.cardanofoundation.rosetta.api.construction.service.TransactionOperationParserImpl; +import org.cardanofoundation.rosetta.common.enumeration.NetworkEnum; +import org.cardanofoundation.rosetta.common.enumeration.OperationType; +import org.cardanofoundation.rosetta.common.exception.ApiException; +import org.cardanofoundation.rosetta.common.model.cardano.transaction.TransactionData; +import org.cardanofoundation.rosetta.common.model.cardano.transaction.TransactionExtraData; +import org.cardanofoundation.rosetta.common.util.RosettaConstants.RosettaErrorType; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openapitools.client.model.*; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Unit tests for {@link TransactionOperationParserImpl}. + *

+ * Tests are organized by the public methods they test: + *

    + *
  • {@link GetSignerFromOperationTests} - Tests for signer extraction from operations
  • + *
  • {@link GetOperationsFromTransactionDataTests} - Tests for operation extraction from transaction data
  • + *
+ *

+ */ +@ExtendWith(MockitoExtension.class) +class TransactionOperationParserImplTest { + + TransactionOperationParserImpl parser = new TransactionOperationParserImpl(); + + /** + * Tests for {@link TransactionOperationParserImpl#getSignerFromOperation(com.bloxbean.cardano.client.common.model.Network, Operation)}. + */ + @Nested + class GetSignerFromOperationTests { + + /** + * Tests for pool-related operations (registration, retirement, registration with certificate). + */ + @Nested + class PoolOperations { + + @Test + void shouldExtractAllSignersFromPoolRegistrationOperation() { + Operation operation = createTestPoolRegistrationOperation("addr1", "rewardAddress", + List.of("poolOwner1", "poolOwner2")); + + List poolSigners = parser.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation); + + assertThat(poolSigners) + .hasSize(4) + .contains("addr1", "rewardAddress", "poolOwner1", "poolOwner2"); + } + + @Test + void shouldExtractPaymentAddressFromPoolRetirementOperation() { + Operation operation = createTestPoolRegistrationOperation("addr1", "rewardAddress", + List.of("poolOwner1", "poolOwner2")); + operation.setType(OperationType.POOL_RETIREMENT.getValue()); + + List poolSigners = parser.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation); + + assertThat(poolSigners).hasSize(1).contains("addr1"); + } + + @Test + void shouldExtractSignersFromPoolRegistrationCertificate() { + Operation operation = createTestPoolRegistrationOperation("addr1", "", null); + operation.setType(OperationType.POOL_REGISTRATION_WITH_CERT.getValue()); + operation.getMetadata().setPoolRegistrationCert( + "8a03581c1b268f4cba3faa7e36d8a0cc4adca2096fb856119412ee7330f692b558208dd154228946bd12967c12bedb1cb6038b78f8b84a1760b1a788fa72a4af3db01a004c4b401a002dc6c0d81e820101581de1bb40f1a647bc88c1bd6b738db8eb66357d926474ea5ffd6baa76c9fb81581c7a9a4d5a6ac7a9d8702818fa3ea533e56c4f1de16da611a730ee3f008184001820445820f5d9505820f5d9ea167fd2e0b19647f18dd1e0826f706f6f6c4d6574616461746155726c58209ac2217288d1ae0b4e15c41b58d3e05a13206fd9ab81cb15943e4174bf30c90b"); + + List poolSigners = parser.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation); + + assertThat(poolSigners) + .hasSize(3) + .contains("stake1uxa5pudxg77g3sdaddecmw8tvc6hmynywn49lltt4fmvn7caek7a5", "addr1", + "stake1u9af5n26dtr6nkrs9qv05049x0jkcncau9k6vyd8xrhr7qq8tez5p"); + } + + @Test + void shouldHandleNullPaymentAddressInPoolRegistration() { + Operation operation = createTestPoolRegistrationOperation(null, "rewardAddress", + List.of("poolOwner1", "poolOwner2")); + + List poolSigners = parser.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation); + + assertThat(poolSigners) + .hasSize(3) + .contains("rewardAddress", "poolOwner1", "poolOwner2"); + + operation.setAccount(null); + poolSigners = parser.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation); + assertThat(poolSigners) + .hasSize(3) + .contains("rewardAddress", "poolOwner1", "poolOwner2"); + + operation.getMetadata().setPoolRegistrationParams(null); + poolSigners = parser.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation); + assertThat(poolSigners).isEmpty(); + + operation.setMetadata(null); + poolSigners = parser.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation); + assertThat(poolSigners).isEmpty(); + } + + @SuppressWarnings("java:S5778") + @Test + void shouldThrowExceptionWhenPoolCertificateIsMissing() { + Operation operation = createTestPoolRegistrationOperation("addr1", null, null); + operation.setType(OperationType.POOL_REGISTRATION_WITH_CERT.getValue()); + operation.getMetadata().setPoolRegistrationCert(null); + + ApiException actualException = assertThrows(ApiException.class, + () -> parser.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation)); + + assertThat(actualException.getError().getMessage()) + .isEqualTo(RosettaErrorType.POOL_CERT_MISSING.getMessage()); + assertThat(actualException.getError().getCode()) + .isEqualTo(RosettaErrorType.POOL_CERT_MISSING.getCode()); + + operation.setMetadata(null); + actualException = assertThrows(ApiException.class, + () -> parser.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation)); + + assertThat(actualException.getError().getMessage()) + .isEqualTo(RosettaErrorType.POOL_CERT_MISSING.getMessage()); + assertThat(actualException.getError().getCode()) + .isEqualTo(RosettaErrorType.POOL_CERT_MISSING.getCode()); + } + } + + /** + * Tests for staking-related operations that use staking credentials. + */ + @Nested + class StakingOperations { + + @Test + void shouldDeriveRewardAddressFromStakingCredential() { + Operation operation = Operation + .builder() + .type("someType") + .metadata(OperationMetadata.builder() + .stakingCredential(PublicKey.builder() + .curveType(CurveType.EDWARDS25519) + .hexBytes("1B400D60AAF34EAF6DCBAB9BBA46001A23497886CF11066F7846933D30E5AD3F") + .build()) + .build()) + .build(); + + List poolSigners = parser.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation); + + assertThat(poolSigners) + .hasSize(1) + .contains("stake1uxa5pudxg77g3sdaddecmw8tvc6hmynywn49lltt4fmvn7caek7a5"); + } + } + + /** + * Tests for governance-related operations (Conway era). + */ + @Nested + class GovernanceOperations { + + @Test + void shouldExtractSignerFromDRepVoteDelegation() { + Operation operation = Operation.builder() + .type(OperationType.VOTE_DREP_DELEGATION.getValue()) + .account(AccountIdentifier.builder() + .address("stake_test1uql3k2h9kskfma8y53vth7wmptmlw8zxx67cx7c3pwj8sqs6zl0wr") + .build()) + .metadata(OperationMetadata.builder() + .stakingCredential(PublicKey.builder() + .hexBytes("fdbee86a702c49d23f45ab80e697b93ec48b744d5f413ef59c338c3f7b2d2de8") + .curveType(CurveType.EDWARDS25519) + .build()) + .drep(DRepParams.builder() + .id("f72050aa252ccc6bf75747184910c0b9386298167656f935d6b6c26a") + .type(DRepTypeParams.KEY_HASH) + .build()) + .build()) + .build(); + + List signers = parser.getSignerFromOperation(NetworkEnum.PREPROD.getNetwork(), operation); + + assertThat(signers) + .hasSize(1) + .contains("stake_test1uql3k2h9kskfma8y53vth7wmptmlw8zxx67cx7c3pwj8sqs6zl0wr"); + } + + @Test + void shouldExtractSignerFromDRepVoteDelegationAbstain() { + Operation operation = Operation.builder() + .type(OperationType.VOTE_DREP_DELEGATION.getValue()) + .account(AccountIdentifier.builder() + .address("stake_test1uqehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gssrtvn") + .build()) + .metadata(OperationMetadata.builder() + .stakingCredential(PublicKey.builder() + .hexBytes("1b400d60aaf34eaf6dcbab9bba46001a23497886cf11066f7846933d30e5ad3f") + .curveType(CurveType.EDWARDS25519) + .build()) + .drep(DRepParams.builder() + .type(DRepTypeParams.ABSTAIN) + .build()) + .build()) + .build(); + + List signers = parser.getSignerFromOperation(NetworkEnum.PREPROD.getNetwork(), operation); + + assertThat(signers) + .hasSize(1) + .contains("stake_test1uqehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gssrtvn"); + } + + @Test + void shouldExtractSignerFromDRepVoteDelegationNoConfidence() { + Operation operation = Operation.builder() + .type(OperationType.VOTE_DREP_DELEGATION.getValue()) + .account(AccountIdentifier.builder() + .address("stake_test1uqehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gssrtvn") + .build()) + .metadata(OperationMetadata.builder() + .stakingCredential(PublicKey.builder() + .hexBytes("1b400d60aaf34eaf6dcbab9bba46001a23497886cf11066f7846933d30e5ad3f") + .curveType(CurveType.EDWARDS25519) + .build()) + .drep(DRepParams.builder() + .type(DRepTypeParams.NO_CONFIDENCE) + .build()) + .build()) + .build(); + + List signers = parser.getSignerFromOperation(NetworkEnum.PREPROD.getNetwork(), operation); + + assertThat(signers) + .hasSize(1) + .contains("stake_test1uqehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gssrtvn"); + } + + @Test + void shouldExtractSignerFromPoolGovernanceVote() { + Operation operation = Operation.builder() + .type(OperationType.POOL_GOVERNANCE_VOTE.getValue()) + .account(AccountIdentifier.builder() + .address("6c518b4861bb88b1395ceb116342cecbcfb8736282655f9a61c4c368") + .build()) + .metadata(OperationMetadata.builder() + .poolGovernanceVoteParams(PoolGovernanceVoteParams.builder() + .governanceActionHash("40c2a42fe324759a640dcfddbc69ef2e3b7fe5a998af8d6660359772bf44c9dc00") + .poolCredential(PublicKey.builder() + .hexBytes("60afbe982faaee34b02ad0e75cd50d5d7a734f5daaf7b67bc8c492eb5299af2b") + .curveType(CurveType.EDWARDS25519) + .build()) + .vote(GovVoteParams.YES) + .build()) + .build()) + .build(); + + List signers = parser.getSignerFromOperation(NetworkEnum.PREPROD.getNetwork(), operation); + + assertThat(signers) + .hasSize(1) + .contains("6c518b4861bb88b1395ceb116342cecbcfb8736282655f9a61c4c368"); + } + + @Test + void shouldExtractSignerFromPoolGovernanceVoteWithRationale() { + Operation operation = Operation.builder() + .type(OperationType.POOL_GOVERNANCE_VOTE.getValue()) + .account(AccountIdentifier.builder() + .address("6c518b4861bb88b1395ceb116342cecbcfb8736282655f9a61c4c368") + .build()) + .metadata(OperationMetadata.builder() + .poolGovernanceVoteParams(PoolGovernanceVoteParams.builder() + .governanceActionHash("40c2a42fe324759a640dcfddbc69ef2e3b7fe5a998af8d6660359772bf44c9dc00") + .poolCredential(PublicKey.builder() + .hexBytes("60afbe982faaee34b02ad0e75cd50d5d7a734f5daaf7b67bc8c492eb5299af2b") + .curveType(CurveType.EDWARDS25519) + .build()) + .vote(GovVoteParams.YES) + .voteRationale(GovVoteRationaleParams.builder() + .dataHash("c77f8427e2808cbd4c7093aa704fb0fcb48b2ab3bdd84fa7f4dec2eb7de344c9") + .url("ipfs://bafybeig7hluox6xefqdgmwcntvsguxcziw2oeogg2fbvygex2aj6qcfo64") + .build()) + .build()) + .build()) + .build(); + + List signers = parser.getSignerFromOperation(NetworkEnum.PREPROD.getNetwork(), operation); + + assertThat(signers) + .hasSize(1) + .contains("6c518b4861bb88b1395ceb116342cecbcfb8736282655f9a61c4c368"); + } + + @Test + void shouldExtractSignerFromPoolGovernanceVoteNo() { + Operation operation = Operation.builder() + .type(OperationType.POOL_GOVERNANCE_VOTE.getValue()) + .account(AccountIdentifier.builder() + .address("pool1test12345678abcdefghijklmnopqrstuvwxyz") + .build()) + .metadata(OperationMetadata.builder() + .poolGovernanceVoteParams(PoolGovernanceVoteParams.builder() + .governanceActionHash("40c2a42fe324759a640dcfddbc69ef2e3b7fe5a998af8d6660359772bf44c9dc00") + .poolCredential(PublicKey.builder() + .hexBytes("60afbe982faaee34b02ad0e75cd50d5d7a734f5daaf7b67bc8c492eb5299af2b") + .curveType(CurveType.EDWARDS25519) + .build()) + .vote(GovVoteParams.NO) + .build()) + .build()) + .build(); + + List signers = parser.getSignerFromOperation(NetworkEnum.PREPROD.getNetwork(), operation); + + assertThat(signers) + .hasSize(1) + .contains("pool1test12345678abcdefghijklmnopqrstuvwxyz"); + } + + @Test + void shouldExtractSignerFromPoolGovernanceVoteAbstain() { + Operation operation = Operation.builder() + .type(OperationType.POOL_GOVERNANCE_VOTE.getValue()) + .account(AccountIdentifier.builder() + .address("pool1test12345678abcdefghijklmnopqrstuvwxyz") + .build()) + .metadata(OperationMetadata.builder() + .poolGovernanceVoteParams(PoolGovernanceVoteParams.builder() + .governanceActionHash("40c2a42fe324759a640dcfddbc69ef2e3b7fe5a998af8d6660359772bf44c9dc00") + .poolCredential(PublicKey.builder() + .hexBytes("60afbe982faaee34b02ad0e75cd50d5d7a734f5daaf7b67bc8c492eb5299af2b") + .curveType(CurveType.EDWARDS25519) + .build()) + .vote(GovVoteParams.ABSTAIN) + .build()) + .build()) + .build(); + + List signers = parser.getSignerFromOperation(NetworkEnum.PREPROD.getNetwork(), operation); + + assertThat(signers) + .hasSize(1) + .contains("pool1test12345678abcdefghijklmnopqrstuvwxyz"); + } + + @Test + void shouldHandleNullAccountInGovernanceOperations() { + Operation operation = Operation.builder() + .type(OperationType.VOTE_DREP_DELEGATION.getValue()) + .account(null) + .metadata(OperationMetadata.builder() + .stakingCredential(PublicKey.builder() + .hexBytes("1b400d60aaf34eaf6dcbab9bba46001a23497886cf11066f7846933d30e5ad3f") + .curveType(CurveType.EDWARDS25519) + .build()) + .drep(DRepParams.builder() + .type(DRepTypeParams.ABSTAIN) + .build()) + .build()) + .build(); + + List signers = parser.getSignerFromOperation(NetworkEnum.PREPROD.getNetwork(), operation); + + assertThat(signers) + .hasSize(1) + .contains("stake_test1uza5pudxg77g3sdaddecmw8tvc6hmynywn49lltt4fmvn7c6nuuef"); + } + } + + /** + * Tests for error handling and edge cases. + */ + @Nested + class ErrorHandling { + + @SuppressWarnings("java:S5778") + @Test + void shouldThrowExceptionWhenStakingKeyIsMissing() { + Operation operation = Operation.builder().metadata(null).type("invalidType").build(); + + ApiException actualException = assertThrows(ApiException.class, + () -> parser.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), operation)); + + assertThat(actualException.getError().getMessage()) + .isEqualTo(RosettaErrorType.STAKING_KEY_MISSING.getMessage()); + assertThat(actualException.getError().getCode()) + .isEqualTo(RosettaErrorType.STAKING_KEY_MISSING.getCode()); + } + + /** + * Verifies that all pool operation types are handled correctly. + *

+ * Note: The default case in getPoolSigners has defensive error message code that's currently + * unreachable with production code, since POOL_OPERATIONS list and switch statement cases + * are kept in sync. The error message "pool operation not supported, operation:{type}" + * provides future-proofing if they ever become out of sync. + *

+ */ + @Test + void shouldHandleAllPoolOperationTypes() { + // Test pool registration + Operation poolRegistration = Operation.builder() + .type(OperationType.POOL_REGISTRATION.getValue()) + .account(AccountIdentifier.builder().address("addr1").build()) + .metadata(OperationMetadata.builder() + .poolRegistrationParams(PoolRegistrationParams.builder() + .rewardAddress("rewardAddress") + .poolOwners(List.of("owner1")) + .build()) + .build()) + .build(); + assertThat(parser.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), poolRegistration)) + .isNotEmpty(); + + // Test pool retirement + Operation poolRetirement = Operation.builder() + .type(OperationType.POOL_RETIREMENT.getValue()) + .account(AccountIdentifier.builder().address("addr1").build()) + .build(); + assertThat(parser.getSignerFromOperation(NetworkEnum.MAINNET.getNetwork(), poolRetirement)) + .hasSize(1) + .contains("addr1"); + } + } + } + + /** + * Tests for {@link OperationServiceImpl#getOperationsFromTransactionData(TransactionData, com.bloxbean.cardano.client.common.model.Network)}. + *

+ * Tests are organized by operation type: + *

    + *
  • {@link PoolOperations} - Pool registration and retirement operations
  • + *
  • {@link StakingOperations} - Stake key registration, deregistration, and delegation
  • + *
  • {@link WithdrawalOperations} - Staking reward withdrawal operations
  • + *
  • {@link GovernanceOperations} - DRep vote delegation and pool governance votes
  • + *
  • {@link InputOutputOperations} - Input and output processing edge cases
  • + *
+ *

+ */ + @Nested + class GetOperationsFromTransactionDataTests { + + /** + * Tests for pool operations extraction from transaction data. + *

+ * Note: Pool operations are complex and tested more thoroughly in integration tests. + * These tests verify basic extraction capability. + *

+ */ + @Nested + class PoolOperations { + + @Test + void shouldHandleTransactionWithPoolOperations() + throws CborException, CborDeserializationException, CborSerializationException { + TransactionData transactionData = getPoolTransactionData1(); + transactionData.transactionBody().setInputs(List.of(new TransactionInput())); + + List operations = parser + .getOperationsFromTransactionData(transactionData, NetworkEnum.MAINNET.getNetwork()); + + // Verify we can process transactions with pool operations without errors + assertThat(operations).isNotEmpty(); + } + } + + /** + * Tests for staking operations extraction from transaction data. + */ + @Nested + class StakingOperations { + + @Test + void shouldExtractStakeKeyRegistrationFromTransactionData() + throws CborException, CborDeserializationException, CborSerializationException { + Operation stakeRegOp = Operation.builder() + .type(OperationType.STAKE_KEY_REGISTRATION.getValue()) + .operationIdentifier(OperationIdentifier.builder().index(1L).build()) + .account(AccountIdentifier.builder() + .address("stake_test1uza5pudxg77g3sdaddecmw8tvc6hmynywn49lltt4fmvn7c6nuuef") + .build()) + .metadata(OperationMetadata.builder() + .stakingCredential(PublicKey.builder() + .hexBytes("1b400d60aaf34eaf6dcbab9bba46001a23497886cf11066f7846933d30e5ad3f") + .curveType(CurveType.EDWARDS25519) + .build()) + .build()) + .build(); + + TransactionExtraData extraData = new TransactionExtraData(List.of(stakeRegOp)); + + com.bloxbean.cardano.client.transaction.spec.cert.StakeRegistration stakeRegCert = + new com.bloxbean.cardano.client.transaction.spec.cert.StakeRegistration( + com.bloxbean.cardano.client.transaction.spec.cert.StakeCredential.fromKey( + com.bloxbean.cardano.client.util.HexUtil.decodeHexString("1b400d60aaf34eaf6dcbab9bba46001a23497886cf11066f7846933d30e5ad3f") + ) + ); + + TransactionBody transactionBody = TransactionBody.builder() + .inputs(List.of(new TransactionInput())) + .certs(List.of(stakeRegCert)) + .build(); + TransactionData transactionData = new TransactionData(transactionBody, extraData); + + List operations = parser + .getOperationsFromTransactionData(transactionData, NetworkEnum.PREPROD.getNetwork()); + + assertThat(operations).hasSizeGreaterThanOrEqualTo(2); + long stakeRegCount = operations.stream() + .filter(op -> op.getType().equals(OperationType.STAKE_KEY_REGISTRATION.getValue())) + .count(); + assertThat(stakeRegCount).isEqualTo(1); + } + + @Test + void shouldExtractStakeKeyDeregistrationFromTransactionData() + throws CborException, CborDeserializationException, CborSerializationException { + Operation stakeDeregOp = Operation.builder() + .type(OperationType.STAKE_KEY_DEREGISTRATION.getValue()) + .operationIdentifier(OperationIdentifier.builder().index(1L).build()) + .account(AccountIdentifier.builder() + .address("stake_test1uza5pudxg77g3sdaddecmw8tvc6hmynywn49lltt4fmvn7c6nuuef") + .build()) + .metadata(OperationMetadata.builder() + .stakingCredential(PublicKey.builder() + .hexBytes("1b400d60aaf34eaf6dcbab9bba46001a23497886cf11066f7846933d30e5ad3f") + .curveType(CurveType.EDWARDS25519) + .build()) + .build()) + .build(); + + TransactionExtraData extraData = new TransactionExtraData(List.of(stakeDeregOp)); + + com.bloxbean.cardano.client.transaction.spec.cert.StakeDeregistration stakeDeregCert = + new com.bloxbean.cardano.client.transaction.spec.cert.StakeDeregistration( + com.bloxbean.cardano.client.transaction.spec.cert.StakeCredential.fromKey( + com.bloxbean.cardano.client.util.HexUtil.decodeHexString("1b400d60aaf34eaf6dcbab9bba46001a23497886cf11066f7846933d30e5ad3f") + ) + ); + + TransactionBody transactionBody = TransactionBody.builder() + .inputs(List.of(new TransactionInput())) + .certs(List.of(stakeDeregCert)) + .build(); + TransactionData transactionData = new TransactionData(transactionBody, extraData); + + List operations = parser + .getOperationsFromTransactionData(transactionData, NetworkEnum.PREPROD.getNetwork()); + + assertThat(operations).hasSizeGreaterThanOrEqualTo(2); + long stakeDeregCount = operations.stream() + .filter(op -> op.getType().equals(OperationType.STAKE_KEY_DEREGISTRATION.getValue())) + .count(); + assertThat(stakeDeregCount).isEqualTo(1); + } + + @Test + void shouldExtractStakeDelegationFromTransactionData() + throws CborException, CborDeserializationException, CborSerializationException { + String poolKeyHash = "1b268f4cba3faa7e36d8a0cc4adca2096fb856119412ee7330f692b5"; + + Operation stakeDelegOp = Operation.builder() + .type(OperationType.STAKE_DELEGATION.getValue()) + .operationIdentifier(OperationIdentifier.builder().index(1L).build()) + .account(AccountIdentifier.builder() + .address("stake_test1uza5pudxg77g3sdaddecmw8tvc6hmynywn49lltt4fmvn7c6nuuef") + .build()) + .metadata(OperationMetadata.builder() + .stakingCredential(PublicKey.builder() + .hexBytes("1b400d60aaf34eaf6dcbab9bba46001a23497886cf11066f7846933d30e5ad3f") + .curveType(CurveType.EDWARDS25519) + .build()) + .poolKeyHash(poolKeyHash) + .build()) + .build(); + + TransactionExtraData extraData = new TransactionExtraData(List.of(stakeDelegOp)); + + com.bloxbean.cardano.client.transaction.spec.cert.StakeDelegation stakeDelegCert = + new com.bloxbean.cardano.client.transaction.spec.cert.StakeDelegation( + com.bloxbean.cardano.client.transaction.spec.cert.StakeCredential.fromKey( + com.bloxbean.cardano.client.util.HexUtil.decodeHexString("1b400d60aaf34eaf6dcbab9bba46001a23497886cf11066f7846933d30e5ad3f") + ), + new com.bloxbean.cardano.client.transaction.spec.cert.StakePoolId( + com.bloxbean.cardano.client.util.HexUtil.decodeHexString(poolKeyHash) + ) + ); + + TransactionBody transactionBody = TransactionBody.builder() + .inputs(List.of(new TransactionInput())) + .certs(List.of(stakeDelegCert)) + .build(); + TransactionData transactionData = new TransactionData(transactionBody, extraData); + + List operations = parser + .getOperationsFromTransactionData(transactionData, NetworkEnum.PREPROD.getNetwork()); + + assertThat(operations).hasSizeGreaterThanOrEqualTo(2); + long stakeDelegCount = operations.stream() + .filter(op -> op.getType().equals(OperationType.STAKE_DELEGATION.getValue())) + .count(); + assertThat(stakeDelegCount).isEqualTo(1); + } + } + + /** + * Tests for withdrawal operations extraction from transaction data. + */ + @Nested + class WithdrawalOperations { + + @Test + void shouldExtractWithdrawalOperationsFromTransactionData() + throws CborException, CborDeserializationException, CborSerializationException { + TransactionData transactionData = getPoolTransactionData1(); + transactionData.transactionBody().setInputs(List.of(new TransactionInput())); + transactionData.transactionBody().setWithdrawals(List.of(new Withdrawal())); + Operation withdrawalOperation = transactionData.transactionExtraData().operations().getFirst(); + withdrawalOperation.setType(OperationType.WITHDRAWAL.getValue()); + withdrawalOperation.setMetadata(OperationMetadata.builder() + .stakingCredential( + PublicKey.builder() + .curveType(CurveType.EDWARDS25519) + .hexBytes("1B400D60AAF34EAF6DCBAB9BBA46001A23497886CF11066F7846933D30E5AD3F") + .build()) + .build()); + withdrawalOperation.setOperationIdentifier(OperationIdentifier.builder().index(22L).build()); + withdrawalOperation.setAmount(Amount.builder().value("value").build()); + + List operations = parser + .getOperationsFromTransactionData(transactionData, NetworkEnum.MAINNET.getNetwork()); + + assertThat(operations).hasSize(2); + assertThat(operations.get(1).getType()).isEqualTo(OperationType.WITHDRAWAL.getValue()); + assertThat(operations.get(1).getOperationIdentifier().getIndex()).isEqualTo(22L); + assertThat(operations.get(1).getAmount().getValue()).isEqualTo("value"); + assertThat(operations.get(1).getMetadata().getStakingCredential().getHexBytes()) + .isEqualTo("1B400D60AAF34EAF6DCBAB9BBA46001A23497886CF11066F7846933D30E5AD3F"); + } + } + + /** + * Tests for governance operations (Conway era) extraction from transaction data. + *

+ * Covers both certificate-based (DRep vote delegation) and non-certificate + * (pool governance votes) governance operations. + *

+ */ + @Nested + class GovernanceOperations { + + /** + * Tests pool governance vote extraction (non-certificate governance operation). + *

+ * Pool governance votes are voting procedures (NOT certificates) and should be + * extracted by fillGovOperations() even when no certificates exist in the transaction. + *

+ */ + @Test + void shouldExtractPoolGovernanceVoteWithoutCertificates() + throws CborException, CborDeserializationException, CborSerializationException { + Operation poolVoteOperation = Operation.builder() + .type(OperationType.POOL_GOVERNANCE_VOTE.getValue()) + .operationIdentifier(OperationIdentifier.builder().index(1L).build()) + .account(AccountIdentifier.builder() + .address("6c518b4861bb88b1395ceb116342cecbcfb8736282655f9a61c4c368") + .build()) + .metadata(OperationMetadata.builder() + .poolGovernanceVoteParams(PoolGovernanceVoteParams.builder() + .governanceActionHash("40c2a42fe324759a640dcfddbc69ef2e3b7fe5a998af8d6660359772bf44c9dc00") + .poolCredential(PublicKey.builder() + .hexBytes("60afbe982faaee34b02ad0e75cd50d5d7a734f5daaf7b67bc8c492eb5299af2b") + .curveType(CurveType.EDWARDS25519) + .build()) + .vote(GovVoteParams.YES) + .build()) + .build()) + .build(); + + TransactionExtraData extraData = new TransactionExtraData(List.of(poolVoteOperation)); + TransactionBody transactionBody = TransactionBody.builder() + .inputs(List.of(new TransactionInput())) + .build(); // No certificates - pool votes are voting procedures, not certificates + TransactionData transactionData = new TransactionData(transactionBody, extraData); + + List operations = parser + .getOperationsFromTransactionData(transactionData, NetworkEnum.PREPROD.getNetwork()); + + assertThat(operations).hasSize(2); // 1 input + 1 governance operation + assertThat(operations.get(1).getType()).isEqualTo(OperationType.POOL_GOVERNANCE_VOTE.getValue()); + assertThat(operations.get(1).getAccount().getAddress()) + .isEqualTo("6c518b4861bb88b1395ceb116342cecbcfb8736282655f9a61c4c368"); + assertThat(operations.get(1).getMetadata().getPoolGovernanceVoteParams().getGovernanceActionHash()) + .isEqualTo("40c2a42fe324759a640dcfddbc69ef2e3b7fe5a998af8d6660359772bf44c9dc00"); + assertThat(operations.get(1).getMetadata().getPoolGovernanceVoteParams().getVote()) + .isEqualTo(GovVoteParams.YES); + } + + @Test + void shouldExtractPoolGovernanceVoteFromTransactionData() + throws CborException, CborDeserializationException, CborSerializationException { + Operation poolVoteOperation = Operation.builder() + .type(OperationType.POOL_GOVERNANCE_VOTE.getValue()) + .operationIdentifier(OperationIdentifier.builder().index(1L).build()) + .account(AccountIdentifier.builder() + .address("6c518b4861bb88b1395ceb116342cecbcfb8736282655f9a61c4c368") + .build()) + .metadata(OperationMetadata.builder() + .poolGovernanceVoteParams(PoolGovernanceVoteParams.builder() + .governanceActionHash("40c2a42fe324759a640dcfddbc69ef2e3b7fe5a998af8d6660359772bf44c9dc00") + .poolCredential(PublicKey.builder() + .hexBytes("60afbe982faaee34b02ad0e75cd50d5d7a734f5daaf7b67bc8c492eb5299af2b") + .curveType(CurveType.EDWARDS25519) + .build()) + .vote(GovVoteParams.YES) + .build()) + .build()) + .build(); + + TransactionExtraData extraData = new TransactionExtraData(List.of(poolVoteOperation)); + TransactionBody transactionBody = TransactionBody.builder() + .inputs(List.of(new TransactionInput())) + .build(); + TransactionData transactionData = new TransactionData(transactionBody, extraData); + + List operations = parser + .getOperationsFromTransactionData(transactionData, NetworkEnum.PREPROD.getNetwork()); + + assertThat(operations).hasSize(2); // 1 input + 1 governance operation + assertThat(operations.get(1).getType()).isEqualTo(OperationType.POOL_GOVERNANCE_VOTE.getValue()); + assertThat(operations.get(1).getAccount().getAddress()) + .isEqualTo("6c518b4861bb88b1395ceb116342cecbcfb8736282655f9a61c4c368"); + assertThat(operations.get(1).getMetadata().getPoolGovernanceVoteParams().getGovernanceActionHash()) + .isEqualTo("40c2a42fe324759a640dcfddbc69ef2e3b7fe5a998af8d6660359772bf44c9dc00"); + assertThat(operations.get(1).getMetadata().getPoolGovernanceVoteParams().getVote()) + .isEqualTo(GovVoteParams.YES); + } + + /** + * Tests that only non-certificate governance operations are extracted when no certificates exist. + *

+ * This test demonstrates the correct behavior after Bug #1 fix: + * - Pool governance votes (voting procedures) are extracted + * - DRep vote delegations (certificates) are NOT extracted when no certificates in transaction body + *

+ */ + @Test + void shouldExtractOnlyNonCertificateGovernanceOperationsWithoutCertificates() + throws CborException, CborDeserializationException, CborSerializationException { + Operation drepOperation = Operation.builder() + .type(OperationType.VOTE_DREP_DELEGATION.getValue()) + .operationIdentifier(OperationIdentifier.builder().index(1L).build()) + .account(AccountIdentifier.builder() + .address("stake_test1uql3k2h9kskfma8y53vth7wmptmlw8zxx67cx7c3pwj8sqs6zl0wr") + .build()) + .metadata(OperationMetadata.builder() + .stakingCredential(PublicKey.builder() + .hexBytes("fdbee86a702c49d23f45ab80e697b93ec48b744d5f413ef59c338c3f7b2d2de8") + .curveType(CurveType.EDWARDS25519) + .build()) + .drep(DRepParams.builder() + .type(DRepTypeParams.ABSTAIN) + .build()) + .build()) + .build(); + + Operation poolVoteOperation = Operation.builder() + .type(OperationType.POOL_GOVERNANCE_VOTE.getValue()) + .operationIdentifier(OperationIdentifier.builder().index(2L).build()) + .account(AccountIdentifier.builder() + .address("6c518b4861bb88b1395ceb116342cecbcfb8736282655f9a61c4c368") + .build()) + .metadata(OperationMetadata.builder() + .poolGovernanceVoteParams(PoolGovernanceVoteParams.builder() + .governanceActionHash("40c2a42fe324759a640dcfddbc69ef2e3b7fe5a998af8d6660359772bf44c9dc00") + .poolCredential(PublicKey.builder() + .hexBytes("60afbe982faaee34b02ad0e75cd50d5d7a734f5daaf7b67bc8c492eb5299af2b") + .curveType(CurveType.EDWARDS25519) + .build()) + .vote(GovVoteParams.NO) + .build()) + .build()) + .build(); + + TransactionExtraData extraData = new TransactionExtraData(List.of(drepOperation, poolVoteOperation)); + TransactionBody transactionBody = TransactionBody.builder() + .inputs(List.of(new TransactionInput())) + .build(); // No certificates! + TransactionData transactionData = new TransactionData(transactionBody, extraData); + + List operations = parser + .getOperationsFromTransactionData(transactionData, NetworkEnum.PREPROD.getNetwork()); + + // Only pool governance vote should be extracted (DRep is a certificate and requires certs in transaction body) + assertThat(operations).hasSize(2); // 1 input + 1 pool governance vote (NOT DRep) + assertThat(operations.get(1).getType()).isEqualTo(OperationType.POOL_GOVERNANCE_VOTE.getValue()); + + // Verify DRep operation was NOT extracted + long drepCount = operations.stream() + .filter(op -> op.getType().equals(OperationType.VOTE_DREP_DELEGATION.getValue())) + .count(); + assertThat(drepCount).isEqualTo(0); + } + + /** + * Tests that DRep vote delegation IS properly extracted when certificates exist. + *

+ * DRep vote delegation is a Cardano certificate (VoteDelegCert) and should be processed + * when the transaction body contains actual VoteDelegCert certificates. + *

+ */ + @Test + void shouldExtractDRepOperationWhenCertificateExistsInTransactionBody() + throws CborException, CborDeserializationException, CborSerializationException { + + // Create DRep operation in extra data + Operation drepOperation = Operation.builder() + .type(OperationType.VOTE_DREP_DELEGATION.getValue()) + .operationIdentifier(OperationIdentifier.builder().index(1L).build()) + .account(AccountIdentifier.builder() + .address("stake_test1uza5pudxg77g3sdaddecmw8tvc6hmynywn49lltt4fmvn7c6nuuef") + .build()) + .metadata(OperationMetadata.builder() + .stakingCredential(PublicKey.builder() + .hexBytes("1b400d60aaf34eaf6dcbab9bba46001a23497886cf11066f7846933d30e5ad3f") + .curveType(CurveType.EDWARDS25519) + .build()) + .drep(DRepParams.builder() + .type(DRepTypeParams.ABSTAIN) + .build()) + .build()) + .build(); + + // Create actual VoteDelegCert certificate to add to transaction body + com.bloxbean.cardano.client.transaction.spec.cert.VoteDelegCert voteDelegCert = + new com.bloxbean.cardano.client.transaction.spec.cert.VoteDelegCert( + com.bloxbean.cardano.client.transaction.spec.cert.StakeCredential.fromKey( + com.bloxbean.cardano.client.util.HexUtil.decodeHexString("1b400d60aaf34eaf6dcbab9bba46001a23497886cf11066f7846933d30e5ad3f") + ), + com.bloxbean.cardano.client.transaction.spec.governance.DRep.abstain() + ); + + TransactionExtraData extraData = new TransactionExtraData(List.of(drepOperation)); + TransactionBody transactionBody = TransactionBody.builder() + .inputs(List.of(new TransactionInput())) + .certs(List.of(voteDelegCert)) // Add actual certificate! + .build(); + TransactionData transactionData = new TransactionData(transactionBody, extraData); + + List operations = parser + .getOperationsFromTransactionData(transactionData, NetworkEnum.PREPROD.getNetwork()); + + // DRep operation SHOULD be extracted because certificate exists in transaction body + long drepOperationCount = operations.stream() + .filter(op -> op.getType().equals(OperationType.VOTE_DREP_DELEGATION.getValue())) + .count(); + + assertThat(drepOperationCount) + .as("DRep operation should be extracted exactly once when certificate exists") + .isEqualTo(1); + + assertThat(operations).hasSize(2); // 1 input + 1 DRep delegation + + // Verify the extracted operation has correct data + Operation extractedDRep = operations.stream() + .filter(op -> op.getType().equals(OperationType.VOTE_DREP_DELEGATION.getValue())) + .findFirst() + .orElseThrow(); + + assertThat(extractedDRep.getAccount().getAddress()) + .isEqualTo("stake_test1uza5pudxg77g3sdaddecmw8tvc6hmynywn49lltt4fmvn7c6nuuef"); + assertThat(extractedDRep.getMetadata().getDrep().getType()) + .isEqualTo(DRepTypeParams.ABSTAIN); + assertThat(extractedDRep.getMetadata().getStakingCredential().getHexBytes()) + .isEqualToIgnoringCase("1b400d60aaf34eaf6dcbab9bba46001a23497886cf11066f7846933d30e5ad3f"); + } + + @Test + void shouldHandleTransactionWithNoGovernanceOperations() + throws CborException, CborDeserializationException, CborSerializationException { + TransactionData transactionData = getPoolTransactionData1(); + transactionData.transactionBody().setInputs(List.of(new TransactionInput())); + + List operations = parser + .getOperationsFromTransactionData(transactionData, NetworkEnum.MAINNET.getNetwork()); + + assertThat(operations).hasSize(1); // Only input operation + assertThat(operations).noneMatch(op -> + op.getType().equals(OperationType.VOTE_DREP_DELEGATION.getValue()) || + op.getType().equals(OperationType.POOL_GOVERNANCE_VOTE.getValue()) + ); + } + } + + /** + * Tests for input and output processing edge cases. + */ + @Nested + class InputOutputOperations { + + /** + * BUG TEST: This test exposes Bug #2 - IndexOutOfBoundsException when + * extraDataInputOperations.size() < inputs.size() + */ + @Test + void shouldHandleMismatchBetweenExtraDataInputsAndTransactionInputs() + throws CborException, CborDeserializationException, CborSerializationException { + // Create 2 input operations in extraData + Operation input1 = Operation.builder() + .type(OperationType.INPUT.getValue()) + .operationIdentifier(OperationIdentifier.builder().index(0L).build()) + .account(AccountIdentifier.builder().address("addr1").build()) + .amount(Amount.builder().value("-1000000").build()) + .build(); + + Operation input2 = Operation.builder() + .type(OperationType.INPUT.getValue()) + .operationIdentifier(OperationIdentifier.builder().index(1L).build()) + .account(AccountIdentifier.builder().address("addr2").build()) + .amount(Amount.builder().value("-2000000").build()) + .build(); + + TransactionExtraData extraData = new TransactionExtraData(List.of(input1, input2)); + + // Create 5 inputs in transaction body (more than extraData) + TransactionBody transactionBody = TransactionBody.builder() + .inputs(List.of( + new TransactionInput(), + new TransactionInput(), + new TransactionInput(), + new TransactionInput(), + new TransactionInput() + )) + .build(); + + TransactionData transactionData = new TransactionData(transactionBody, extraData); + + // BUG: This will throw IndexOutOfBoundsException + // When i=2, it tries to access extraDataInputOperations.get(2) but size is only 2 + List operations = parser + .getOperationsFromTransactionData(transactionData, NetworkEnum.MAINNET.getNetwork()); + + // Should have 5 input operations (fallback to parsing from transactionBody when extraData insufficient) + assertThat(operations).hasSizeGreaterThanOrEqualTo(5); + } + + @Test + void shouldHandleEmptyInputsList() + throws CborException, CborDeserializationException, CborSerializationException { + TransactionExtraData extraData = new TransactionExtraData(List.of()); + TransactionBody transactionBody = TransactionBody.builder() + .inputs(List.of()) + .outputs(List.of()) + .build(); + TransactionData transactionData = new TransactionData(transactionBody, extraData); + + List operations = parser + .getOperationsFromTransactionData(transactionData, NetworkEnum.MAINNET.getNetwork()); + + assertThat(operations).isEmpty(); + } + + @Test + void shouldHandleNullOutputsList() + throws CborException, CborDeserializationException, CborSerializationException { + TransactionExtraData extraData = new TransactionExtraData(List.of()); + TransactionBody transactionBody = TransactionBody.builder() + .inputs(List.of()) + .outputs(null) + .build(); + TransactionData transactionData = new TransactionData(transactionBody, extraData); + + List operations = parser + .getOperationsFromTransactionData(transactionData, NetworkEnum.MAINNET.getNetwork()); + + assertThat(operations).isEmpty(); + } + } + } + + // ========== Helper Methods ========== + + private static Operation createTestPoolRegistrationOperation(String accountAddress, String rewardAddress, + List poolOwners) { + return Operation.builder() + .type(OperationType.POOL_REGISTRATION.getValue()) + .account(AccountIdentifier.builder() + .address(accountAddress) + .build()) + .metadata(OperationMetadata.builder() + .poolRegistrationParams(PoolRegistrationParams.builder() + .rewardAddress(rewardAddress) + .poolOwners(poolOwners) + .build()) + .build()) + .build(); + } + + private static TransactionData getPoolTransactionData1() { + Operation operation1 = createTestPoolRegistrationOperation("addr1", "rewardAddress", List.of("poolOwner1", "poolOwner2")); + Operation operation2 = createTestPoolRegistrationOperation("addr2", "rewardAddress", List.of("poolOwner3", "poolOwner4")); + TransactionExtraData transactionExtraData = new TransactionExtraData(List.of(operation1, operation2)); + + return new TransactionData( + TransactionBody.builder().build(), + transactionExtraData + ); + } + +} diff --git a/pom.xml b/pom.xml index 1efe775a79..df54d5a2a7 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ - 2.0.0-beta + 2.1.0-beta 24 UTF-8 3.5.0 diff --git a/tests/data-endpoints/conftest.py b/tests/data-endpoints/conftest.py index a47bfad336..cd64a7be54 100644 --- a/tests/data-endpoints/conftest.py +++ b/tests/data-endpoints/conftest.py @@ -160,3 +160,16 @@ def get_error_message(error_response): details_message = error_response.get("details", {}).get("message", "") return (message + " " + details_message).strip() + +def assert_operations_ordered(operations): + """Operations must be sorted by operation_identifier.index.""" + indices = [op["operation_identifier"]["index"] for op in operations] + assert indices == sorted(indices), f"Operations not ordered: {indices}" + + +def assert_operations_sequential(operations): + """Operation indices must be [0, 1, 2, ..., n-1].""" + indices = [op["operation_identifier"]["index"] for op in operations] + expected = list(range(len(operations))) + assert indices == expected, f"Expected {expected}, got {indices}" + diff --git a/tests/data-endpoints/test_block_endpoints.py b/tests/data-endpoints/test_block_endpoints.py index a972729527..2bcc7fcef6 100644 --- a/tests/data-endpoints/test_block_endpoints.py +++ b/tests/data-endpoints/test_block_endpoints.py @@ -6,7 +6,7 @@ import pytest import allure -from conftest import get_error_message +from conftest import get_error_message, assert_operations_ordered, assert_operations_sequential pytestmark = pytest.mark.pr @@ -314,3 +314,75 @@ def test_invalid_transaction_hash_returns_error(self, client, network): transaction_identifier={"hash": "invalid_hash"}, ) assert response.status_code == 500 + + +@allure.feature("Block") +@allure.story("Operation Invariants") +class TestBlockOperationInvariants: + """Operations must be ordered and sequential in all block transactions.""" + + def test_block_operations_ordered_by_index(self, client, network): + """Operations in /block response must be sorted by index.""" + # Find block with transactions + search = client.search_transactions(network=network) + block_id = search.json()["transactions"][0]["block_identifier"] + + response = client.block(network=network, block_identifier={"index": block_id["index"]}) + assert response.status_code == 200 + + for tx in response.json()["block"]["transactions"]: + assert_operations_ordered(tx["operations"]) + + def test_block_operations_sequential_indices(self, client, network): + """Operations in /block must have sequential indices [0, 1, 2, ...].""" + # Find block with transactions + search = client.search_transactions(network=network) + block_id = search.json()["transactions"][0]["block_identifier"] + + response = client.block(network=network, block_identifier={"index": block_id["index"]}) + assert response.status_code == 200 + + for tx in response.json()["block"]["transactions"]: + assert_operations_sequential(tx["operations"]) + + +@allure.feature("Block Transaction") +@allure.story("Operation Invariants") +class TestBlockTransactionOperationInvariants: + """Operations must be ordered and sequential in /block/transaction.""" + + def test_block_transaction_operations_ordered_by_index(self, client, network): + """Operations in /block/transaction must be sorted by index.""" + # Find transaction + search = client.search_transactions(network=network) + block_tx = search.json()["transactions"][0] + + response = client.block_transaction( + network=network, + block_identifier={ + "index": block_tx["block_identifier"]["index"], + "hash": block_tx["block_identifier"]["hash"] + }, + transaction_identifier={"hash": block_tx["transaction"]["transaction_identifier"]["hash"]} + ) + assert response.status_code == 200 + + assert_operations_ordered(response.json()["transaction"]["operations"]) + + def test_block_transaction_operations_sequential_indices(self, client, network): + """Operations in /block/transaction must have sequential indices.""" + # Find transaction + search = client.search_transactions(network=network) + block_tx = search.json()["transactions"][0] + + response = client.block_transaction( + network=network, + block_identifier={ + "index": block_tx["block_identifier"]["index"], + "hash": block_tx["block_identifier"]["hash"] + }, + transaction_identifier={"hash": block_tx["transaction"]["transaction_identifier"]["hash"]} + ) + assert response.status_code == 200 + + assert_operations_sequential(response.json()["transaction"]["operations"]) diff --git a/tests/data-endpoints/test_search_transactions.py b/tests/data-endpoints/test_search_transactions.py index 616a45ee5c..9cc3a8016b 100644 --- a/tests/data-endpoints/test_search_transactions.py +++ b/tests/data-endpoints/test_search_transactions.py @@ -6,7 +6,7 @@ import os import pytest import allure -from conftest import get_error_message +from conftest import get_error_message, assert_operations_ordered, assert_operations_sequential # Network configuration from environment - works on ANY network @@ -793,3 +793,29 @@ def test_native_asset_filtering_with_policy_id(self, client, network, network_da f"Transaction must contain asset with policyId {asset['policy_id']} " f"and symbol {asset['symbol_hex']}. Found assets: {assets_in_tx}" ) + + +class TestOperationInvariants: + """Operations must be ordered and sequential in all transactions.""" + + @allure.feature("Search Transactions") + @allure.story("Operation Invariants") + def test_operations_ordered_by_index(self, client, network): + """Operations array must be sorted by operation_identifier.index.""" + response = client.search_transactions(network=network, limit=10) + assert response.status_code == 200 + + for block_tx in response.json()["transactions"]: + operations = block_tx["transaction"]["operations"] + assert_operations_ordered(operations) + + @allure.feature("Search Transactions") + @allure.story("Operation Invariants") + def test_operations_sequential_indices(self, client, network): + """Operation indices must be [0, 1, 2, ..., n-1] with no gaps.""" + response = client.search_transactions(network=network, limit=10) + assert response.status_code == 200 + + for block_tx in response.json()["transactions"]: + operations = block_tx["transaction"]["operations"] + assert_operations_sequential(operations) diff --git a/tests/integration/golden_examples/rosetta_java/construction/parse/complex_transactions/combined_stake_and_drep_delegation.json b/tests/integration/golden_examples/rosetta_java/construction/parse/complex_transactions/combined_stake_and_drep_delegation.json index 3d04feba8a..5536b22564 100644 --- a/tests/integration/golden_examples/rosetta_java/construction/parse/complex_transactions/combined_stake_and_drep_delegation.json +++ b/tests/integration/golden_examples/rosetta_java/construction/parse/complex_transactions/combined_stake_and_drep_delegation.json @@ -91,25 +91,6 @@ "type": "key_hash" } } - }, - { - "operation_identifier": { - "index": 3 - }, - "type": "dRepVoteDelegation", - "account": { - "address": "stake_test1uql3k2h9kskfma8y53vth7wmptmlw8zxx67cx7c3pwj8sqs6zl0wr" - }, - "metadata": { - "staking_credential": { - "hex_bytes": "fdbee86a702c49d23f45ab80e697b93ec48b744d5f413ef59c338c3f7b2d2de8", - "curve_type": "edwards25519" - }, - "drep": { - "id": "f72050aa252ccc6bf75747184910c0b9386298167656f935d6b6c26a", - "type": "key_hash" - } - } } ], "account_identifier_signers": [] diff --git a/tests/integration/golden_examples/rosetta_java/construction/parse/complex_transactions/drep_delegation_with_native_assets.json b/tests/integration/golden_examples/rosetta_java/construction/parse/complex_transactions/drep_delegation_with_native_assets.json index 259f07e884..cf75bea9a1 100644 --- a/tests/integration/golden_examples/rosetta_java/construction/parse/complex_transactions/drep_delegation_with_native_assets.json +++ b/tests/integration/golden_examples/rosetta_java/construction/parse/complex_transactions/drep_delegation_with_native_assets.json @@ -144,25 +144,6 @@ "type": "script_hash" } } - }, - { - "operation_identifier": { - "index": 3 - }, - "type": "dRepVoteDelegation", - "account": { - "address": "stake_test1uql3k2h9kskfma8y53vth7wmptmlw8zxx67cx7c3pwj8sqs6zl0wr" - }, - "metadata": { - "staking_credential": { - "hex_bytes": "fdbee86a702c49d23f45ab80e697b93ec48b744d5f413ef59c338c3f7b2d2de8", - "curve_type": "edwards25519" - }, - "drep": { - "id": "2d4cb680b5f400d3521d272b4295d61150e0eff3950ef4285406a953", - "type": "script_hash" - } - } } ], "account_identifier_signers": [] diff --git a/tests/integration/golden_examples/rosetta_java/construction/parse/complex_transactions/drep_delegation_with_stake_key_registration.json b/tests/integration/golden_examples/rosetta_java/construction/parse/complex_transactions/drep_delegation_with_stake_key_registration.json index 23fa279bb6..d47b822b2c 100644 --- a/tests/integration/golden_examples/rosetta_java/construction/parse/complex_transactions/drep_delegation_with_stake_key_registration.json +++ b/tests/integration/golden_examples/rosetta_java/construction/parse/complex_transactions/drep_delegation_with_stake_key_registration.json @@ -89,24 +89,6 @@ "type": "abstain" } } - }, - { - "operation_identifier": { - "index": 3 - }, - "type": "dRepVoteDelegation", - "account": { - "address": "stake_test1uql3k2h9kskfma8y53vth7wmptmlw8zxx67cx7c3pwj8sqs6zl0wr" - }, - "metadata": { - "staking_credential": { - "hex_bytes": "fdbee86a702c49d23f45ab80e697b93ec48b744d5f413ef59c338c3f7b2d2de8", - "curve_type": "edwards25519" - }, - "drep": { - "type": "abstain" - } - } } ], "account_identifier_signers": [] diff --git a/tests/integration/golden_examples/rosetta_java/construction/parse/complex_transactions/withdrawal_with_drep_delegation.json b/tests/integration/golden_examples/rosetta_java/construction/parse/complex_transactions/withdrawal_with_drep_delegation.json index f2cc94c1ad..9459362f8c 100644 --- a/tests/integration/golden_examples/rosetta_java/construction/parse/complex_transactions/withdrawal_with_drep_delegation.json +++ b/tests/integration/golden_examples/rosetta_java/construction/parse/complex_transactions/withdrawal_with_drep_delegation.json @@ -55,25 +55,6 @@ } } }, - { - "operation_identifier": { - "index": 3 - }, - "type": "dRepVoteDelegation", - "status": "", - "account": { - "address": "stake_test1uql3k2h9kskfma8y53vth7wmptmlw8zxx67cx7c3pwj8sqs6zl0wr" - }, - "metadata": { - "staking_credential": { - "hex_bytes": "fdbee86a702c49d23f45ab80e697b93ec48b744d5f413ef59c338c3f7b2d2de8", - "curve_type": "edwards25519" - }, - "drep": { - "type": "no_confidence" - } - } - }, { "operation_identifier": { "index": 2 @@ -102,6 +83,7 @@ "index": 3 }, "type": "dRepVoteDelegation", + "status": "", "account": { "address": "stake_test1uql3k2h9kskfma8y53vth7wmptmlw8zxx67cx7c3pwj8sqs6zl0wr" }, diff --git a/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_delegation_abstain.json b/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_delegation_abstain.json index 0113c06736..c4da1bd472 100644 --- a/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_delegation_abstain.json +++ b/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_delegation_abstain.json @@ -73,24 +73,6 @@ "type": "abstain" } } - }, - { - "operation_identifier": { - "index": 2 - }, - "type": "dRepVoteDelegation", - "account": { - "address": "stake_test1uql3k2h9kskfma8y53vth7wmptmlw8zxx67cx7c3pwj8sqs6zl0wr" - }, - "metadata": { - "staking_credential": { - "hex_bytes": "fdbee86a702c49d23f45ab80e697b93ec48b744d5f413ef59c338c3f7b2d2de8", - "curve_type": "edwards25519" - }, - "drep": { - "type": "abstain" - } - } } ], "account_identifier_signers": [] diff --git a/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_delegation_key_hash.json b/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_delegation_key_hash.json index 0d130a4068..9822a74516 100644 --- a/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_delegation_key_hash.json +++ b/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_delegation_key_hash.json @@ -74,25 +74,6 @@ "type": "key_hash" } } - }, - { - "operation_identifier": { - "index": 2 - }, - "type": "dRepVoteDelegation", - "account": { - "address": "stake_test1uql3k2h9kskfma8y53vth7wmptmlw8zxx67cx7c3pwj8sqs6zl0wr" - }, - "metadata": { - "staking_credential": { - "hex_bytes": "fdbee86a702c49d23f45ab80e697b93ec48b744d5f413ef59c338c3f7b2d2de8", - "curve_type": "edwards25519" - }, - "drep": { - "id": "f72050aa252ccc6bf75747184910c0b9386298167656f935d6b6c26a", - "type": "key_hash" - } - } } ], "account_identifier_signers": [] diff --git a/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_delegation_no_confidence.json b/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_delegation_no_confidence.json index 6db2e8b676..a301697e06 100644 --- a/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_delegation_no_confidence.json +++ b/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_delegation_no_confidence.json @@ -73,24 +73,6 @@ "type": "no_confidence" } } - }, - { - "operation_identifier": { - "index": 2 - }, - "type": "dRepVoteDelegation", - "account": { - "address": "stake_test1uql3k2h9kskfma8y53vth7wmptmlw8zxx67cx7c3pwj8sqs6zl0wr" - }, - "metadata": { - "staking_credential": { - "hex_bytes": "fdbee86a702c49d23f45ab80e697b93ec48b744d5f413ef59c338c3f7b2d2de8", - "curve_type": "edwards25519" - }, - "drep": { - "type": "no_confidence" - } - } } ], "account_identifier_signers": [] diff --git a/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_delegation_script_hash.json b/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_delegation_script_hash.json index 1eababb37a..6dbce0df84 100644 --- a/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_delegation_script_hash.json +++ b/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_delegation_script_hash.json @@ -74,25 +74,6 @@ "type": "script_hash" } } - }, - { - "operation_identifier": { - "index": 2 - }, - "type": "dRepVoteDelegation", - "account": { - "address": "stake_test1uql3k2h9kskfma8y53vth7wmptmlw8zxx67cx7c3pwj8sqs6zl0wr" - }, - "metadata": { - "staking_credential": { - "hex_bytes": "fdbee86a702c49d23f45ab80e697b93ec48b744d5f413ef59c338c3f7b2d2de8", - "curve_type": "edwards25519" - }, - "drep": { - "id": "2d4cb680b5f400d3521d272b4295d61150e0eff3950ef4285406a953", - "type": "script_hash" - } - } } ], "account_identifier_signers": [] diff --git a/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_id_provided_for_abstain.json b/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_id_provided_for_abstain.json index 3d36143fd5..bf2c1ea13f 100644 --- a/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_id_provided_for_abstain.json +++ b/tests/integration/golden_examples/rosetta_java/construction/parse/drep_vote_delegation/drep_id_provided_for_abstain.json @@ -73,25 +73,6 @@ "type": "abstain" } } - }, - { - "operation_identifier": { - "index": 2 - }, - "type": "dRepVoteDelegation", - "account": { - "address": "stake_test1uql3k2h9kskfma8y53vth7wmptmlw8zxx67cx7c3pwj8sqs6zl0wr" - }, - "metadata": { - "staking_credential": { - "hex_bytes": "fdbee86a702c49d23f45ab80e697b93ec48b744d5f413ef59c338c3f7b2d2de8", - "curve_type": "edwards25519" - }, - "drep": { - "id": "f72050aa252ccc6bf75747184910c0b9386298167656f935d6b6c26a", - "type": "abstain" - } - } } ], "account_identifier_signers": [] diff --git a/tests/load-tests/.env.example b/tests/load-tests/.env.example new file mode 100644 index 0000000000..6f5c870d92 --- /dev/null +++ b/tests/load-tests/.env.example @@ -0,0 +1,13 @@ +# Database connection for populating test data from Yaci Store +# Copy this file to .env and fill in your values + +# Database connection (default rosetta-java credentials) +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=rosetta-java +DB_USER=rosetta_db_admin +DB_PASSWORD=weakpwd#123_d +DB_SCHEMA=public + +# Alternative: use a connection string +# DATABASE_URL=postgresql://rosetta_db_admin:weakpwd#123_d@localhost:5432/rosetta-java diff --git a/tests/load-tests/.gitignore b/tests/load-tests/.gitignore new file mode 100644 index 0000000000..eef5deb249 --- /dev/null +++ b/tests/load-tests/.gitignore @@ -0,0 +1,20 @@ +# Environment files with credentials +.env + +# Python cache +__pycache__/ +*.py[cod] +*$py.class + +# Virtual environment +.venv/ + +# Locust reports +*.html +*_stats.csv +*_stats_history.csv +*_failures.csv +*_exceptions.csv + +# Test data backups +test_data.py.bak diff --git a/tests/load-tests/.python-version b/tests/load-tests/.python-version new file mode 100644 index 0000000000..e4fba21835 --- /dev/null +++ b/tests/load-tests/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/tests/load-tests/README.md b/tests/load-tests/README.md new file mode 100644 index 0000000000..dc9c573a95 --- /dev/null +++ b/tests/load-tests/README.md @@ -0,0 +1,240 @@ +# Locust Load Testing for Cardano Rosetta API + +This directory contains a **prototype** Locust-based load testing setup for comparing against the existing Apache Bench (`ab`) stability tests. + +## 🎯 Purpose (Spike Investigation) + +This is a **spike** to evaluate whether Locust can provide better insights than Apache Bench by: + +1. **Varying data per request** - avoiding database caching bias +2. **Categorizing data** - revealing performance patterns (light/medium/heavy loads) +3. **Tracking metrics by category** - identifying which data types are slow +4. **Providing richer metrics** - p95/p99, real-time UI, per-endpoint breakdown + +## 🏗️ Architecture + +``` +tests/load-tests/ +├── pyproject.toml # uv dependencies (locust, python-dotenv) +├── locustfile.py # Main load test with all 7 endpoints +├── test_data.py # Categorized test data (light/medium/heavy) +└── README.md # This file +``` + +## 📦 Setup + +```bash +cd tests/load-tests + +# Install dependencies with uv +uv sync + +# Activate virtual environment +source .venv/bin/activate +``` + +## 🗄️ Populate Test Data + +Before running load tests, you need to populate `test_data.py` with real preprod data. + +### Step 1: Port-forward the Yaci Store Database + +```bash +# SSH into preview machine and forward PostgreSQL port +ssh -L 5432:localhost:5432 preview +``` + +### Step 2: Configure Database Connection + +```bash +# Copy example environment file +cp .env.example .env + +# Edit .env with your database credentials +# DB_HOST=localhost +# DB_PORT=5432 +# DB_NAME=preprod +# DB_USER=postgres +# DB_PASSWORD=your_password +``` + +### Step 3: Run the Population Script + +```bash +uv run python populate_test_data.py +``` + +This will: +- Query Yaci Store database for diverse addresses/blocks/transactions +- Categorize by performance characteristics (light/medium/heavy) +- Generate `test_data.py` with real preprod data + +**Expected output:** +``` +📍 Querying addresses... + ✓ Light addresses (1-10 UTXOs): 10 + ✓ Medium addresses (100-1K UTXOs): 10 + ✓ Heavy addresses (10K+ UTXOs): 10 + +🧱 Querying blocks... + ✓ Light blocks (1-5 txs): 5 + ✓ Heavy blocks (100+ txs): 5 + +📄 Querying transactions... + ✓ Small transactions (<500 bytes): 10 + ✓ Large transactions (>10KB): 10 +``` + +## 🚀 Usage + +### 1. Port-forward Preprod Rosetta Instance + +```bash +# SSH into preview server and forward port 8082 +ssh -L 8082:localhost:8082 preview +``` + +### 2. Run Locust + +**Web UI Mode (Recommended for exploration):** +```bash +uv run locust --host=http://localhost:8082 +``` + +Then open http://localhost:8089 in your browser to: +- Set number of users +- Set spawn rate +- Monitor real-time metrics +- View charts and breakdowns + +**Headless Mode (CI/CD friendly):** +```bash +uv run locust --host=http://localhost:8082 \ + --users 50 \ + --spawn-rate 5 \ + --run-time 300s \ + --headless +``` + +**Generate HTML Report:** +```bash +uv run locust --host=http://localhost:8082 \ + --users 50 \ + --spawn-rate 5 \ + --run-time 300s \ + --headless \ + --html=report.html \ + --csv=results +``` + +This creates: +- `report.html` - Interactive HTML report +- `results_stats.csv` - Request statistics +- `results_stats_history.csv` - Time-series data +- `results_failures.csv` - Failure details + +## 📊 Metrics Provided + +Locust provides these metrics **out of the box**: + +- **Response time percentiles**: p50, p66, p75, p80, p90, p95, p99 +- **Throughput**: Requests per second (RPS) +- **Failure rate**: Count and percentage +- **Per-endpoint breakdown**: All metrics split by endpoint +- **Per-category breakdown**: Metrics split by data category (light/medium/heavy) + +### Example Output: + +``` +/account/balance [light] + Requests: 7000 + Failures: 0 + Avg: 45.23ms + p95: 89.12ms + p99: 123.45ms + +/account/balance [heavy] + Requests: 1000 + Failures: 0 + Avg: 456.78ms + p95: 890.12ms + p99: 1234.56ms +``` + +This clearly shows that **heavy addresses (10K+ UTXOs) are ~10x slower** - something that Apache Bench's identical payloads would miss! + +## 🎭 Comparison with Apache Bench + +| Feature | Apache Bench | Locust | +|---------|-------------|---------| +| **Data variation** | ❌ Identical payload | ✅ Categorized data | +| **Cache bias** | ❌ Heavy caching | ✅ Avoids caching | +| **Percentiles** | ✅ p95, p99 | ✅ p50-p99 | +| **Real-time UI** | ❌ CLI only | ✅ Web UI | +| **Endpoint weights** | ❌ Manual | ✅ Task decorators | +| **Category tracking** | ❌ Not possible | ✅ Built-in | +| **CI/CD** | ✅ Scriptable | ✅ Headless mode | +| **Reports** | 📊 Text output | 📊 HTML + CSV | + +## 📝 Test Data Structure + +Data is organized in `test_data.py` by **categories**: + +```python +ADDRESSES = { + "light": [...], # 1-10 UTXOs (fast) + "medium": [...], # 100-1K UTXOs (moderate) + "heavy": [...] # 10K+ UTXOs (slow) +} + +BLOCKS = { + "light": [...], # 1-5 transactions + "heavy": [...] # 100+ transactions +} + +TRANSACTIONS = { + "small": [...], # <500 bytes + "large": [...] # >10KB +} +``` + +**Weights** control distribution: +```python +CATEGORY_WEIGHTS = { + "address_light": 0.7, # 70% of requests + "address_heavy": 0.1, # 10% of requests +} +``` + +## 🔧 Next Steps (TODOs) + +- [ ] **Populate test_data.py** with actual preprod addresses/blocks/transactions + - Query preprod Rosetta to find addresses with varying UTXO counts + - Identify light vs heavy blocks + - Categorize transactions by size +- [ ] **Run comparison test**: ab vs Locust with identical vs varied data +- [ ] **Document findings**: metrics differences, insights, recommendations +- [ ] **Decide**: Full migration? Hybrid approach? Keep current ab tests? + +## 🎯 Success Criteria + +This spike is successful if: + +1. ✅ Locust can vary data per request +2. ✅ Metrics reveal performance degradation patterns by category +3. ✅ p95/p99 metrics match or exceed ab capabilities +4. ✅ CI/CD integration path is clear +5. ⏳ Comparison shows meaningful differences vs ab + +## 🚫 Out of Scope (for this spike) + +- Full implementation (prototype only) +- Grafana/monitoring integration +- Automated CSV generation +- Full endpoint coverage (7 endpoints is enough for spike) + +## 📚 Resources + +- [Locust Documentation](https://docs.locust.io/) +- [Task #638: Replace ab with Locust](https://github.com/cardano-foundation/cardano-rosetta-java/issues/638) +- Existing ab tests: `../../load-tests/stability_test.py` diff --git a/tests/load-tests/locustfile.py b/tests/load-tests/locustfile.py new file mode 100644 index 0000000000..454c7330f5 --- /dev/null +++ b/tests/load-tests/locustfile.py @@ -0,0 +1,340 @@ +""" +Locust load testing for Cardano Rosetta API with categorized data. + +This demonstrates the key advantage over Apache Bench: ability to cycle through +CATEGORIZED test data per request, revealing performance patterns and avoiding +database caching bias. + +The data is organized by categories (light/medium/heavy) to measure how +performance degrades with data complexity (UTXO count, block size, tx size). + +Usage: + # Port-forward preprod Rosetta instance first: + ssh -L 8082:localhost:8082 preview + + # Web UI mode (real-time monitoring) + uv run locust --host=http://localhost:8082 + + # Headless mode (CI/CD friendly) + uv run locust --host=http://localhost:8082 \ + --users 10 --spawn-rate 2 --run-time 60s --headless + + # Generate HTML report + uv run locust --host=http://localhost:8082 \ + --users 50 --spawn-rate 5 --run-time 300s --headless \ + --html=report.html +""" + +import random +from typing import Dict, List, Tuple + +from locust import HttpUser, between, events, task + +from test_data import ( + ADDRESSES, + BLOCKS, + CATEGORY_WEIGHTS, + CONSTRUCTION_METADATA, + NETWORK, + TRANSACTIONS, +) + + +class CategorizedDataProvider: + """ + Provides test data based on categories and weights. + + This allows us to: + 1. Control distribution (70% light, 20% medium, 10% heavy) + 2. Track performance by category + 3. Reveal degradation patterns + """ + + @staticmethod + def get_weighted_choice( + items_dict: Dict[str, List], weight_prefix: str + ) -> Tuple[str, any]: + """ + Select an item from a categorized dictionary based on weights. + + Returns: (category, item) + """ + categories = list(items_dict.keys()) + weights = [ + CATEGORY_WEIGHTS.get(f"{weight_prefix}_{cat}", 1.0) for cat in categories + ] + + # Normalize weights to sum to 1.0 + total = sum(weights) + normalized_weights = [w / total for w in weights] + + # Choose category based on weights + category = random.choices(categories, weights=normalized_weights)[0] + + # Choose random item from that category + items = items_dict[category] + if not items: + # Fallback to first available category if selected category is empty + for cat, cat_items in items_dict.items(): + if cat_items: + return cat, random.choice(cat_items) + raise ValueError(f"No data available in {weight_prefix}") + + item = random.choice(items) + return category, item + + +data_provider = CategorizedDataProvider() + + +class RosettaUser(HttpUser): + """ + Simulates a user making requests to Cardano Rosetta API. + + Task weights simulate realistic traffic distribution: + - Account Balance: 10x (most common) + - Account Coins: 8x + - Search Transactions: 5x + - Block: 5x + - Block Transaction: 3x + - Construction Metadata: 2x + - Network Status: 1x (baseline) + """ + + # Wait 1-3 seconds between requests (simulates real user behavior) + wait_time = between(1, 3) + + @task(1) + def network_status(self): + """ + Test /network/status endpoint. + + Weight: 1 (baseline) + No data variation needed. + """ + payload = { + "network_identifier": {"blockchain": "cardano", "network": NETWORK}, + "metadata": {}, + } + + with self.client.post( + "/network/status", json=payload, catch_response=True, name="/network/status" + ) as response: + if response.status_code == 200: + response.success() + else: + response.failure(f"Status {response.status_code}") + + @task(10) + def account_balance(self): + """ + Test /account/balance with CATEGORIZED addresses. + + Weight: 10 (most frequent) + Reveals performance degradation with UTXO count: + - Light addresses: fast + - Medium addresses: moderate + - Heavy addresses: slow (10K+ UTXOs) + """ + category, address = data_provider.get_weighted_choice(ADDRESSES, "address") + + payload = { + "network_identifier": {"blockchain": "cardano", "network": NETWORK}, + "account_identifier": {"address": address}, + } + + with self.client.post( + "/account/balance", + json=payload, + catch_response=True, + name=f"/account/balance [{category}]", # Track by category + ) as response: + if response.status_code == 200: + response.success() + else: + response.failure(f"Status {response.status_code}") + + @task(8) + def account_coins(self): + """ + Test /account/coins with CATEGORIZED addresses. + + Weight: 8 + Similar to account/balance but includes mempool. + """ + category, address = data_provider.get_weighted_choice(ADDRESSES, "address") + + payload = { + "network_identifier": {"blockchain": "cardano", "network": NETWORK}, + "account_identifier": {"address": address}, + } + + with self.client.post( + "/account/coins", + json=payload, + catch_response=True, + name=f"/account/coins [{category}]", + ) as response: + if response.status_code == 200: + response.success() + else: + response.failure(f"Status {response.status_code}") + + @task(5) + def block(self): + """ + Test /block with CATEGORIZED blocks. + + Weight: 5 + Reveals performance degradation with block size: + - Light blocks (1-5 txs): fast + - Heavy blocks (100+ txs): slow + """ + category, block = data_provider.get_weighted_choice(BLOCKS, "block") + + payload = { + "network_identifier": {"blockchain": "cardano", "network": NETWORK}, + "block_identifier": {"index": block["index"], "hash": block["hash"]}, + } + + with self.client.post( + "/block", json=payload, catch_response=True, name=f"/block [{category}]" + ) as response: + if response.status_code == 200: + response.success() + else: + response.failure(f"Status {response.status_code}") + + @task(3) + def block_transaction(self): + """ + Test /block/transaction with CATEGORIZED transactions. + + Weight: 3 + Reveals performance with transaction complexity. + """ + category, tx = data_provider.get_weighted_choice(TRANSACTIONS, "tx") + + payload = { + "network_identifier": {"blockchain": "cardano", "network": NETWORK}, + "block_identifier": {"index": tx["block_index"], "hash": tx["block_hash"]}, + "transaction_identifier": {"hash": tx["hash"]}, + } + + with self.client.post( + "/block/transaction", + json=payload, + catch_response=True, + name=f"/block/transaction [{category}]", + ) as response: + if response.status_code == 200: + response.success() + else: + response.failure(f"Status {response.status_code}") + + @task(5) + def search_transactions(self): + """ + Test /search/transactions with CATEGORIZED transactions. + + Weight: 5 + """ + category, tx = data_provider.get_weighted_choice(TRANSACTIONS, "tx") + + payload = { + "network_identifier": {"blockchain": "cardano", "network": NETWORK}, + "transaction_identifier": {"hash": tx["hash"]}, + } + + with self.client.post( + "/search/transactions", + json=payload, + catch_response=True, + name=f"/search/transactions [{category}]", + ) as response: + if response.status_code == 200: + response.success() + else: + response.failure(f"Status {response.status_code}") + + @task(2) + def construction_metadata(self): + """ + Test /construction/metadata with CATEGORIZED sizes. + + Weight: 2 + Reveals performance with transaction size estimation. + """ + category, metadata = data_provider.get_weighted_choice( + CONSTRUCTION_METADATA, "construction" + ) + + payload = { + "network_identifier": {"blockchain": "cardano", "network": NETWORK}, + "options": { + "transaction_size": metadata["transaction_size"], + "relative_ttl": metadata["relative_ttl"], + }, + } + + with self.client.post( + "/construction/metadata", + json=payload, + catch_response=True, + name=f"/construction/metadata [{category}]", + ) as response: + if response.status_code == 200: + response.success() + else: + response.failure(f"Status {response.status_code}") + + +@events.test_start.add_listener +def on_test_start(environment, **kwargs): + """Event handler called when test starts.""" + print("=" * 80) + print(f"Starting Locust load test") + print(f"Target: {environment.host}") + print(f"Network: {NETWORK}") + print(f"Data categories loaded:") + print(f" - Addresses: {sum(len(v) for v in ADDRESSES.values())} total") + for cat, items in ADDRESSES.items(): + print( + f" - {cat}: {len(items)} items (weight: {CATEGORY_WEIGHTS.get(f'address_{cat}', 1.0)})" + ) + print(f" - Blocks: {sum(len(v) for v in BLOCKS.values())} total") + for cat, items in BLOCKS.items(): + print( + f" - {cat}: {len(items)} items (weight: {CATEGORY_WEIGHTS.get(f'block_{cat}', 1.0)})" + ) + print(f" - Transactions: {sum(len(v) for v in TRANSACTIONS.values())} total") + for cat, items in TRANSACTIONS.items(): + print( + f" - {cat}: {len(items)} items (weight: {CATEGORY_WEIGHTS.get(f'tx_{cat}', 1.0)})" + ) + print("=" * 80) + + +@events.test_stop.add_listener +def on_test_stop(environment, **kwargs): + """Event handler called when test stops.""" + print("=" * 80) + print(f"Load test completed") + print(f"Total requests: {environment.stats.total.num_requests}") + print(f"Total failures: {environment.stats.total.num_failures}") + print(f"Average response time: {environment.stats.total.avg_response_time:.2f}ms") + print(f"p95: {environment.stats.total.get_response_time_percentile(0.95):.2f}ms") + print(f"p99: {environment.stats.total.get_response_time_percentile(0.99):.2f}ms") + print(f"Requests/sec: {environment.stats.total.total_rps:.2f}") + print("=" * 80) + print("\nPer-endpoint breakdown:") + print("-" * 80) + for name, stats in environment.stats.entries.items(): + if stats.num_requests > 0: + print(f"{name}") + print(f" Requests: {stats.num_requests}") + print(f" Failures: {stats.num_failures}") + print(f" Avg: {stats.avg_response_time:.2f}ms") + print(f" p95: {stats.get_response_time_percentile(0.95):.2f}ms") + print(f" p99: {stats.get_response_time_percentile(0.99):.2f}ms") + print("=" * 80) diff --git a/tests/load-tests/main.py b/tests/load-tests/main.py new file mode 100644 index 0000000000..e433c40266 --- /dev/null +++ b/tests/load-tests/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from load-tests!") + + +if __name__ == "__main__": + main() diff --git a/tests/load-tests/populate_test_data.py b/tests/load-tests/populate_test_data.py new file mode 100755 index 0000000000..b31e5c092e --- /dev/null +++ b/tests/load-tests/populate_test_data.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python3 +""" +Helper script to populate test_data.py with real preprod data from Yaci Store DB. + +Usage: + 1. Port-forward: ssh -L 5435:localhost:5435 preview + 2. Run: uv run python populate_test_data.py +""" + +import os +import sys +from typing import List, Dict +from dotenv import load_dotenv + +try: + import psycopg2 + from psycopg2.extras import RealDictCursor +except ImportError: + print("Error: psycopg2 not installed. Run: uv add psycopg2-binary") + sys.exit(1) + +load_dotenv() + +DB_CONFIG = { + 'host': os.getenv('DB_HOST', 'localhost'), + 'port': os.getenv('DB_PORT', '5435'), + 'database': os.getenv('DB_NAME', 'rosetta-java'), + 'user': os.getenv('DB_USER', 'rosetta_db_admin'), + 'password': os.getenv('DB_PASSWORD', ''), +} + +DATABASE_URL = os.getenv('DATABASE_URL') +TARGET_PER_CATEGORY = 10 + + +class YaciDBQuerier: + """Query Yaci Store database for categorized test data.""" + + def __init__(self): + self.conn = None + self.schema = os.getenv('DB_SCHEMA', 'public') + + def connect(self): + """Establish database connection.""" + try: + if DATABASE_URL: + self.conn = psycopg2.connect(DATABASE_URL) + else: + self.conn = psycopg2.connect(**DB_CONFIG) + self.conn.autocommit = True # Prevent transaction issues + print(f"✓ Connected to database: {DB_CONFIG['database']}") + print(f"✓ Using schema: {self.schema}") + except Exception as e: + print(f"✗ Database connection failed: {e}") + sys.exit(1) + + def close(self): + if self.conn: + self.conn.close() + + def get_light_addresses(self) -> List[str]: + """Find addresses with 1-10 UTXOs (fast queries).""" + query = f""" + SELECT owner_addr, COUNT(*) as utxo_count + FROM {self.schema}.address_utxo + WHERE owner_addr IS NOT NULL + GROUP BY owner_addr + HAVING COUNT(*) BETWEEN 1 AND 10 + ORDER BY RANDOM() + LIMIT %s + """ + return self._execute_query(query, (TARGET_PER_CATEGORY,), fetch_column='owner_addr') + + def get_medium_addresses(self) -> List[str]: + """Find addresses with 100-1000 UTXOs (moderate load).""" + query = f""" + SELECT owner_addr, COUNT(*) as utxo_count + FROM {self.schema}.address_utxo + WHERE owner_addr IS NOT NULL + GROUP BY owner_addr + HAVING COUNT(*) BETWEEN 100 AND 1000 + ORDER BY RANDOM() + LIMIT %s + """ + return self._execute_query(query, (TARGET_PER_CATEGORY,), fetch_column='owner_addr') + + def get_heavy_addresses(self) -> List[str]: + """Find addresses with 10000+ UTXOs (slow queries).""" + query = f""" + SELECT owner_addr, COUNT(*) as utxo_count + FROM {self.schema}.address_utxo + WHERE owner_addr IS NOT NULL + GROUP BY owner_addr + HAVING COUNT(*) >= 10000 + ORDER BY RANDOM() + LIMIT %s + """ + return self._execute_query(query, (TARGET_PER_CATEGORY,), fetch_column='owner_addr') + + def get_light_blocks(self) -> List[Dict]: + """Find blocks with 1-5 transactions (fast to process).""" + query = f""" + SELECT number as index, hash, no_of_txs as tx_count + FROM {self.schema}.block + WHERE no_of_txs BETWEEN 1 AND 5 + ORDER BY RANDOM() + LIMIT %s + """ + return self._execute_query(query, (TARGET_PER_CATEGORY // 2,)) + + def get_heavy_blocks(self) -> List[Dict]: + """Find blocks with 100+ transactions (slow to process).""" + query = f""" + SELECT number as index, hash, no_of_txs as tx_count + FROM {self.schema}.block + WHERE no_of_txs >= 100 + ORDER BY RANDOM() + LIMIT %s + """ + return self._execute_query(query, (TARGET_PER_CATEGORY // 2,)) + + def get_small_transactions(self) -> List[Dict]: + """Find transactions with few inputs/outputs (simple).""" + query = f""" + SELECT + t.tx_hash as hash, + t.block as block_index, + t.block_hash, + jsonb_array_length(t.inputs) as input_count, + jsonb_array_length(t.outputs) as output_count + FROM {self.schema}.transaction t + WHERE + jsonb_array_length(t.inputs) <= 2 + AND jsonb_array_length(t.outputs) <= 2 + AND t.block IS NOT NULL + ORDER BY RANDOM() + LIMIT %s + """ + return self._execute_query(query, (TARGET_PER_CATEGORY,)) + + def get_large_transactions(self) -> List[Dict]: + """Find transactions with many inputs/outputs (complex).""" + query = f""" + SELECT + t.tx_hash as hash, + t.block as block_index, + t.block_hash, + jsonb_array_length(t.inputs) as input_count, + jsonb_array_length(t.outputs) as output_count + FROM {self.schema}.transaction t + WHERE + (jsonb_array_length(t.inputs) >= 10 + OR jsonb_array_length(t.outputs) >= 10) + AND t.block IS NOT NULL + ORDER BY RANDOM() + LIMIT %s + """ + return self._execute_query(query, (TARGET_PER_CATEGORY,)) + + def _execute_query(self, query: str, params: tuple = (), fetch_column: str = None) -> List: + """Execute query and return results.""" + try: + with self.conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute(query, params) + results = cur.fetchall() + + if fetch_column: + return [row[fetch_column] for row in results if row[fetch_column]] + else: + return [dict(row) for row in results] + + except Exception as e: + print(f"✗ Query failed: {e}") + return [] + + +def generate_test_data_file(querier: YaciDBQuerier): + """Generate test_data.py with real preprod data.""" + + print("\n" + "=" * 80) + print("Querying Yaci Store database for categorized test data...") + print("=" * 80) + + print("\n📍 Querying addresses...") + light_addrs = querier.get_light_addresses() + print(f" ✓ Light addresses (1-10 UTXOs): {len(light_addrs)}") + + medium_addrs = querier.get_medium_addresses() + print(f" ✓ Medium addresses (100-1K UTXOs): {len(medium_addrs)}") + + heavy_addrs = querier.get_heavy_addresses() + print(f" ✓ Heavy addresses (10K+ UTXOs): {len(heavy_addrs)}") + + print("\n🧱 Querying blocks...") + light_blocks = querier.get_light_blocks() + print(f" ✓ Light blocks (1-5 txs): {len(light_blocks)}") + + heavy_blocks = querier.get_heavy_blocks() + print(f" ✓ Heavy blocks (100+ txs): {len(heavy_blocks)}") + + print("\n📄 Querying transactions...") + small_txs = querier.get_small_transactions() + print(f" ✓ Small transactions (few inputs/outputs): {len(small_txs)}") + + large_txs = querier.get_large_transactions() + print(f" ✓ Large transactions (many inputs/outputs): {len(large_txs)}") + + print("\n📝 Generating test_data.py...") + + content = f'''""" +Test data for Locust load testing - Preprod Network. + +AUTO-GENERATED by populate_test_data.py from Yaci Store database. +DO NOT EDIT MANUALLY - regenerate by running: uv run python populate_test_data.py +""" + +# Network configuration +NETWORK = "preprod" + +# Addresses categorized by UTXO count +ADDRESSES = {{ + "light": {light_addrs}, + "medium": {medium_addrs}, + "heavy": {heavy_addrs}, +}} + +# Blocks categorized by transaction count +BLOCKS = {{ + "light": {light_blocks}, + "heavy": {heavy_blocks}, +}} + +# Transactions categorized by complexity (inputs/outputs count) +TRANSACTIONS = {{ + "small": {small_txs}, + "large": {large_txs}, +}} + +# Construction metadata test cases +CONSTRUCTION_METADATA = {{ + "small_tx": [ + {{"transaction_size": 500, "relative_ttl": 1000}}, + {{"transaction_size": 800, "relative_ttl": 1500}}, + ], + "large_tx": [ + {{"transaction_size": 15000, "relative_ttl": 3600}}, + {{"transaction_size": 20000, "relative_ttl": 7200}}, + ], +}} + +# Weights control the distribution of requests across categories +CATEGORY_WEIGHTS = {{ + "address_light": 0.7, + "address_medium": 0.2, + "address_heavy": 0.1, + + "block_light": 0.8, + "block_heavy": 0.2, + + "tx_small": 0.7, + "tx_large": 0.3, + + "construction_small": 0.6, + "construction_large": 0.4, +}} +''' + + with open('test_data.py', 'w') as f: + f.write(content) + + print(" ✓ test_data.py generated successfully!") + + print("\n" + "=" * 80) + print("Summary:") + print("=" * 80) + print(f"Total addresses: {len(light_addrs) + len(medium_addrs) + len(heavy_addrs)}") + print(f"Total blocks: {len(light_blocks) + len(heavy_blocks)}") + print(f"Total transactions: {len(small_txs) + len(large_txs)}") + print("\n✓ Ready for load testing!") + print("\nNext steps:") + print(" 1. Review test_data.py to verify data quality") + print(" 2. Port-forward Rosetta API: ssh -L 8082:localhost:8082 preview") + print(" 3. Run Locust: uv run locust --host=http://localhost:8082") + + +def main(): + print("Preprod Load Test Data Generator") + print("=" * 80) + + querier = YaciDBQuerier() + + try: + querier.connect() + generate_test_data_file(querier) + finally: + querier.close() + + +if __name__ == "__main__": + main() diff --git a/tests/load-tests/pyproject.toml b/tests/load-tests/pyproject.toml new file mode 100644 index 0000000000..3c1468bd6f --- /dev/null +++ b/tests/load-tests/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "load-tests" +version = "0.1.0" +description = "Add your description here" +requires-python = ">=3.12" +dependencies = [ + "locust>=2.42.3", + "psycopg2-binary>=2.9.11", + "python-dotenv>=1.2.1", +] diff --git a/tests/load-tests/test_data.py b/tests/load-tests/test_data.py new file mode 100644 index 0000000000..cb1a8d80c0 --- /dev/null +++ b/tests/load-tests/test_data.py @@ -0,0 +1,56 @@ +""" +Test data for Locust load testing - Preprod Network. + +AUTO-GENERATED by populate_test_data.py from Yaci Store database. +DO NOT EDIT MANUALLY - regenerate by running: uv run python populate_test_data.py +""" + +# Network configuration +NETWORK = "preprod" + +# Addresses categorized by UTXO count +ADDRESSES = { + "light": ['addr_test1vpu98njlmzp5406al9cnujld0gq8mzurh2655y4l7qr9tms7wdc66', 'addr_test1qraudqut00yxlm2rd4d53ygjjztg2mk2gy5kjh6qy8ljpm3g3292mxuf3kq7nysjumlxjrlsfn9tp85r0l54l29x3qcs75n4j6', 'addr_test1vqd6nscqhpx9dk3zwrwukzwef3l050z0nys3vutq6yz5c7glclh77', 'addr_test1vztfh3gc8ega520qal5lv6dz0ydt6fhcz65x59dj4lhv0agc5dt4h', 'KjgoiXJS2con6pZQFx9UR1SzXYTDb4KLfmsQmrH3ZnN7R7oQDCtoG3mxXTeFPofy1uekwUP8RQ2kWSV4eB4SJ4ZD2iFnXECwPvy7PoXXZhjC', 'addr_test1vpef4cx46pz27xal8tey2w5h3exyapfe3u7vwt8rkvgdlsqtfm27v', 'addr_test1vq5edvurt4cxl8zyckzx0gsu7ycr7hls6feke9a7rtl7x7g74c8nq', 'addr_test1qzcdtu8ezk7270wwwy5cx96nk33q02pmuh06h2eh95puv4x6ududyye89rmj83hv788fpfcw79df656r86jnnu587jdsdnwysq', 'addr_test1qqyaww9l9wmwd05hr7pfdv5kwskp5anwhzamaxarcfhggf7a7yd2n0a98q8tzvaz2qujsl2jem8jt9ddp63veshz5hks6fgr00', 'addr_test1vrz92ng79nc4rf0673ev422alxg74l450cpljy4prgh6lccnu2skj'], + "medium": ['addr_test1zprvze2el2ecdrx0t0euqxxyk5zuy4jjewq5gqzlc2j9cd4s92sh3xwt0zcre2932qntdpd85r4ppa7cqe6gvmtwxgfsmahfs4', 'addr_test1qqvpfagzal48n7dg3279hyke7neh458u9pqm9x0s6q4vqud0y24upx4r8nxa9wveg6kys8h4rctzhgf9c4yjueqtqmeqsj4ke3', 'addr_test1qzp2a7ux93jph3n64eewufrqa82y0j3ag2r4vggxu2z9kcdlpk0qnjke5t9mr57j89t4dxhzsujprvyhr6w7n4nytt9s2a9s6q', 'addr_test1qrjde7acgmjpux7237808pclpdl4h9quts3ja7g4jm7w68wmeu7x6nvnlr6zgmy5rpy9fxyn3fpkxh9sv52pqgu0wywq5tfjw0', 'addr_test1wq20c349uqjnzemmvyvgwukgznjp4h9ezmk9czpxsdwz67q4g7xm7', 'addr_test1qr04qurckrj9vcugeu3jdneh3m6gg228fdgek2t3dzz7z6fez06qrmdhdrh7pcf96lyr74mm6g4yfcwps5nrqtnwc68q2zu0kv', 'addr_test1qpxumpuvtt7d07vqnhees96mztzj74fyt680q65cmrjtkknvum4m3utcgk9rfcevwg4zj5p8epvu68t2yhvd9wezvg0qanh287', 'addr_test1qq5thkqe6yqteekdahfqqh9q48frgzd4954kk2eknyz5eypu2qs6rsgt5w5gejhdkly2ph4uqlcwfgarchx2qwc72ufsd2spfs', 'addr_test1qpdg2wjrcvzjjn3nedrdnm248uqlm8jed2zcjzwmxyn2qe59qu90y49gamr5rj7d7ndy8mfe7emslxq5mppdjywq0f4qxkkuaq', 'addr_test1qz5nl4t5xwa5vut8smt4upk64v5dj5rkdupp6vwam2hplev7k9fcwefqwx9h3vlqjhvqt2qntxhdugzvr83c5al6tptqd5xnff'], + "heavy": ['addr_test1zr5ghvm0m8463gfjsvdwg09uerslfqkc7u5wcxueh7kf8mhxq98rskrmxdgx4hmax4uuqrklkzg0n52fk5gug57g7cts2ath6s', 'addr_test1wzdlt3c5azanf2hranc7pvt7lstd8f8lpjm38z9ld7jvx9s2746hp', 'addr_test1vz68ktz3tm33vwqkkhtg2c885unc6t9qkx2m7g04d38dquqa336hu', 'addr_test1qqq0vvmxc4gmz3e309q5hhmgfd9djaztjwcnjvfy06ezckwa54ce6qqms3qv3gf4pxy2jy88m6txz522q0vl5pznntrsxtcrv0', 'addr_test1wr7wkztlrpgjtr3y5hdy85lte0r04r9gg20m5mxer8hq34c0c496l', 'addr_test1vrkzm32hu4rks450dfc5w56lyjklk9ru72czs6heexfv5ms4aj7ym', 'addr_test1vpzn75adlmduxp26jekd9fl2le4md5plugzyxc8q27lr9jct64r5f', 'addr_test1vpmr498q23kh4hzn9qjtv9cuynpac5zh7sffklwt22hlmmqv3wufq', 'addr_test1vry2786k5pv7s365qxyvf7mhwt3h6ew6cn5a8uj84hscx2chmj38d', 'addr_test1vphrwwxg0utzm847w520zq33w9wjcjqwxf6pxc8myu6yjwcf89zt9'], +} + +# Blocks categorized by transaction count +BLOCKS = { + "light": [{'index': 2540949, 'hash': '524b36dea0af8dc774454a9c5c51b1c3619e2d41723e314f79fe2ddad8a25d45', 'tx_count': 1}, {'index': 2462234, 'hash': '8ed8f7774b6cafe544ce289e23384c75969f5d3aaba402016c39fc46c90cd4ff', 'tx_count': 1}, {'index': 1301312, 'hash': 'afb4e456d5c4ebdadb6a99f3d17c8b6ae64e790d8844e51627f0c8a3bd2d12ff', 'tx_count': 1}, {'index': 3217421, 'hash': '61237be5797896064b6a9a3aeec92c452b8f8be040701ea7ad8934cf5ccd308b', 'tx_count': 4}, {'index': 1482096, 'hash': '9969d5628aff464ec7adcd90318f60ea62c7507e788c8a5fd98b347dcd7e86a0', 'tx_count': 4}], + "heavy": [{'index': 1356056, 'hash': 'cbfebcce9fb58bd60c72dba37c1f32a2c9b19e587d3b7d48e303069dd9e4eef3', 'tx_count': 131}, {'index': 283411, 'hash': '561be268047239116824e15c01f544a9aa09827e97c853fccc786bd3036edb86', 'tx_count': 327}, {'index': 848536, 'hash': 'd4d213b287b554196c18d8c1e709983a13c3b289f5abf5377b00b19a21f52a17', 'tx_count': 157}, {'index': 3484522, 'hash': 'eee23e2ee6f3acc593aba3781d37ee170e16b2dd22cfbac8937e783d535d783e', 'tx_count': 392}, {'index': 2389912, 'hash': 'd1fa388224ee731705a6a3bcb9df5eb42ca852ee9b8d0de4b4f1dd2178062672', 'tx_count': 114}], +} + +# Transactions categorized by complexity (inputs/outputs count) +TRANSACTIONS = { + "small": [{'hash': '0ce73cda8dfa403f1202e515d9c69ab4b4d9b03c3c2e4b4b37be05cdc0052c53', 'block_index': 2049832, 'block_hash': 'c49ffa8a588ab47302a9e92927e4967f1dc83af1ef20483265ce351bda17142b', 'input_count': 2, 'output_count': 2}, {'hash': 'ca0cfd116a77fedc97f6bd77acf24b4fada009211f2c5788030764dab841ebcd', 'block_index': 722647, 'block_hash': '9354fe96cb86d4ecd1687069ac2fe0c56c2f73e3490b0232f931941b87c2fee5', 'input_count': 1, 'output_count': 2}, {'hash': '327ffc57f1bfcfa96cd2973798d6d05f70219d8edecb60309783c387f5480510', 'block_index': 3274697, 'block_hash': '5c60267f837227234d74b4c148b4cc251754531a4dbb5756be4ffd7a465c6fea', 'input_count': 1, 'output_count': 2}, {'hash': '2d61a6f3740498625bca522d1d399d5a9844a586ada5702f3012eb13afcdb83a', 'block_index': 1095633, 'block_hash': '8eb28a265d30873df02eed4158f7e0416e33815236b2ffa92b670d0b89717434', 'input_count': 2, 'output_count': 2}, {'hash': '23b317197dce3948099a87344aa87029fd32e32d491b43b7ed244d1e431e317b', 'block_index': 271829, 'block_hash': 'e8d13e87e78cd11badb5b8c5cda163bbb8598f7f7c7117af55e093382559e715', 'input_count': 2, 'output_count': 2}, {'hash': '5936cc53f66bd3cd5ec61e5f68b3c1cda77bf4b5a70742a6c62982fe7307a8a0', 'block_index': 746670, 'block_hash': '43900524e50448f175d0fdfd8b77fd9e62920436ea1bd553414f6a416ccc980b', 'input_count': 1, 'output_count': 2}, {'hash': '3080d1049eece509054c06e1ac9336bd5e57ca94028d6efea70b3e943737726e', 'block_index': 502548, 'block_hash': '9564d870532f1b7c745993623aa18febebb4cb8917a956375dca9596c8209d42', 'input_count': 1, 'output_count': 2}, {'hash': '77d99a7a87f888419d928510937d8be35201d897bb6fd260c0799a461bb09152', 'block_index': 349539, 'block_hash': '4248374754583c87dcda29b56bbbe0eb074b21f044e8bbd4add33d776bde5359', 'input_count': 2, 'output_count': 2}, {'hash': '358d14657c8953d1d81fbbe06ff5e39d12aad18616c7dfe2a6f58e984b7accb2', 'block_index': 3441917, 'block_hash': 'e86d87840173bd2768b3925515e11dbaf48e7bfaf60ee6079ee85cdf4adc1ff2', 'input_count': 1, 'output_count': 1}, {'hash': '9b12b9b8fc682fe30c8d4eaae5816f0a129c4d17e0f1453f35be862b998e85e8', 'block_index': 2555866, 'block_hash': 'b101c6bc2db95baf1add4480a291566383c32450447b73aff47405c916ce7a65', 'input_count': 1, 'output_count': 2}], + "large": [{'hash': '72c90c2083a7a4c54d263a031093c1354646a93c76580d3489bb9b671e029954', 'block_index': 1401831, 'block_hash': 'f551b7af97c818934456c4e20d231fa00a815b0999d720b9dc4b09cb189de587', 'input_count': 10, 'output_count': 11}, {'hash': 'edc8c6c95042f6a0d4dcca29bbeeb1930db51aaf749962f0f986c75e86555f11', 'block_index': 2866803, 'block_hash': '659d30bf36192460877e82babb48ca4e6cbc2a1440bbe1f288053e7a51a1e34e', 'input_count': 1, 'output_count': 31}, {'hash': 'e73252dae7f6f4e5342c6ecf2d9024b20e3d901522d7b9abbb7f1f600d0e5b3c', 'block_index': 698109, 'block_hash': '25b6b24f5910d2d325d8083e229c4fd9b97067e24f99847084149aa48996094f', 'input_count': 19, 'output_count': 32}, {'hash': 'c802cc5b3db2b5be0deb8474afe01ee833ca5d47dac7074646e9025666a45334', 'block_index': 275337, 'block_hash': '72af29bb833a6c0f6a12ab8f67f0448eb72043405ff5c70a3d0c2cc26b145152', 'input_count': 1, 'output_count': 13}, {'hash': '962571f8123cda447756e0bf78f97ad4ee3c7b248bebd6edc8c47f182b739228', 'block_index': 3540735, 'block_hash': '3084f4c1de7ef01e99f510a0b8dae380cc957e178ee31aca30c0877b66578a76', 'input_count': 41, 'output_count': 31}, {'hash': 'e0356dc84494d9ca75e85a725d7cdba6ec156e49f48bf3099b0f92bc351859b8', 'block_index': 3470157, 'block_hash': 'fa1ccfd22d2cd88bd28ad81dba010e10c5e65ed218ded201f9c2bd6985f44ff2', 'input_count': 12, 'output_count': 16}, {'hash': '57ce2e45f8dcdf6aa968be719812a8a1c19c61ba5a34851f16265f43f4bf5d1c', 'block_index': 3540742, 'block_hash': 'e9da1891e4b796156da479ad367c3611ae28e6e44fda4a88dad0821a7012ddd7', 'input_count': 8, 'output_count': 12}, {'hash': 'be4709bc250bd5e5a179a502619f7a8c94922e57e29bb58c84d4e91b362dc513', 'block_index': 2495159, 'block_hash': 'c5eeb12af9704c6ad15198d6b30bfec0ec9a78082ac548d46dd1be26ebfd8e27', 'input_count': 2, 'output_count': 30}, {'hash': '86f04a63d814a9c5ad469dc871a2110fe4fae251abeca2dd1423ac61eb3b3256', 'block_index': 4114317, 'block_hash': '5dd213795edf8d6b13e9e1fbb561d86097356f5b25bac4431fbb98d1cae03efa', 'input_count': 1, 'output_count': 101}, {'hash': '7a9ee97946a81fcb2f672b43096af6a237fa533abbda62e617799b66418079d7', 'block_index': 633692, 'block_hash': '9e986012a2095e09ffc69e5e19a51c1df02bb098bfaa56a90657ebbcfb75bf23', 'input_count': 37, 'output_count': 36}], +} + +# Construction metadata test cases +CONSTRUCTION_METADATA = { + "small_tx": [ + {"transaction_size": 500, "relative_ttl": 1000}, + {"transaction_size": 800, "relative_ttl": 1500}, + ], + "large_tx": [ + {"transaction_size": 15000, "relative_ttl": 3600}, + {"transaction_size": 20000, "relative_ttl": 7200}, + ], +} + +# Weights control the distribution of requests across categories +CATEGORY_WEIGHTS = { + "address_light": 0.7, + "address_medium": 0.2, + "address_heavy": 0.1, + + "block_light": 0.8, + "block_heavy": 0.2, + + "tx_small": 0.7, + "tx_large": 0.3, + + "construction_small": 0.6, + "construction_large": 0.4, +} diff --git a/tests/load-tests/uv.lock b/tests/load-tests/uv.lock new file mode 100644 index 0000000000..0cdb6306fa --- /dev/null +++ b/tests/load-tests/uv.lock @@ -0,0 +1,932 @@ +version = 1 +revision = 1 +requires-python = ">=3.12" + +[[package]] +name = "bidict" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764 }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 }, +] + +[[package]] +name = "brotli" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543 }, + { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288 }, + { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071 }, + { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913 }, + { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762 }, + { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494 }, + { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302 }, + { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913 }, + { url = "https://files.pythonhosted.org/packages/62/28/4d00cb9bd76a6357a66fcd54b4b6d70288385584063f4b07884c1e7286ac/brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", size = 334362 }, + { url = "https://files.pythonhosted.org/packages/1c/4e/bc1dcac9498859d5e353c9b153627a3752868a9d5f05ce8dedd81a2354ab/brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", size = 369115 }, + { url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523 }, + { url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289 }, + { url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076 }, + { url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880 }, + { url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737 }, + { url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440 }, + { url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313 }, + { url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945 }, + { url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368 }, + { url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116 }, + { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080 }, + { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453 }, + { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168 }, + { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098 }, + { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861 }, + { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594 }, + { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455 }, + { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164 }, + { url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280 }, + { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639 }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438 }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271 }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048 }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529 }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097 }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983 }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519 }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572 }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963 }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361 }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932 }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557 }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762 }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230 }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043 }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446 }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101 }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948 }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422 }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499 }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928 }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302 }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320 }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487 }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049 }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793 }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300 }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244 }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828 }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926 }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328 }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650 }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687 }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773 }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013 }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593 }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354 }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480 }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584 }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443 }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437 }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487 }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726 }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425 }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162 }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558 }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497 }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240 }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471 }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864 }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647 }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110 }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839 }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667 }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535 }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816 }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694 }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131 }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390 }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091 }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936 }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180 }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346 }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874 }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076 }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601 }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376 }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825 }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583 }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366 }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300 }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465 }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404 }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092 }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408 }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746 }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889 }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641 }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779 }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035 }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542 }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524 }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395 }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680 }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045 }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687 }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014 }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044 }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940 }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104 }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743 }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402 }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "configargparse" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/4d/6c9ef746dfcc2a32e26f3860bb4a011c008c392b83eabdfb598d1a8bbe5d/configargparse-1.7.1.tar.gz", hash = "sha256:79c2ddae836a1e5914b71d58e4b9adbd9f7779d4e6351a637b7d2d9b6c46d3d9", size = 43958 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/28/d28211d29bcc3620b1fece85a65ce5bb22f18670a03cd28ea4b75ede270c/configargparse-1.7.1-py3-none-any.whl", hash = "sha256:8b586a31f9d873abd1ca527ffbe58863c99f36d896e2829779803125e83be4b6", size = 25607 }, +] + +[[package]] +name = "flask" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308 }, +] + +[[package]] +name = "flask-cors" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/37/bcfa6c7d5eec777c4c7cf45ce6b27631cebe5230caf88d85eadd63edd37a/flask_cors-6.0.1.tar.gz", hash = "sha256:d81bcb31f07b0985be7f48406247e9243aced229b7747219160a0559edd678db", size = 13463 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/f8/01bf35a3afd734345528f98d0353f2a978a476528ad4d7e78b70c4d149dd/flask_cors-6.0.1-py3-none-any.whl", hash = "sha256:c7b2cbfb1a31aa0d2e5341eea03a6805349f7a61647daee1a15c46bbe981494c", size = 13244 }, +] + +[[package]] +name = "flask-login" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/6e/2f4e13e373bb49e68c02c51ceadd22d172715a06716f9299d9df01b6ddb2/Flask-Login-0.6.3.tar.gz", hash = "sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333", size = 48834 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/f5/67e9cc5c2036f58115f9fe0f00d203cf6780c3ff8ae0e705e7a9d9e8ff9e/Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d", size = 17303 }, +] + +[[package]] +name = "gevent" +version = "25.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation == 'CPython' and sys_platform == 'win32'" }, + { name = "greenlet", marker = "platform_python_implementation == 'CPython'" }, + { name = "zope-event" }, + { name = "zope-interface" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/48/b3ef2673ffb940f980966694e40d6d32560f3ffa284ecaeb5ea3a90a6d3f/gevent-25.9.1.tar.gz", hash = "sha256:adf9cd552de44a4e6754c51ff2e78d9193b7fa6eab123db9578a210e657235dd", size = 5059025 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/49/e55930ba5259629eb28ac7ee1abbca971996a9165f902f0249b561602f24/gevent-25.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:46b188248c84ffdec18a686fcac5dbb32365d76912e14fda350db5dc0bfd4f86", size = 2955991 }, + { url = "https://files.pythonhosted.org/packages/aa/88/63dc9e903980e1da1e16541ec5c70f2b224ec0a8e34088cb42794f1c7f52/gevent-25.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f2b54ea3ca6f0c763281cd3f96010ac7e98c2e267feb1221b5a26e2ca0b9a692", size = 1808503 }, + { url = "https://files.pythonhosted.org/packages/7a/8d/7236c3a8f6ef7e94c22e658397009596fa90f24c7d19da11ad7ab3a9248e/gevent-25.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7a834804ac00ed8a92a69d3826342c677be651b1c3cd66cc35df8bc711057aa2", size = 1890001 }, + { url = "https://files.pythonhosted.org/packages/4f/63/0d7f38c4a2085ecce26b50492fc6161aa67250d381e26d6a7322c309b00f/gevent-25.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:323a27192ec4da6b22a9e51c3d9d896ff20bc53fdc9e45e56eaab76d1c39dd74", size = 1855335 }, + { url = "https://files.pythonhosted.org/packages/95/18/da5211dfc54c7a57e7432fd9a6ffeae1ce36fe5a313fa782b1c96529ea3d/gevent-25.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6ea78b39a2c51d47ff0f130f4c755a9a4bbb2dd9721149420ad4712743911a51", size = 2109046 }, + { url = "https://files.pythonhosted.org/packages/a6/5a/7bb5ec8e43a2c6444853c4a9f955f3e72f479d7c24ea86c95fb264a2de65/gevent-25.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:dc45cd3e1cc07514a419960af932a62eb8515552ed004e56755e4bf20bad30c5", size = 1827099 }, + { url = "https://files.pythonhosted.org/packages/ca/d4/b63a0a60635470d7d986ef19897e893c15326dd69e8fb342c76a4f07fe9e/gevent-25.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34e01e50c71eaf67e92c186ee0196a039d6e4f4b35670396baed4a2d8f1b347f", size = 2172623 }, + { url = "https://files.pythonhosted.org/packages/d5/98/caf06d5d22a7c129c1fb2fc1477306902a2c8ddfd399cd26bbbd4caf2141/gevent-25.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acd6bcd5feabf22c7c5174bd3b9535ee9f088d2bbce789f740ad8d6554b18f3", size = 1682837 }, + { url = "https://files.pythonhosted.org/packages/5a/77/b97f086388f87f8ad3e01364f845004aef0123d4430241c7c9b1f9bde742/gevent-25.9.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:4f84591d13845ee31c13f44bdf6bd6c3dbf385b5af98b2f25ec328213775f2ed", size = 2973739 }, + { url = "https://files.pythonhosted.org/packages/3c/2e/9d5f204ead343e5b27bbb2fedaec7cd0009d50696b2266f590ae845d0331/gevent-25.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9cdbb24c276a2d0110ad5c978e49daf620b153719ac8a548ce1250a7eb1b9245", size = 1809165 }, + { url = "https://files.pythonhosted.org/packages/10/3e/791d1bf1eb47748606d5f2c2aa66571f474d63e0176228b1f1fd7b77ab37/gevent-25.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:88b6c07169468af631dcf0fdd3658f9246d6822cc51461d43f7c44f28b0abb82", size = 1890638 }, + { url = "https://files.pythonhosted.org/packages/f2/5c/9ad0229b2b4d81249ca41e4f91dd8057deaa0da6d4fbe40bf13cdc5f7a47/gevent-25.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b7bb0e29a7b3e6ca9bed2394aa820244069982c36dc30b70eb1004dd67851a48", size = 1857118 }, + { url = "https://files.pythonhosted.org/packages/49/2a/3010ed6c44179a3a5c5c152e6de43a30ff8bc2c8de3115ad8733533a018f/gevent-25.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2951bb070c0ee37b632ac9134e4fdaad70d2e660c931bb792983a0837fe5b7d7", size = 2111598 }, + { url = "https://files.pythonhosted.org/packages/08/75/6bbe57c19a7aa4527cc0f9afcdf5a5f2aed2603b08aadbccb5bf7f607ff4/gevent-25.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4e17c2d57e9a42e25f2a73d297b22b60b2470a74be5a515b36c984e1a246d47", size = 1829059 }, + { url = "https://files.pythonhosted.org/packages/06/6e/19a9bee9092be45679cb69e4dd2e0bf5f897b7140b4b39c57cc123d24829/gevent-25.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d94936f8f8b23d9de2251798fcb603b84f083fdf0d7f427183c1828fb64f117", size = 2173529 }, + { url = "https://files.pythonhosted.org/packages/ca/4f/50de9afd879440e25737e63f5ba6ee764b75a3abe17376496ab57f432546/gevent-25.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb51c5f9537b07da673258b4832f6635014fee31690c3f0944d34741b69f92fa", size = 1681518 }, + { url = "https://files.pythonhosted.org/packages/15/1a/948f8167b2cdce573cf01cec07afc64d0456dc134b07900b26ac7018b37e/gevent-25.9.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1a3fe4ea1c312dbf6b375b416925036fe79a40054e6bf6248ee46526ea628be1", size = 2982934 }, + { url = "https://files.pythonhosted.org/packages/9b/ec/726b146d1d3aad82e03d2e1e1507048ab6072f906e83f97f40667866e582/gevent-25.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0adb937f13e5fb90cca2edf66d8d7e99d62a299687400ce2edee3f3504009356", size = 1813982 }, + { url = "https://files.pythonhosted.org/packages/35/5d/5f83f17162301662bd1ce702f8a736a8a8cac7b7a35e1d8b9866938d1f9d/gevent-25.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:427f869a2050a4202d93cf7fd6ab5cffb06d3e9113c10c967b6e2a0d45237cb8", size = 1894902 }, + { url = "https://files.pythonhosted.org/packages/83/cd/cf5e74e353f60dab357829069ffc300a7bb414c761f52cf8c0c6e9728b8d/gevent-25.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c049880175e8c93124188f9d926af0a62826a3b81aa6d3074928345f8238279e", size = 1861792 }, + { url = "https://files.pythonhosted.org/packages/dd/65/b9a4526d4a4edce26fe4b3b993914ec9dc64baabad625a3101e51adb17f3/gevent-25.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5a67a0974ad9f24721034d1e008856111e0535f1541499f72a733a73d658d1c", size = 2113215 }, + { url = "https://files.pythonhosted.org/packages/e5/be/7d35731dfaf8370795b606e515d964a0967e129db76ea7873f552045dd39/gevent-25.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d0f5d8d73f97e24ea8d24d8be0f51e0cf7c54b8021c1fddb580bf239474690f", size = 1833449 }, + { url = "https://files.pythonhosted.org/packages/65/58/7bc52544ea5e63af88c4a26c90776feb42551b7555a1c89c20069c168a3f/gevent-25.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddd3ff26e5c4240d3fbf5516c2d9d5f2a998ef87cfb73e1429cfaeaaec860fa6", size = 2176034 }, + { url = "https://files.pythonhosted.org/packages/c2/69/a7c4ba2ffbc7c7dbf6d8b4f5d0f0a421f7815d229f4909854266c445a3d4/gevent-25.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:bb63c0d6cb9950cc94036a4995b9cc4667b8915366613449236970f4394f94d7", size = 1703019 }, +] + +[[package]] +name = "geventhttpclient" +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "brotli" }, + { name = "certifi" }, + { name = "gevent" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/6b/c9be60c4f4de31e9234d5cd927096cb44136767aa58b21ee4e3f0a60a15e/geventhttpclient-2.3.5.tar.gz", hash = "sha256:0f0cf13528de7628a21b28b80ee90a471d4840e3fe26f84b394644c366595151", size = 83673 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/6c/bef9fbdf02ffbeea0fdc5c928c0a9824e2797951b93db295ace43efbd2c5/geventhttpclient-2.3.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c262e295fa017ad7d6d62873e2a781478cb03852b1d0559ccfba598ac059fd23", size = 69745 }, + { url = "https://files.pythonhosted.org/packages/af/71/d9dfd1fd5d3ee0674942d0cdf1342001ce2c63cd95ffbd91901ace2820ab/geventhttpclient-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:44b822ce5ebddac4cd4ac4199acc2cbec1e968e3bce0ed4c62a4ce8ffaae9277", size = 51388 }, + { url = "https://files.pythonhosted.org/packages/6e/49/711a28fe4ac99537a051a1839872d740e40825be66c9c4b74d966f3554ef/geventhttpclient-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e8926ac5338764cabcf8fb54be706a6533d45756f164940a7568b03c80adb1f8", size = 51133 }, + { url = "https://files.pythonhosted.org/packages/15/6b/d1a6056deb14aff2839b11e9b1a2536b0d47f1553f7385bf83180f764210/geventhttpclient-2.3.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e84e3985a6a3f9ce39efb8fcfa4273365de2898739eea07d4b259b30ae8d58b7", size = 114985 }, + { url = "https://files.pythonhosted.org/packages/98/56/fb6b7a7c5d1b5ebe18ff9eff9f877f059231b436012c2f0498d17198f28b/geventhttpclient-2.3.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abc63685019c5d6ec08d036248a0743df36e2afa6ab8a1fc833e2a82d0be723f", size = 115657 }, + { url = "https://files.pythonhosted.org/packages/51/8b/35068d11f81f4c928dfc188db3c1a2db92f8236ad30d2be50ef64e6f59c7/geventhttpclient-2.3.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18e129e49ec1dadfb5fc067ac15bd43a3e6f80ddb2b6fd994ce8235c4f8b5e92", size = 121674 }, + { url = "https://files.pythonhosted.org/packages/7f/d3/3fe234574f6baf1f85784136757b5715b4636bc3576cc9b14d303949ca1d/geventhttpclient-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a04a3bdf102100a14dab58991e984b54e7db9ed950d12d8cb9fdfe5fc5088f0", size = 111577 }, + { url = "https://files.pythonhosted.org/packages/b7/e8/186b62f2774b5bb33b08576a8094b7bce1145553df9843cfb86ad10fe301/geventhttpclient-2.3.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3ecaea089408add812a7c1ad9c6043741155f4fbe5ed5c1741ce9322044f419d", size = 118453 }, + { url = "https://files.pythonhosted.org/packages/e6/b2/3374065e10242c3013dc8a5973abd7c1514cd013a3f40b28a40de4070849/geventhttpclient-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:47fa4d0b9f1739570960b5125e5c86974dff8baaa245d3b96f3e214efbb3ae5e", size = 112226 }, + { url = "https://files.pythonhosted.org/packages/84/d9/ea4ed02204c84888acae2d834cf09e165388ee450ec90fc0deed6106dce0/geventhttpclient-2.3.5-cp312-cp312-win32.whl", hash = "sha256:677be43d1941543d2897123b98831867a48286c12cd378ad995f545442854558", size = 48360 }, + { url = "https://files.pythonhosted.org/packages/ec/75/7686abde7a8b2b83040a306339558b6964ebfad66ff5b83c83a4a0aaa8a7/geventhttpclient-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:cee0ce8bb23668fb6b1a2cc572cb3d01765c5d95734c5d205e1ff459708e4c19", size = 48994 }, + { url = "https://files.pythonhosted.org/packages/8d/a7/bdcb92b4d6240538eaf7194bde4a086607a86061e31acbd4c065958e52ea/geventhttpclient-2.3.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:700d28d00d77e3c32d9e65dc078ee52a5ca77c3ac16f55674ae36250fe2550a1", size = 69750 }, + { url = "https://files.pythonhosted.org/packages/59/19/91d9c585a5c3221882bc372de19885c14b04534895e68ebc8fd66a897a3c/geventhttpclient-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9a0c0d37fc2bc60dea9d66e839c497374a5c15ec45523ae358593c760a5d433e", size = 51392 }, + { url = "https://files.pythonhosted.org/packages/c5/30/2297177c2a5d6fde7345ff44543afb61ede37eb4b9f156fea8aed2593776/geventhttpclient-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c8fceda991eab2afd95c92b3e4177ce684ea8738ef15043ebc911eb7b336dc38", size = 51126 }, + { url = "https://files.pythonhosted.org/packages/9c/eb/b11c05d6864e4726795b6a4b41c30a6e6df5f3d4709e24a3db1f1c597240/geventhttpclient-2.3.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1fbc86461e993ff6e15ee33a8252bcec6aede03ce8d8640da4205112eba28d11", size = 115000 }, + { url = "https://files.pythonhosted.org/packages/ae/09/0a5efe53df27303793a2aeaf1181fde21e490bbae9bd2cdf4ea2befba867/geventhttpclient-2.3.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d8c2b55d2c3e22be8a6fa48acde4771dcdecf01309125f1d8630de8bb4daa", size = 115693 }, + { url = "https://files.pythonhosted.org/packages/0e/60/ab039a4eb2537fa0d7c70f467fa97816035b8c0556a7cd5bf830be67160a/geventhttpclient-2.3.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:006d301f98222d1649b5df7e5b475eefc79519fbaf3309c5fde606db188686c8", size = 121682 }, + { url = "https://files.pythonhosted.org/packages/5a/8b/c480772879b7b731c1bf4301da9df55bcb9c6e947d8a71ec2ba6705b39e6/geventhttpclient-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:75bd6b8131e4c566ef69df881f1861e90d00c1222e41ab211f328bec71559d75", size = 111666 }, + { url = "https://files.pythonhosted.org/packages/b9/93/b31c882d3748ca39528ce755bba243ef316803acc6a4f9157d74332bc147/geventhttpclient-2.3.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3081221440b270e535cc796b8d3d4e9c423e89a58ac825de94af5a630ea9911e", size = 118445 }, + { url = "https://files.pythonhosted.org/packages/3e/c0/3c035c26e1740fd3cf83d73b36657ed2c227a6ae4d097898127b1ae71e46/geventhttpclient-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee48b9cdde46f4c1e4609f9ba7e4a4096f0447bb5e07ddd531b3bb67461cc4e2", size = 112256 }, + { url = "https://files.pythonhosted.org/packages/62/57/d010e546212f36797090ff88df4ab38becb01749e9ed07bf9a5916305ef0/geventhttpclient-2.3.5-cp313-cp313-win32.whl", hash = "sha256:22b6bd036ce0cfe5e7a280eda17ab6358b7a0f340ed5893015f3d2575624b4a4", size = 48357 }, + { url = "https://files.pythonhosted.org/packages/9d/aa/eaeefdeec8fb35dc707be4f3fa0b0034053727aa0ce6729fe13f6ce22751/geventhttpclient-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:4d89b59ee8b672b355a598dd2a964b768c1acf9e0c3429bb8e393a9eea31dd26", size = 48986 }, + { url = "https://files.pythonhosted.org/packages/70/50/5fee5b08580997e17fb796bdde90cd6d7bdb7a971b7b736bc606370a5e49/geventhttpclient-2.3.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7a5f79c9bd0a47b18e3cf58c27f9aa4e8e13fedb12f20ea494771ad4d721f053", size = 70024 }, + { url = "https://files.pythonhosted.org/packages/74/e7/c77dc7b00cd59c59b63b2bfe3c9ebf7d4583a5eabcc6e31a5b64d29cb923/geventhttpclient-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2e294e70d7c30f0209921dc1548428887923e85f28a78a3905b4a11aefb13746", size = 51515 }, + { url = "https://files.pythonhosted.org/packages/46/bb/946b17788d00e02a8ef2a1fde6c4769dacb00a2d628e6bf2f06e2991c885/geventhttpclient-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c5d8a4a57ecc9281c037544645141514a5753db6d78b2dda014f11ef639cd641", size = 51169 }, + { url = "https://files.pythonhosted.org/packages/53/90/105337fda82dd39a735042077e4e86cccca527aea4aaf9f8cf65c0dc2416/geventhttpclient-2.3.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:18f1a02a1f51731e7433876be07859c8b1ccfd826e79ce7db03a54a1c64c9cb3", size = 115034 }, + { url = "https://files.pythonhosted.org/packages/d2/ef/3dea5e1f08c8e02769dd9facd9606b1c77d697d73e69feec856c5c708823/geventhttpclient-2.3.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4024739fd05b193b233e084014ee9d87f49cbeb24727d4adf23698417f6fff13", size = 115760 }, + { url = "https://files.pythonhosted.org/packages/e0/4c/9a32a96636aadec1c043fdefbf4b0150b532b3df9f02afb3a66d008f222c/geventhttpclient-2.3.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4cabd19028ccbfa5871d550f627c7b9e163de99f7ad80d451ffcbeee6fb427d9", size = 121757 }, + { url = "https://files.pythonhosted.org/packages/15/83/7d491256c1cfe9208ffa0ee7780699918b5b24d5336f719e17a4909df8be/geventhttpclient-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:228e639471ed636a7ea46b17fdd207da34f3519e6f84da30b510673ddf2fe2a6", size = 111747 }, + { url = "https://files.pythonhosted.org/packages/c0/27/5fdc2b47a3975d050a665c1f8562bcaf6e2cf5fc92d56ee1f4ec9ec48210/geventhttpclient-2.3.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ac0d3da9228f53f7a4960619172a6b6c11e0b3e8a470903166d83af66bfc8ce6", size = 118489 }, + { url = "https://files.pythonhosted.org/packages/d3/e0/704fa92777563f24beebee63567b0e04601e13ae171764dc67c7da5a8f2c/geventhttpclient-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d84c96d8b83c5e9b9059e4f2f62917eed834519c00b61d820b2d6aaefb4012a2", size = 112198 }, + { url = "https://files.pythonhosted.org/packages/16/6f/ac0e5d6b51a03183ee171ba3d4fd3cbfed7284d3dceaef37b6c209a67597/geventhttpclient-2.3.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:849bd108028ae0fc24ed65ca8e693c8d4ac140ecffa394e69fc77203c4dd93a2", size = 70417 }, + { url = "https://files.pythonhosted.org/packages/09/ca/6c3de521fd84a52505269ca2afd713d5d72d04badb0a27cbd2a4964d39ff/geventhttpclient-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3c412be766aced0bec5d4a7b12a499bc8619a6d692ac2f6df7b8062de26f724b", size = 51695 }, + { url = "https://files.pythonhosted.org/packages/40/da/47413b2483b98cd9e519c76a52aa82f049cb586ee12420e9dd4f13c56d67/geventhttpclient-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:29a8efd438bf13f69bf5099e7577c44fcec8864a832b1de39c484346f0a9bf62", size = 51384 }, + { url = "https://files.pythonhosted.org/packages/02/8e/c45858e81cfe8208f41e186896403f711fd0caae3298c11c0e5d6f1638cf/geventhttpclient-2.3.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ab68459780add7b52ada0092af1a4773d0acc870373e6fd21179d9e32d23bfb", size = 117934 }, + { url = "https://files.pythonhosted.org/packages/6e/a9/ae0c4b5b50090c878eeee938747e3cae94776ac066fee35ca2bc51f146bc/geventhttpclient-2.3.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:966ec7a7948adbf2dc5f68d76119d29f05e0c1f645c0d516a5ddb35f9e5d3242", size = 119584 }, + { url = "https://files.pythonhosted.org/packages/9d/a5/82bff95f26d4fda52a9279142a47fcf49bc3a70b2489a55cf609481d0081/geventhttpclient-2.3.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693d8fea804cd2547b9cc9bab13c73f9394b912391ab6e34ea3719a1a875e58c", size = 125389 }, + { url = "https://files.pythonhosted.org/packages/29/24/445a824edd51e43bfd81fa03f91b91580540592f312237c21a75c97e2fa4/geventhttpclient-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac03db48b1e0e913b3becd1e5fb2b52453754172be6868e067787f72cd1158ed", size = 115216 }, + { url = "https://files.pythonhosted.org/packages/49/d8/ef1c37860cc8f0f5d7ce8086bf385818fcc5e8e44c1fe4aad3783c34eaef/geventhttpclient-2.3.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:79e2afab2ec6562bb3814bdac6bb04333f3c6ab4824666565a73f73caf91d8fd", size = 121867 }, + { url = "https://files.pythonhosted.org/packages/7a/50/75d5a7d123015c8cd57710fecc7c1ecdecb7a98381034c1f87ba5dfb87e4/geventhttpclient-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7803e3e2db5f2bc87743afd015b86b7250c20dc4ace68899b2510a98519d8643", size = 114998 }, +] + +[[package]] +name = "greenlet" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079 }, + { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997 }, + { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185 }, + { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926 }, + { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839 }, + { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586 }, + { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281 }, + { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142 }, + { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846 }, + { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814 }, + { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899 }, + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814 }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073 }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191 }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516 }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169 }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497 }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662 }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210 }, + { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759 }, + { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288 }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685 }, + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586 }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346 }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218 }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659 }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355 }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512 }, + { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508 }, + { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760 }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425 }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "load-tests" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "locust" }, + { name = "psycopg2-binary" }, + { name = "python-dotenv" }, +] + +[package.metadata] +requires-dist = [ + { name = "locust", specifier = ">=2.42.3" }, + { name = "psycopg2-binary", specifier = ">=2.9.11" }, + { name = "python-dotenv", specifier = ">=1.2.1" }, +] + +[[package]] +name = "locust" +version = "2.42.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "configargparse" }, + { name = "flask" }, + { name = "flask-cors" }, + { name = "flask-login" }, + { name = "gevent" }, + { name = "geventhttpclient" }, + { name = "locust-cloud" }, + { name = "msgpack" }, + { name = "psutil" }, + { name = "pytest" }, + { name = "python-engineio" }, + { name = "python-socketio", extra = ["client"] }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "pyzmq" }, + { name = "requests" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/f9/ba22c81fb6fbb81610e4c661c6ffe4f495cbb5d5e8296de31964e710feb8/locust-2.42.3.tar.gz", hash = "sha256:01227890df7bf5268332f2dc041558ae45561b72a06193b068f7e02774f75ab2", size = 1416478 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/b3/38dd0eb197ca63d1ee39f6cedfb1aab8e6c844da3c8f3fd8d0f41192a293/locust-2.42.3-py3-none-any.whl", hash = "sha256:89c6359abcb79f8cbd870cd33c01acd3dc099baaea8774155cfdede4167d6a1d", size = 1435187 }, +] + +[[package]] +name = "locust-cloud" +version = "1.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "configargparse" }, + { name = "gevent" }, + { name = "platformdirs" }, + { name = "python-engineio" }, + { name = "python-socketio", extra = ["client"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/59/9779e5606e9580795ad1fdf45e259300a1cbd18155994e84277fa7bb2aa9/locust_cloud-1.29.0.tar.gz", hash = "sha256:2fd2465eef49253e9c39ac6f7d7ac18588159ceff9520218860de0d798581e44", size = 457177 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/1d/f2442ba26ebee956e54c84dbfe7c275d354707faf23232f0af59c71114ce/locust_cloud-1.29.0-py3-none-any.whl", hash = "sha256:abe5d1ac82f7b39d5a26e5ef8263091f12f9c0d4416b7029b12461f67e03c6ec", size = 413323 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615 }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020 }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332 }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947 }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962 }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760 }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529 }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015 }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540 }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105 }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906 }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622 }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029 }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374 }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980 }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990 }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784 }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588 }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041 }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543 }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113 }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911 }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658 }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066 }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639 }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569 }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284 }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801 }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769 }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642 }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612 }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200 }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973 }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619 }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029 }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408 }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005 }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048 }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821 }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606 }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043 }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747 }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341 }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073 }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661 }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069 }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670 }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598 }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261 }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835 }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733 }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672 }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819 }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426 }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146 }, +] + +[[package]] +name = "msgpack" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939 }, + { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064 }, + { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131 }, + { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556 }, + { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920 }, + { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013 }, + { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096 }, + { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708 }, + { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119 }, + { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212 }, + { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315 }, + { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721 }, + { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657 }, + { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668 }, + { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040 }, + { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037 }, + { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631 }, + { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118 }, + { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127 }, + { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981 }, + { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885 }, + { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658 }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290 }, + { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234 }, + { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391 }, + { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787 }, + { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453 }, + { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264 }, + { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076 }, + { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242 }, + { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509 }, + { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957 }, + { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910 }, + { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197 }, + { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772 }, + { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "platformdirs" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651 }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "psutil" +version = "7.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", size = 489059 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/93/0c49e776b8734fef56ec9c5c57f923922f2cf0497d62e0f419465f28f3d0/psutil-7.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc", size = 239751 }, + { url = "https://files.pythonhosted.org/packages/6f/8d/b31e39c769e70780f007969815195a55c81a63efebdd4dbe9e7a113adb2f/psutil-7.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0", size = 240368 }, + { url = "https://files.pythonhosted.org/packages/62/61/23fd4acc3c9eebbf6b6c78bcd89e5d020cfde4acf0a9233e9d4e3fa698b4/psutil-7.1.3-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7", size = 287134 }, + { url = "https://files.pythonhosted.org/packages/30/1c/f921a009ea9ceb51aa355cb0cc118f68d354db36eae18174bab63affb3e6/psutil-7.1.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251", size = 289904 }, + { url = "https://files.pythonhosted.org/packages/a6/82/62d68066e13e46a5116df187d319d1724b3f437ddd0f958756fc052677f4/psutil-7.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa", size = 249642 }, + { url = "https://files.pythonhosted.org/packages/df/ad/c1cd5fe965c14a0392112f68362cfceb5230819dbb5b1888950d18a11d9f/psutil-7.1.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee", size = 245518 }, + { url = "https://files.pythonhosted.org/packages/2e/bb/6670bded3e3236eb4287c7bcdc167e9fae6e1e9286e437f7111caed2f909/psutil-7.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b403da1df4d6d43973dc004d19cee3b848e998ae3154cc8097d139b77156c353", size = 239843 }, + { url = "https://files.pythonhosted.org/packages/b8/66/853d50e75a38c9a7370ddbeefabdd3d3116b9c31ef94dc92c6729bc36bec/psutil-7.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad81425efc5e75da3f39b3e636293360ad8d0b49bed7df824c79764fb4ba9b8b", size = 240369 }, + { url = "https://files.pythonhosted.org/packages/41/bd/313aba97cb5bfb26916dc29cf0646cbe4dd6a89ca69e8c6edce654876d39/psutil-7.1.3-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f33a3702e167783a9213db10ad29650ebf383946e91bc77f28a5eb083496bc9", size = 288210 }, + { url = "https://files.pythonhosted.org/packages/c2/fa/76e3c06e760927a0cfb5705eb38164254de34e9bd86db656d4dbaa228b04/psutil-7.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fac9cd332c67f4422504297889da5ab7e05fd11e3c4392140f7370f4208ded1f", size = 291182 }, + { url = "https://files.pythonhosted.org/packages/0f/1d/5774a91607035ee5078b8fd747686ebec28a962f178712de100d00b78a32/psutil-7.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:3792983e23b69843aea49c8f5b8f115572c5ab64c153bada5270086a2123c7e7", size = 250466 }, + { url = "https://files.pythonhosted.org/packages/00/ca/e426584bacb43a5cb1ac91fae1937f478cd8fbe5e4ff96574e698a2c77cd/psutil-7.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:31d77fcedb7529f27bb3a0472bea9334349f9a04160e8e6e5020f22c59893264", size = 245756 }, + { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359 }, + { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171 }, + { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261 }, + { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635 }, + { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633 }, + { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608 }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603 }, + { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509 }, + { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159 }, + { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234 }, + { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236 }, + { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083 }, + { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281 }, + { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010 }, + { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641 }, + { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940 }, + { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147 }, + { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572 }, + { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529 }, + { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242 }, + { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258 }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295 }, + { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133 }, + { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383 }, + { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168 }, + { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712 }, + { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549 }, + { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215 }, + { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567 }, + { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755 }, + { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646 }, + { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701 }, + { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293 }, + { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184 }, + { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650 }, + { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663 }, + { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737 }, + { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643 }, + { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913 }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140 }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750 }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230 }, +] + +[[package]] +name = "python-engineio" +version = "4.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "simple-websocket" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/d8/63e5535ab21dc4998ba1cfe13690ccf122883a38f025dca24d6e56c05eba/python_engineio-4.12.3.tar.gz", hash = "sha256:35633e55ec30915e7fc8f7e34ca8d73ee0c080cec8a8cd04faf2d7396f0a7a7a", size = 91910 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/f0/c5aa0a69fd9326f013110653543f36ece4913c17921f3e1dbd78e1b423ee/python_engineio-4.12.3-py3-none-any.whl", hash = "sha256:7c099abb2a27ea7ab429c04da86ab2d82698cdd6c52406cb73766fe454feb7e1", size = 59637 }, +] + +[[package]] +name = "python-socketio" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bidict" }, + { name = "python-engineio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/02f5970c82285bd015ec433078bfc3275580b03715ed6024607dbe0f1966/python_socketio-5.14.3.tar.gz", hash = "sha256:cd8da5e0666e741b4be19e07882e880f57a4751d1645f92c2bc746c95f23b1eb", size = 124266 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/1a/b393a06aa6f2f6ab4a9c5c160a62d488b17d6da5cf93a67bc13a6e3239cd/python_socketio-5.14.3-py3-none-any.whl", hash = "sha256:a5208c1bbf45a8d6328d01ed67e3fa52ec8b186fd3ea44cfcfcbd120f0c71fbe", size = 79010 }, +] + +[package.optional-dependencies] +client = [ + { name = "requests" }, + { name = "websocket-client" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543 }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040 }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102 }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700 }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700 }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318 }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714 }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800 }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540 }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279 }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645 }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574 }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995 }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070 }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121 }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550 }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184 }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480 }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993 }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436 }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301 }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197 }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275 }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469 }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961 }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282 }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468 }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394 }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964 }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029 }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541 }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197 }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175 }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427 }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929 }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193 }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388 }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316 }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472 }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401 }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170 }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 }, +] + +[[package]] +name = "simple-websocket" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842 }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616 }, +] + +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 }, +] + +[[package]] +name = "wsproto" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/8d/48e227460422d3f78f52618d8ef7d7a0474c6fcdaddf7f2d1aa25854ea75/wsproto-1.3.1.tar.gz", hash = "sha256:81529992325c28f0d9b86ca66fc973da96eb80ab53410249ce2e502749c7723c", size = 50083 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/da/539c2d24b13025e54a86ce3215eb9b6297b023937a087db9ef2a436cc7b4/wsproto-1.3.1-py3-none-any.whl", hash = "sha256:297ce79322989c0d286cc158681641cd18bc7632dfb38cf4054696a89179b993", size = 24402 }, +] + +[[package]] +name = "zope-event" +version = "6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/33/d3eeac228fc14de76615612ee208be2d8a5b5b0fada36bf9b62d6b40600c/zope_event-6.1.tar.gz", hash = "sha256:6052a3e0cb8565d3d4ef1a3a7809336ac519bc4fe38398cb8d466db09adef4f0", size = 18739 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/b0/956902e5e1302f8c5d124e219c6bf214e2649f92ad5fce85b05c039a04c9/zope_event-6.1-py3-none-any.whl", hash = "sha256:0ca78b6391b694272b23ec1335c0294cc471065ed10f7f606858fc54566c25a0", size = 6414 }, +] + +[[package]] +name = "zope-interface" +version = "8.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/c9/5ec8679a04d37c797d343f650c51ad67d178f0001c363e44b6ac5f97a9da/zope_interface-8.1.1.tar.gz", hash = "sha256:51b10e6e8e238d719636a401f44f1e366146912407b58453936b781a19be19ec", size = 254748 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/3d/f5b8dd2512f33bfab4faba71f66f6873603d625212206dd36f12403ae4ca/zope_interface-8.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a16715808408db7252b8c1597ed9008bdad7bf378ed48eb9b0595fad4170e49d", size = 208660 }, + { url = "https://files.pythonhosted.org/packages/e5/41/c331adea9b11e05ff9ac4eb7d3032b24c36a3654ae9f2bf4ef2997048211/zope_interface-8.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce6b58752acc3352c4aa0b55bbeae2a941d61537e6afdad2467a624219025aae", size = 208851 }, + { url = "https://files.pythonhosted.org/packages/25/00/7a8019c3bb8b119c5f50f0a4869183a4b699ca004a7f87ce98382e6b364c/zope_interface-8.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:807778883d07177713136479de7fd566f9056a13aef63b686f0ab4807c6be259", size = 259292 }, + { url = "https://files.pythonhosted.org/packages/1a/fc/b70e963bf89345edffdd5d16b61e789fdc09365972b603e13785360fea6f/zope_interface-8.1.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50e5eb3b504a7d63dc25211b9298071d5b10a3eb754d6bf2f8ef06cb49f807ab", size = 264741 }, + { url = "https://files.pythonhosted.org/packages/96/fe/7d0b5c0692b283901b34847f2b2f50d805bfff4b31de4021ac9dfb516d2a/zope_interface-8.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eee6f93b2512ec9466cf30c37548fd3ed7bc4436ab29cd5943d7a0b561f14f0f", size = 264281 }, + { url = "https://files.pythonhosted.org/packages/2b/2c/a7cebede1cf2757be158bcb151fe533fa951038cfc5007c7597f9f86804b/zope_interface-8.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:80edee6116d569883c58ff8efcecac3b737733d646802036dc337aa839a5f06b", size = 212327 }, + { url = "https://files.pythonhosted.org/packages/85/81/3c3b5386ce4fba4612fd82ffb8a90d76bcfea33ca2b6399f21e94d38484f/zope_interface-8.1.1-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:84f9be6d959640de9da5d14ac1f6a89148b16da766e88db37ed17e936160b0b1", size = 209046 }, + { url = "https://files.pythonhosted.org/packages/4a/e3/32b7cb950c4c4326b3760a8e28e5d6f70ad15f852bfd8f9364b58634f74b/zope_interface-8.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:531fba91dcb97538f70cf4642a19d6574269460274e3f6004bba6fe684449c51", size = 209104 }, + { url = "https://files.pythonhosted.org/packages/a3/3d/c4c68e1752a5f5effa2c1f5eaa4fea4399433c9b058fb7000a34bfb1c447/zope_interface-8.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:fc65f5633d5a9583ee8d88d1f5de6b46cd42c62e47757cfe86be36fb7c8c4c9b", size = 259277 }, + { url = "https://files.pythonhosted.org/packages/fd/5b/cf4437b174af7591ee29bbad728f620cab5f47bd6e9c02f87d59f31a0dda/zope_interface-8.1.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efef80ddec4d7d99618ef71bc93b88859248075ca2e1ae1c78636654d3d55533", size = 264742 }, + { url = "https://files.pythonhosted.org/packages/0b/0e/0cf77356862852d3d3e62db9aadae5419a1a7d89bf963b219745283ab5ca/zope_interface-8.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49aad83525eca3b4747ef51117d302e891f0042b06f32aa1c7023c62642f962b", size = 264252 }, + { url = "https://files.pythonhosted.org/packages/8a/10/2af54aa88b2fa172d12364116cc40d325fedbb1877c3bb031b0da6052855/zope_interface-8.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:71cf329a21f98cb2bd9077340a589e316ac8a415cac900575a32544b3dffcb98", size = 212330 }, + { url = "https://files.pythonhosted.org/packages/b9/f5/44efbd98ba06cb937fce7a69fcd7a78c4ac7aa4e1ad2125536801376d2d0/zope_interface-8.1.1-cp314-cp314-macosx_10_9_x86_64.whl", hash = "sha256:da311e9d253991ca327601f47c4644d72359bac6950fbb22f971b24cd7850f8c", size = 209099 }, + { url = "https://files.pythonhosted.org/packages/fd/36/a19866c09c8485c36a4c6908e1dd3f8820b41c1ee333c291157cf4cf09e7/zope_interface-8.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3fb25fca0442c7fb93c4ee40b42e3e033fef2f648730c4b7ae6d43222a3e8946", size = 209240 }, + { url = "https://files.pythonhosted.org/packages/c1/28/0dbf40db772d779a4ac8d006a57ad60936d42ad4769a3d5410dcfb98f6f9/zope_interface-8.1.1-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:bac588d0742b4e35efb7c7df1dacc0397b51ed37a17d4169a38019a1cebacf0a", size = 260919 }, + { url = "https://files.pythonhosted.org/packages/72/ae/650cd4c01dd1b32c26c800b2c4d852f044552c34a56fbb74d41f569cee31/zope_interface-8.1.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d1f053d2d5e2b393e619bce1e55954885c2e63969159aa521839e719442db49", size = 264102 }, + { url = "https://files.pythonhosted.org/packages/46/f0/f534a2c34c006aa090c593cd70eaf94e259fd0786f934698d81f0534d907/zope_interface-8.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:64a1ad7f4cb17d948c6bdc525a1d60c0e567b2526feb4fa38b38f249961306b8", size = 264276 }, + { url = "https://files.pythonhosted.org/packages/5b/a8/d7e9cf03067b767e23908dbab5f6be7735d70cb4818311a248a8c4bb23cc/zope_interface-8.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:169214da1b82b7695d1a36f92d70b11166d66b6b09d03df35d150cc62ac52276", size = 212492 }, +] diff --git a/yaci-indexer/pom.xml b/yaci-indexer/pom.xml index 1ebe38d2de..55ec675525 100644 --- a/yaci-indexer/pom.xml +++ b/yaci-indexer/pom.xml @@ -10,7 +10,7 @@ yaci-indexer - 1.0.0 + 2.1.0-beta yaci-indexer yaci-indexer