From 93eed7c340fb65356c3a06ac78f74402c3a169af Mon Sep 17 00:00:00 2001 From: Manish Dait <90558243+manishdait@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:44:45 +0530 Subject: [PATCH 01/60] fix: TransactionGetReceiptQuery to raise ReceiptStatusError (#1990) Signed-off-by: Manish Dait --- CHANGELOG.md | 2 +- ...ction_get_receipt_query_validate_status.py | 88 ++++++++++++ src/hiero_sdk_python/__init__.py | 8 ++ .../topic_message_submit_transaction.py | 18 ++- .../file/file_append_transaction.py | 18 ++- .../query/transaction_get_receipt_query.py | 28 ++++ .../transaction/transaction.py | 8 +- .../transaction/transaction_response.py | 16 ++- tests/integration/transaction_e2e_test.py | 39 +++++ .../transaction_get_receipt_query_e2e_test.py | 49 ++++++- tests/unit/file_append_transaction_test.py | 134 ++++++++++++++++- tests/unit/get_receipt_query_test.py | 56 +++++++- .../topic_message_submit_transaction_test.py | 136 +++++++++++++++++- tests/unit/transaction_response_test.py | 51 +++++++ tests/unit/transaction_test.py | 62 ++++++++ 15 files changed, 692 insertions(+), 21 deletions(-) create mode 100644 examples/query/transaction_get_receipt_query_validate_status.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ca8fe607..5745217f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,10 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ## [Unreleased] ### Src +- Fix the TransactionGetReceiptQuery to raise ReceiptStatusError for the non-retryable and non success receipt status ### Examples -- edit ### Tests diff --git a/examples/query/transaction_get_receipt_query_validate_status.py b/examples/query/transaction_get_receipt_query_validate_status.py new file mode 100644 index 000000000..5291bb43a --- /dev/null +++ b/examples/query/transaction_get_receipt_query_validate_status.py @@ -0,0 +1,88 @@ +""" +Example demonstrating the validate_status feature for Transaction Receipts. + +uv run examples/query/transaction_get_receipt_query_validate_status.py +python examples/query/transaction_get_receipt_query_validate_status.py +""" +import sys + +from hiero_sdk_python import ( + AccountDeleteTransaction, + AccountId, + Client, + ResponseCode, + TransactionGetReceiptQuery, + ReceiptStatusError, +) + +def setup_client(): + """Initialize the Hiero client from environment variables.""" + try: + client = Client.from_env() + print(f"Client set up for {client.network.network}...") + return client + except ValueError as e: + print(f"Error setting up client: {e}") + sys.exit(1) + +def submit_failing_transaction(client): + """Submit a transaction designed to fail to demonstrate receipt handling.""" + tx = ( + AccountDeleteTransaction() + .set_account_id(AccountId(0, 0, 9999999)) + .set_transfer_account_id(client.operator_account_id) + .freeze_with(client) + ) + + # wait_for_receipt=False allows us to query the receipt manually later + response = tx.execute(client, wait_for_receipt=False) + + print(f"Transaction submitted: {response.transaction_id}") + + return response.transaction_id + +def run_manual_validation(client, transaction_id): + """Demonstrate manual status checking (the default behavior).""" + print("\n--- Option A: Manual Validation (Default) ---") + query = ( + TransactionGetReceiptQuery() + .set_transaction_id(transaction_id) + .set_validate_status(False) + ) + + print("Executing query with validate_status=False") + receipt = query.execute(client) + status_name = ResponseCode(receipt.status).name + print(f"Query returned receipt with status: {status_name}") + + +def run_automatic_validation(client, transaction_id): + """Demonstrate automatic validation using ReceiptStatusError.""" + print("\n--- Option B: Automatic Validation ---") + query = ( + TransactionGetReceiptQuery() + .set_transaction_id(transaction_id) + .set_validate_status(True) + ) + + try: + print("Executing query with validate_status=True") + query.execute(client) + except ReceiptStatusError as e: + status_name = ResponseCode(e.status).name + print(f"Query raises expected exception: {status_name}") + +def main(): + client = setup_client() + + # Get a transaction ID to query + tx_id = submit_failing_transaction(client) + + # Default receipt query tx + run_manual_validation(client, tx_id) + + # Receipt query with validate_status + run_automatic_validation(client, tx_id) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/hiero_sdk_python/__init__.py b/src/hiero_sdk_python/__init__.py index 10519418d..e9ffb31e2 100644 --- a/src/hiero_sdk_python/__init__.py +++ b/src/hiero_sdk_python/__init__.py @@ -150,6 +150,10 @@ from .system.freeze_transaction import FreezeTransaction from .system.freeze_type import FreezeType +# Errors +from .exceptions import ReceiptStatusError +from .exceptions import PrecheckError + __all__ = [ # Client "Client", @@ -298,4 +302,8 @@ # System "FreezeTransaction", "FreezeType", + + # Errors + "ReceiptStatusError", + "PrecheckError" ] diff --git a/src/hiero_sdk_python/consensus/topic_message_submit_transaction.py b/src/hiero_sdk_python/consensus/topic_message_submit_transaction.py index 7757c799b..48ae989e9 100644 --- a/src/hiero_sdk_python/consensus/topic_message_submit_transaction.py +++ b/src/hiero_sdk_python/consensus/topic_message_submit_transaction.py @@ -291,6 +291,7 @@ def execute( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[True] = True, + validate_status: bool = False ) -> "TransactionReceipt": ... @@ -300,6 +301,7 @@ def execute( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[False] = False, + validate_status: bool = False ) -> "TransactionResponse": ... @@ -307,7 +309,8 @@ def execute( self, client: "Client", timeout: int | float | None = None, - wait_for_receipt: bool = True + wait_for_receipt: bool = True, + validate_status: bool = False ) -> TransactionReceipt | TransactionResponse: """ Executes the topic message submit transaction. @@ -319,13 +322,14 @@ def execute( timeout (int | float | None, optional): The total execution timeout (in seconds) for this execution. wait_for_receipt (bool, optional): Whether to wait for consensus and return the receipt. If False, the method returns a TransactionResponse immediately after submission. + validate_status: (bool): Whether the query should automatically validate the transaction status (default False). Returns: TransactionReceipt: If wait_for_receipt is True (default) TransactionResponse: If wait_for_receipt is False """ # Return the first response as the JS SDK does - return self.execute_all(client, timeout, wait_for_receipt)[0] + return self.execute_all(client, timeout, wait_for_receipt, validate_status)[0] @overload def execute_all( @@ -333,6 +337,7 @@ def execute_all( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[True] = True, + validate_status: bool = False ) -> List["TransactionReceipt"]: ... @@ -342,6 +347,7 @@ def execute_all( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[False] = False, + validate_status: bool = False ) -> List["TransactionResponse"]: ... @@ -349,7 +355,8 @@ def execute_all( self, client: "Client", timeout: int | float | None = None, - wait_for_receipt: bool = True + wait_for_receipt: bool = True, + validate_status: bool = False ) -> List["TransactionReceipt"] | List["TransactionResponse"]: """ Executes the topic message submit transaction. @@ -361,6 +368,7 @@ def execute_all( timeout (int | float | None, optional): The total execution timeout (in seconds) for this execution. wait_for_receipt (bool, optional): Whether to wait for consensus and return the receipt. If False, the method returns a TransactionResponse immediately after submission. + validate_status: (bool): Whether the query should automatically validate the transaction status (default False). Returns: List[TransactionReceipt]: If wait_for_receipt is True (default) @@ -369,7 +377,7 @@ def execute_all( self._validate_chunking() if self.get_required_chunks() == 1: - return [super().execute(client, timeout, wait_for_receipt)] + return [super().execute(client, timeout, wait_for_receipt, validate_status)] # Multi-chunk transaction - execute all chunks responses = [] @@ -389,7 +397,7 @@ def execute_all( super().sign(signing_key) # Execute the chunk - response = super().execute(client, timeout, wait_for_receipt) + response = super().execute(client, timeout, wait_for_receipt, validate_status) responses.append(response) return responses diff --git a/src/hiero_sdk_python/file/file_append_transaction.py b/src/hiero_sdk_python/file/file_append_transaction.py index 48da95320..3265f9884 100644 --- a/src/hiero_sdk_python/file/file_append_transaction.py +++ b/src/hiero_sdk_python/file/file_append_transaction.py @@ -327,6 +327,7 @@ def execute( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[True] = True, + validate_status: bool = False ) -> "TransactionReceipt": ... @@ -336,6 +337,7 @@ def execute( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[False] = False, + validate_status: bool = False ) -> "TransactionResponse": ... @@ -343,7 +345,8 @@ def execute( self, client: "Client", timeout: int | float | None = None, - wait_for_receipt: bool = True + wait_for_receipt: bool = True, + validate_status: bool = False ) -> "TransactionReceipt" | "TransactionResponse": """ Executes the file append transaction. @@ -355,13 +358,14 @@ def execute( timeout (int | float | None, optional): The total execution timeout (in seconds) for this execution. wait_for_receipt (bool, optional): Whether to wait for consensus and return the receipt. If False, the method returns a TransactionResponse immediately after submission. + validate_status: (bool): Whether the query should automatically validate the transaction status (default False). Returns: TransactionReceipt: If wait_for_receipt is True (default) TransactionResponse: If wait_for_receipt is False """ # Return the first response (as per JavaScript implementation) - return self.execute_all(client, timeout, wait_for_receipt)[0] + return self.execute_all(client, timeout, wait_for_receipt, validate_status)[0] @overload def execute_all( @@ -369,6 +373,7 @@ def execute_all( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[True] = True, + validate_status: bool = False ) -> List["TransactionReceipt"]: ... @@ -378,6 +383,7 @@ def execute_all( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[False] = False, + validate_status: bool = False ) -> List["TransactionResponse"]: ... @@ -385,7 +391,8 @@ def execute_all( self, client: "Client", timeout: int | float | None = None, - wait_for_receipt: bool = True + wait_for_receipt: bool = True, + validate_status: bool = False ) -> List["TransactionReceipt"] | List["TransactionResponse"]: """ Executes the file append transaction. @@ -397,6 +404,7 @@ def execute_all( timeout (int | float | None, optional): The total execution timeout (in seconds) for this execution. wait_for_receipt (bool, optional): Whether to wait for consensus and return the receipt. If False, the method returns a TransactionResponse immediately after submission. + validate_status: (bool): Whether the query should automatically validate the transaction status (default False). Returns: List[TransactionReceipt]: If wait_for_receipt is True (default) @@ -406,7 +414,7 @@ def execute_all( if self.get_required_chunks() == 1: # Single chunk transaction - return [super().execute(client, timeout, wait_for_receipt)] + return [super().execute(client, timeout, wait_for_receipt, validate_status)] # Multi-chunk transaction - execute all chunks responses = [] @@ -430,7 +438,7 @@ def execute_all( super().sign(signing_key) # Execute the chunk - response = super().execute(client, timeout, wait_for_receipt) + response = super().execute(client, timeout, wait_for_receipt, validate_status) responses.append(response) return responses diff --git a/src/hiero_sdk_python/query/transaction_get_receipt_query.py b/src/hiero_sdk_python/query/transaction_get_receipt_query.py index 49418728d..0b8ab52a5 100644 --- a/src/hiero_sdk_python/query/transaction_get_receipt_query.py +++ b/src/hiero_sdk_python/query/transaction_get_receipt_query.py @@ -29,6 +29,7 @@ def __init__( transaction_id: Optional[TransactionId] = None, include_children: bool = False, include_duplicates: bool = False, + validate_status: bool = False ) -> None: """ Initializes a new instance of the TransactionGetReceiptQuery class. @@ -37,12 +38,14 @@ def __init__( transaction_id (TransactionId, optional): The ID of the transaction. include_children (bool): Whether to include child transaction receipts. include_duplicates (bool): Whether to include duplicate transaction receipts. + validate_status: (bool): Whether the query should automatically validate the transaction status. """ super().__init__() self.transaction_id: Optional[TransactionId] = transaction_id self._frozen: bool = False self.include_children = include_children self.include_duplicates = include_duplicates + self.validate_status = validate_status # To keep backward compatible def _require_not_frozen(self) -> None: """ @@ -108,6 +111,27 @@ def set_include_duplicates( self._require_not_frozen() self.include_duplicates = include_duplicates return self + + def set_validate_status(self, validate_status: bool) -> "TransactionGetReceiptQuery": + """ + Sets whether the query should automatically validate the transaction status. + + When set to True, the execute() method will raise a ReceiptStatusError if + the transaction receipt status is anything other than SUCCESS. + + Args: + validate_status (bool): True to enable automatic error raising on failure statuses; + False to return the receipt regardless of outcome. (default False) + + Returns: + TransactionGetReceiptQuery: The current instance for method chaining. + + Raises: + ValueError: If the query is frozen and cannot be modified. + """ + self._require_not_frozen() + self.validate_status = validate_status + return self def freeze(self) -> "TransactionGetReceiptQuery": """ @@ -211,7 +235,11 @@ def _should_retry(self, response: response_pb2.Response) -> _ExecutionState: if status in retryable_statuses or status == ResponseCode.OK: return _ExecutionState.RETRY + elif status == ResponseCode.SUCCESS: + return _ExecutionState.FINISHED else: + if self.validate_status: + return _ExecutionState.ERROR return _ExecutionState.FINISHED def _map_status_error(self, response: response_pb2.Response) -> Union[PrecheckError, ReceiptStatusError]: diff --git a/src/hiero_sdk_python/transaction/transaction.py b/src/hiero_sdk_python/transaction/transaction.py index 0fe86b183..22a721491 100644 --- a/src/hiero_sdk_python/transaction/transaction.py +++ b/src/hiero_sdk_python/transaction/transaction.py @@ -335,6 +335,7 @@ def execute( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[True] = True, + validate_status: bool = False ) -> "TransactionReceipt": ... @@ -344,6 +345,7 @@ def execute( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[False] = False, + validate_status: bool = False ) -> "TransactionResponse": ... @@ -351,7 +353,8 @@ def execute( self, client: "Client", timeout: int | float | None = None, - wait_for_receipt: bool = True + wait_for_receipt: bool = True, + validate_status: bool = False ) -> TransactionReceipt | TransactionResponse: """ Executes the transaction on the Hedera network using the provided client. @@ -363,6 +366,7 @@ def execute( timeout (int | float | None, optional): The total execution timeout (in seconds) for this execution. wait_for_receipt (bool, optional): Whether to wait for consensus and return the receipt. If False, the method returns a TransactionResponse immediately after submission. + validate_status: (bool, optional): Whether the query should automatically validate the transaction status. Returns: TransactionReceipt: If wait_for_receipt is True (default) @@ -394,7 +398,7 @@ def execute( response.transaction_id = self.transaction_id if wait_for_receipt: - return response.get_receipt(client, timeout=timeout) + return response.get_receipt(client, timeout=timeout, validate_status=validate_status) return response diff --git a/src/hiero_sdk_python/transaction/transaction_response.py b/src/hiero_sdk_python/transaction/transaction_response.py index 6ef7e7618..e52835793 100644 --- a/src/hiero_sdk_python/transaction/transaction_response.py +++ b/src/hiero_sdk_python/transaction/transaction_response.py @@ -34,10 +34,13 @@ def __init__(self) -> None: self.validate_status: bool = False self.transaction: Optional["Transaction"] = None - def get_receipt_query(self): + def get_receipt_query(self, validate_status: bool = False): """ Create a receipt query for this transaction. + Args: + validate_status (bool, Optional): The query should automatically validate the transaction status. (default False) + Returns: TransactionGetReceiptQuery: A configured receipt query. """ @@ -46,21 +49,28 @@ def get_receipt_query(self): TransactionGetReceiptQuery() .set_transaction_id(self.transaction_id) .set_node_account_id(self.node_id) + .set_validate_status(validate_status) ) - def get_receipt(self, client: "Client", timeout: Optional[Union[int, float]] = None) -> "TransactionReceipt": + def get_receipt( + self, + client: "Client", + timeout: Optional[Union[int, float]] = None, + validate_status: bool = False + ) -> "TransactionReceipt": """ Retrieves the receipt for this transaction from the network. Args: client (Client): The client instance to use for receipt retrieval. timeout (Optional[Union[int, float]]): The total execution timeout (in seconds) for this execution. + validate_status (bool, Optional): The query should automatically validate the transaction status. (default False) Returns: TransactionReceipt: The receipt from the network, containing the status and any entities created by the transaction """ - receipt = self.get_receipt_query().execute(client, timeout) + receipt = self.get_receipt_query(validate_status=validate_status).execute(client, timeout) return receipt def get_record_query(self): diff --git a/tests/integration/transaction_e2e_test.py b/tests/integration/transaction_e2e_test.py index 2aa5c2fc2..65fe9b577 100644 --- a/tests/integration/transaction_e2e_test.py +++ b/tests/integration/transaction_e2e_test.py @@ -1,10 +1,13 @@ import pytest from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction +from hiero_sdk_python.account.account_delete_transaction import AccountDeleteTransaction +from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.consensus.topic_create_transaction import TopicCreateTransaction from hiero_sdk_python.consensus.topic_delete_transaction import TopicDeleteTransaction from hiero_sdk_python.consensus.topic_message_submit_transaction import TopicMessageSubmitTransaction from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.exceptions import ReceiptStatusError from hiero_sdk_python.query.topic_info_query import TopicInfoQuery from hiero_sdk_python.query.transaction_get_receipt_query import ( TransactionGetReceiptQuery, @@ -238,3 +241,39 @@ def test_chunk_tx_returns_receipts_with_wait_for_receipt(env): TopicDeleteTransaction().set_topic_id(topic_id).execute(env.client) +@pytest.mark.integration +def test_get_receipt_returns_failed_status_by_default(env): + """Test receipt is returned normally on failure when validation is disabled.""" + tx = ( + AccountDeleteTransaction() + .set_transfer_account_id(env.operator_id) + .set_account_id(AccountId(0, 0, 0)) + ) + response = tx.execute(env.client, wait_for_receipt=False) + + assert isinstance(response, TransactionResponse) + assert response.transaction is tx + + receipt = response.get_receipt(env.client) + assert isinstance(receipt, TransactionReceipt) + assert receipt.transaction_id == tx.transaction_id + assert receipt.status == ResponseCode.INVALID_ACCOUNT_ID + + +@pytest.mark.integration +def test_get_receipt_raises_exception_on_failure_with_validation(env): + """Test error is raised for failing transactions when validation is enabled.""" + tx = ( + AccountDeleteTransaction() + .set_transfer_account_id(env.operator_id) + .set_account_id(AccountId(0, 0, 0)) + ) + response = tx.execute(env.client, wait_for_receipt=False) + + assert isinstance(response, TransactionResponse) + assert response.transaction is tx + + with pytest.raises(ReceiptStatusError) as e: + response.get_receipt(env.client, validate_status=True) + + assert e.value.status == ResponseCode.INVALID_ACCOUNT_ID diff --git a/tests/integration/transaction_get_receipt_query_e2e_test.py b/tests/integration/transaction_get_receipt_query_e2e_test.py index a80608998..6874b6bde 100644 --- a/tests/integration/transaction_get_receipt_query_e2e_test.py +++ b/tests/integration/transaction_get_receipt_query_e2e_test.py @@ -13,6 +13,9 @@ not children count > 0. """ +from hiero_sdk_python.account.account_delete_transaction import AccountDeleteTransaction +from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.exceptions import ReceiptStatusError from hiero_sdk_python.transaction.transaction_id import TransactionId import pytest import threading @@ -20,6 +23,7 @@ from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.query.transaction_get_receipt_query import TransactionGetReceiptQuery from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction from tests.integration.utils import env @@ -218,4 +222,47 @@ def test_get_receipt_query_include_duplicates_execute_e2e(env): assert queried.status == ResponseCode.SUCCESS assert isinstance(queried.duplicates, list) - assert len(queried.duplicates) == 1 \ No newline at end of file + assert len(queried.duplicates) == 1 + +@pytest.mark.integration +def test_get_receipt_returns_failed_status_receipt_if_validate_status_false(env): + """Test receipt is returned despite failure when validate_status is False.""" + tx = ( + AccountDeleteTransaction() + .set_transfer_account_id(env.operator_id) + .set_account_id(AccountId(0, 0, 0)) + ) + response = tx.execute(env.client, wait_for_receipt=False) + + query = ( + TransactionGetReceiptQuery() + .set_transaction_id(response.transaction_id) + .set_validate_status(False) # Default value + ) + + receipt = query.execute(env.client) + assert isinstance(receipt, TransactionReceipt) + assert receipt.transaction_id == tx.transaction_id + assert receipt.status == ResponseCode.INVALID_ACCOUNT_ID + + +@pytest.mark.integration +def test_get_receipt_throws_status_error_when_validation_enabled(env): + """Test error is raised for failures when validate_status is True.""" + tx = ( + AccountDeleteTransaction() + .set_transfer_account_id(env.operator_id) + .set_account_id(AccountId(1, 0, 0)) + ) + response = tx.execute(env.client, wait_for_receipt=False) + + query = ( + TransactionGetReceiptQuery() + .set_transaction_id(response.transaction_id) + .set_validate_status(True) + ) + + with pytest.raises(ReceiptStatusError) as e: + query.execute(env.client) + + assert e.value.status == ResponseCode.INVALID_ACCOUNT_ID diff --git a/tests/unit/file_append_transaction_test.py b/tests/unit/file_append_transaction_test.py index affd517da..7b6c3cf10 100644 --- a/tests/unit/file_append_transaction_test.py +++ b/tests/unit/file_append_transaction_test.py @@ -1,7 +1,7 @@ from unittest.mock import patch, MagicMock import pytest -from hiero_sdk_python.exceptions import PrecheckError +from hiero_sdk_python.exceptions import PrecheckError, ReceiptStatusError from hiero_sdk_python.file.file_append_transaction import FileAppendTransaction from hiero_sdk_python.file.file_id import FileId from hiero_sdk_python.hapi.services import response_header_pb2, response_pb2, transaction_get_receipt_pb2, transaction_receipt_pb2, transaction_response_pb2 @@ -132,7 +132,7 @@ def test_multi_chunk_execution(): # Mock client and responses mock_client = MagicMock() - mock_receipt = MagicMock() + mock_receipt = MagicMock(spec=TransactionReceipt) mock_receipt.status = ResponseCode.SUCCESS # Mock the execute method to return our mock receipt @@ -328,3 +328,133 @@ def test_file_append_tx_execute_all_throw_error_when_transaction_fails(file_id): with pytest.raises(PrecheckError, match="Transaction failed precheck"): tx.execute_all(client) + +def test_execute_raises_error_when_validation_enabled(file_id): + """Test execute raises error for failing transactions when validate_status is True.""" + ok_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ), + ) + ) + + response_sequence = [[ok_response, receipt_response]] + + with mock_hedera_servers(response_sequence) as client: + tx = ( + FileAppendTransaction() + .set_file_id(file_id) + .set_contents("Hello Hiero") + .freeze_with(client) + ) + + with pytest.raises(ReceiptStatusError) as e: + tx.execute(client, validate_status=True) + + assert e.value.status == ResponseCode.INVALID_SIGNATURE + +def test_execute_returns_failed_receipt_when_validation_disabled(file_id): + """Test execute returns the failing receipt by default when validation is disabled.""" + ok_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ), + ) + ) + + response_sequence = [[ok_response, receipt_response]] + + with mock_hedera_servers(response_sequence) as client: + tx = ( + FileAppendTransaction() + .set_file_id(file_id) + .set_contents("Hello Hiero") + .freeze_with(client) + ) + + receipt = tx.execute(client) + + assert receipt.status == ResponseCode.INVALID_SIGNATURE + +def test_file_append_execute_all_raises_error_with_validation(file_id): + """Test execute_all raises error when validate_status is True and a chunk fails.""" + content = "A" * 1024 + + tx_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_FILE_ID + ) + ) + ) + + response_sequence = [tx_response, receipt_response] * 4 + + with mock_hedera_servers([response_sequence]) as client: + tx = ( + FileAppendTransaction() + .set_file_id(file_id) + .set_contents(content) + .freeze_with(client) + ) + + with pytest.raises(ReceiptStatusError) as e: + tx.execute_all(client, validate_status=True) + + assert e.value.status == ResponseCode.INVALID_FILE_ID + +def test_file_append_execute_all_returns_receipt_without_validation(file_id): + """Test execute_all returns failing receipts normally when validation is disabled.""" + content = "A" * 1024 + + tx_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_FILE_ID + ) + ) + ) + + response_sequence = [tx_response, receipt_response] * 4 # 4 chunks + + with mock_hedera_servers([response_sequence]) as client: + tx = ( + FileAppendTransaction() + .set_file_id(file_id) + .set_contents(content) + .freeze_with(client) + ) + + receipts = tx.execute_all(client) + + assert receipts[0].status == ResponseCode.INVALID_FILE_ID diff --git a/tests/unit/get_receipt_query_test.py b/tests/unit/get_receipt_query_test.py index 79b4d3d35..66cdc0225 100644 --- a/tests/unit/get_receipt_query_test.py +++ b/tests/unit/get_receipt_query_test.py @@ -4,7 +4,7 @@ from unittest.mock import patch from hiero_sdk_python.account.account_id import AccountId -from hiero_sdk_python.exceptions import MaxAttemptsError +from hiero_sdk_python.exceptions import MaxAttemptsError, ReceiptStatusError from hiero_sdk_python.hapi.services import ( basic_types_pb2, response_header_pb2, @@ -375,3 +375,57 @@ def test_transaction_get_receipt_query_returns_empty_duplicate_receipts_when_not assert query.include_duplicates is False assert result.status == ResponseCode.SUCCESS assert len(result.duplicates) == 0 + +def test_transaction_receipt_query_should_not_raise_receipt_error(transaction_id): + """Test receipt query should not raise error if the status is non success and non retryable when validate_status is false.""" + response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ), + ) + ) + + response_sequences = [[response]] + + with mock_hedera_servers(response_sequences) as client: + query = ( + TransactionGetReceiptQuery() + .set_transaction_id(transaction_id) + .set_validate_status(False) + ) + + receipt = query.execute(client) + + assert receipt.status == ResponseCode.INVALID_SIGNATURE + + +def test_transaction_receipt_query_should_raise_receipt_error(transaction_id): + """Test receipt query should raise error if the status is non success and non retryable when validate_status is true.""" + response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ), + ) + ) + + response_sequences = [[response]] + + with mock_hedera_servers(response_sequences) as client: + query = ( + TransactionGetReceiptQuery() + .set_transaction_id(transaction_id) + .set_validate_status(True) + ) + + with pytest.raises(ReceiptStatusError) as e: + query.execute(client) + + assert e.value.status == ResponseCode.INVALID_SIGNATURE \ No newline at end of file diff --git a/tests/unit/topic_message_submit_transaction_test.py b/tests/unit/topic_message_submit_transaction_test.py index 2d206d3a3..bb2f75de7 100644 --- a/tests/unit/topic_message_submit_transaction_test.py +++ b/tests/unit/topic_message_submit_transaction_test.py @@ -5,7 +5,7 @@ import pytest from hiero_sdk_python.consensus.topic_message_submit_transaction import TopicMessageSubmitTransaction -from hiero_sdk_python.exceptions import PrecheckError +from hiero_sdk_python.exceptions import PrecheckError, ReceiptStatusError from hiero_sdk_python.hapi.services import ( response_header_pb2, response_pb2, @@ -504,3 +504,137 @@ def test_topic_message_submit_execute_all_throw_error_when_transaction_fails(top with pytest.raises(PrecheckError, match="Transaction failed precheck"): tx.execute_all(client) + +def test_topic_submit_execute_all_raises_error_with_validation(topic_id): + """Test execute_all raises error when validate_status is True and a chunk fails.""" + message = "Hello Hiero" + + tx_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ) + ) + ) + + response_sequence = [tx_response, receipt_response] + + with mock_hedera_servers([response_sequence]) as client: + tx = ( + TopicMessageSubmitTransaction() + .set_topic_id(topic_id) + .set_message(message) + .freeze_with(client) + ) + + with pytest.raises(ReceiptStatusError) as e: + tx.execute_all(client, validate_status=True) + + assert e.value.status == ResponseCode.INVALID_SIGNATURE + +def test_topic_submit_execute_all_returns_failed_receipt_by_default(topic_id): + """Test execute_all returns failing receipts normally when validation is disabled.""" + message = "A" * 1024 + + tx_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ) + ) + ) + + response_sequence = [tx_response, receipt_response] * 4 # 4 chunks + + with mock_hedera_servers([response_sequence]) as client: + tx = ( + TopicMessageSubmitTransaction() + .set_topic_id(topic_id) + .set_message(message) + .freeze_with(client) + ) + + receipt = tx.execute_all(client) + + assert receipt[0].status == ResponseCode.INVALID_SIGNATURE + +def test_topic_submit_execute_raises_error_with_validation(topic_id): + """Test execute raises error for failing messages when validate_status is True.""" + message = "A" * 1024 + + tx_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ) + ) + ) + + response_sequence = [tx_response, receipt_response] * 4 + + with mock_hedera_servers([response_sequence]) as client: + tx = ( + TopicMessageSubmitTransaction() + .set_topic_id(topic_id) + .set_message(message) + .freeze_with(client) + ) + + with pytest.raises(ReceiptStatusError) as e: + tx.execute(client, validate_status=True) + + assert e.value.status == ResponseCode.INVALID_SIGNATURE + +def test_topic_submit_execute_returns_failed_receipt_by_default(topic_id): + """Test execute returns the failing receipt by default when validation is disabled.""" + message = "Hello Hiero" + + tx_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ) + ) + ) + + response_sequence = [tx_response, receipt_response] + + with mock_hedera_servers([response_sequence]) as client: + tx = ( + TopicMessageSubmitTransaction() + .set_topic_id(topic_id) + .set_message(message) + .freeze_with(client) + ) + + receipt = tx.execute(client) + + assert receipt.status == ResponseCode.INVALID_SIGNATURE diff --git a/tests/unit/transaction_response_test.py b/tests/unit/transaction_response_test.py index 7ebff9189..5a594b1d1 100644 --- a/tests/unit/transaction_response_test.py +++ b/tests/unit/transaction_response_test.py @@ -1,6 +1,7 @@ import pytest from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.exceptions import ReceiptStatusError from hiero_sdk_python.hapi.services import ( basic_types_pb2, response_header_pb2, @@ -71,6 +72,56 @@ def test_get_receipt_executes_and_returns_receipt(transaction_response): assert receipt.account_id.num == 1234 +def test_get_receipt_query_set_validate_status(transaction_response): + """Test receipt query correctly initializes with the validate_status flag.""" + query = transaction_response.get_receipt_query(validate_status=True) + + assert isinstance(query, TransactionGetReceiptQuery) + assert query.validate_status == True + assert query.transaction_id == transaction_response.transaction_id + assert len(query.node_account_ids) == 1 + assert query.node_account_ids[0] == transaction_response.node_id + + +def test_get_receipt_returns_failure_status_without_validate_status(transaction_response): + """Test failing status returns a receipt instead of raising an error when validation is disabled.""" + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ), + ) + ) + + with mock_hedera_servers([[receipt_response]]) as client: + receipt = transaction_response.get_receipt(client) + + assert isinstance(receipt, TransactionReceipt) + assert receipt.status == ResponseCode.INVALID_SIGNATURE + +def test_get_receipt_raises_exception_with_validate_status(transaction_response): + """Test get_receipt error is raised for non-success statuses when validation is enabled.""" + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ), + ) + ) + + with mock_hedera_servers([[receipt_response]]) as client: + with pytest.raises(ReceiptStatusError) as e: + transaction_response.get_receipt(client, validate_status=True) + + assert e.value.status == ResponseCode.INVALID_SIGNATURE + + def test_get_record_query_builds_query(transaction_response): """Test get_record_query builds and returns the transaction record query.""" query = transaction_response.get_record_query() diff --git a/tests/unit/transaction_test.py b/tests/unit/transaction_test.py index 3b1a3eff9..d3504e144 100644 --- a/tests/unit/transaction_test.py +++ b/tests/unit/transaction_test.py @@ -2,6 +2,7 @@ from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.exceptions import ReceiptStatusError from hiero_sdk_python.hapi.services import ( basic_types_pb2, response_header_pb2, @@ -90,3 +91,64 @@ def test_execute_without_wait_returns_transaction_response(): assert response.transaction is tx assert response.node_id == tx.node_account_id assert response.validate_status is True + +def test_execute_raises_error_when_validation_enabled_and_transaction_fails(): + """Test execute raises error for failing transactions when validate_status is True.""" + ok_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ), + ) + ) + + response_sequence = [[ok_response, receipt_response]] + + with mock_hedera_servers(response_sequence) as client: + tx = ( + AccountCreateTransaction() + .set_initial_balance(1) + .set_key_without_alias(PrivateKey.generate()) + ) + + with pytest.raises(ReceiptStatusError) as e: + tx.execute(client, validate_status=True) + + assert e.value.status == ResponseCode.INVALID_SIGNATURE + +def test_execute_returns_receipt_without_error_when_validation_disabled(): + """Test execute returns a receipt normally on failure when validate_status is False.""" + ok_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ), + ) + ) + + response_sequence = [[ok_response, receipt_response]] + + with mock_hedera_servers(response_sequence) as client: + tx = ( + AccountCreateTransaction() + .set_initial_balance(1) + .set_key_without_alias(PrivateKey.generate()) + ) + + receipt = tx.execute(client) + + assert receipt.status == ResponseCode.INVALID_SIGNATURE From 51c3d9da837bac978e94aa07ab925c8ad678af96 Mon Sep 17 00:00:00 2001 From: Mounil Kankhara Date: Wed, 18 Mar 2026 14:59:28 +0530 Subject: [PATCH 02/60] chore: wrap staking fields in StakingInfo wrapper class (#1914) Signed-off-by: Mounil Signed-off-by: Mounil Kanakhara --- CHANGELOG.md | 2 +- src/hiero_sdk_python/account/account_info.py | 212 ++++++++++++++---- .../account_create_transaction_e2e_test.py | 7 +- .../account_update_transaction_e2e_test.py | 11 +- tests/unit/account_info_test.py | 184 +++++++++++++++ 5 files changed, 363 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5745217f7..932a41260 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ### Src - Fix the TransactionGetReceiptQuery to raise ReceiptStatusError for the non-retryable and non success receipt status +- Refactor `AccountInfo` to use the existing `StakingInfo` wrapper class instead of flattened staking fields. Access is now via `info.staking_info.staked_account_id`, `info.staking_info.staked_node_id`, and `info.staking_info.decline_reward`. The old flat accessors (`info.staked_account_id`, `info.staked_node_id`, `info.decline_staking_reward`) are still available as deprecated properties and will emit a `DeprecationWarning`. (#1366) ### Examples @@ -37,7 +38,6 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Updated `generated_proto.py` file to work with new proto version - fix: Ensure UTF-8 encoding when reading and writing proto files in `generate_proto.py` to prevent encoding issues on Windows (`#1963`) - ### Examples - Updated the `examples/consensus/topic_create_transaction_revenue_generating.py` example to use `Client.from_env()` for simpler client setup. (#1964) diff --git a/src/hiero_sdk_python/account/account_info.py b/src/hiero_sdk_python/account/account_info.py index e6844a9f8..b97db4998 100644 --- a/src/hiero_sdk_python/account/account_info.py +++ b/src/hiero_sdk_python/account/account_info.py @@ -3,13 +3,14 @@ AccountInfo class. """ +import warnings from dataclasses import dataclass, field from typing import Optional from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.staking_info import StakingInfo from hiero_sdk_python.crypto.public_key import PublicKey from hiero_sdk_python.Duration import Duration -from hiero_sdk_python.hapi.services.basic_types_pb2 import StakingInfo from hiero_sdk_python.hapi.services.crypto_get_info_pb2 import CryptoGetInfoResponse from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.timestamp import Timestamp @@ -38,9 +39,7 @@ class AccountInfo: associated with this account. account_memo (Optional[str]): The memo associated with this account. owned_nfts (Optional[int]): The number of NFTs owned by this account. - staked_account_id (Optional[AccountId]): The account to which this account is staked. - staked_node_id (Optional[int]): The node to which this account is staked. - decline_staking_reward (bool): Whether this account declines receiving staking rewards. + staking_info (Optional[StakingInfo]): The staking information for this account. """ account_id: Optional[AccountId] = None @@ -56,9 +55,7 @@ class AccountInfo: account_memo: Optional[str] = None owned_nfts: Optional[int] = None max_automatic_token_associations: Optional[int] = None - staked_account_id: Optional[AccountId] = None - staked_node_id: Optional[int] = None - decline_staking_reward: Optional[bool] = None + staking_info: Optional[StakingInfo] = None @classmethod def _from_proto(cls, proto: CryptoGetInfoResponse.AccountInfo) -> "AccountInfo": @@ -100,21 +97,12 @@ def _from_proto(cls, proto: CryptoGetInfoResponse.AccountInfo) -> "AccountInfo": account_memo=proto.memo, owned_nfts=proto.ownedNfts, max_automatic_token_associations=proto.max_automatic_token_associations, + staking_info=( + StakingInfo._from_proto(proto.staking_info) + if proto.HasField('staking_info') else None + ), ) - staking_info = proto.staking_info if proto.HasField('staking_info') else None - - if staking_info: - account_info.staked_account_id = ( - AccountId._from_proto(staking_info.staked_account_id) - if staking_info.HasField('staked_account_id') else None - ) - account_info.staked_node_id = ( - staking_info.staked_node_id - if staking_info.HasField('staked_node_id') else None - ) - account_info.decline_staking_reward = staking_info.decline_reward - return account_info def _to_proto(self) -> CryptoGetInfoResponse.AccountInfo: @@ -131,7 +119,7 @@ def _to_proto(self) -> CryptoGetInfoResponse.AccountInfo: CryptoGetInfoResponse.AccountInfo: The protobuf message representation of this `AccountInfo` object. """ - return CryptoGetInfoResponse.AccountInfo( + proto = CryptoGetInfoResponse.AccountInfo( accountID=self.account_id._to_proto() if self.account_id else None, contractAccountID=self.contract_account_id, deleted=self.is_deleted, @@ -147,12 +135,10 @@ def _to_proto(self) -> CryptoGetInfoResponse.AccountInfo: memo=self.account_memo, ownedNfts=self.owned_nfts, max_automatic_token_associations=self.max_automatic_token_associations, - staking_info=StakingInfo( - staked_account_id=self.staked_account_id._to_proto() if self.staked_account_id else None, - staked_node_id=self.staked_node_id if self.staked_node_id else None, - decline_reward=self.decline_staking_reward - ), ) + if self.staking_info is not None: + proto.staking_info.CopyFrom(self.staking_info._to_proto()) + return proto def __str__(self) -> str: """Returns a user-friendly string representation of the AccountInfo.""" @@ -166,8 +152,7 @@ def __str__(self) -> str: (self.account_memo, "Memo"), (self.owned_nfts, "Owned NFTs"), (self.max_automatic_token_associations, "Max Automatic Token Associations"), - (self.staked_account_id, "Staked Account ID"), - (self.staked_node_id, "Staked Node ID"), + (self.staking_info, "Staking Info"), (self.proxy_received, "Proxy Received"), (self.expiration_time, "Expiration Time"), (self.auto_renew_period, "Auto Renew Period"), @@ -182,9 +167,6 @@ def __str__(self) -> str: if self.receiver_signature_required is not None: lines.append(f"Receiver Signature Required: {self.receiver_signature_required}") - - if self.decline_staking_reward is not None: - lines.append(f"Decline Staking Reward: {self.decline_staking_reward}") if self.token_relationships: lines.append(f"Token Relationships: {len(self.token_relationships)}") @@ -193,16 +175,158 @@ def __str__(self) -> str: def __repr__(self) -> str: """Returns a string representation of the AccountInfo object for debugging.""" - return ( - f"AccountInfo(" - f"account_id={self.account_id!r}, " - f"contract_account_id={self.contract_account_id!r}, " - f"is_deleted={self.is_deleted!r}, " - f"balance={self.balance!r}, " - f"receiver_signature_required={self.receiver_signature_required!r}, " - f"owned_nfts={self.owned_nfts!r}, " - f"account_memo={self.account_memo!r}, " - f"staked_node_id={self.staked_node_id!r}, " - f"staked_account_id={self.staked_account_id!r}" - f")" - ) \ No newline at end of file + parts = [ + f"account_id={self.account_id!r}", + f"contract_account_id={self.contract_account_id!r}", + f"is_deleted={self.is_deleted!r}", + f"balance={self.balance!r}", + f"receiver_signature_required={self.receiver_signature_required!r}", + f"owned_nfts={self.owned_nfts!r}", + f"account_memo={self.account_memo!r}", + ] + if self.staking_info is not None: + parts.append(f"staking_info={self.staking_info!r}") + return f"AccountInfo({', '.join(parts)})" + + @property + def staked_account_id(self): + """Deprecated: use staking_info.staked_account_id instead.""" + warnings.warn( + "AccountInfo.staked_account_id is deprecated, " + "use AccountInfo.staking_info.staked_account_id instead", + DeprecationWarning, + stacklevel=2, + ) + return self.staking_info.staked_account_id if self.staking_info else None + + @staked_account_id.setter + def staked_account_id(self, value): + """Deprecated setter: use staking_info.staked_account_id instead.""" + warnings.warn( + "AccountInfo.staked_account_id setter is deprecated, " + "use AccountInfo.staking_info instead", + DeprecationWarning, + stacklevel=2, + ) + if self.staking_info is None: + object.__setattr__(self, 'staking_info', StakingInfo(staked_account_id=value)) + else: + # Reconstruct StakingInfo with updated field, clearing staked_node_id oneof conflict + object.__setattr__(self, 'staking_info', StakingInfo( + pending_reward=self.staking_info.pending_reward, + staked_to_me=self.staking_info.staked_to_me, + stake_period_start=self.staking_info.stake_period_start, + staked_account_id=value, + staked_node_id=None, # Clear oneof conflict + decline_reward=self.staking_info.decline_reward, + )) + + @property + def staked_node_id(self): + """Deprecated: use staking_info.staked_node_id instead.""" + warnings.warn( + "AccountInfo.staked_node_id is deprecated, " + "use AccountInfo.staking_info.staked_node_id instead", + DeprecationWarning, + stacklevel=2, + ) + return self.staking_info.staked_node_id if self.staking_info else None + + @staked_node_id.setter + def staked_node_id(self, value): + """Deprecated setter: use staking_info.staked_node_id instead.""" + warnings.warn( + "AccountInfo.staked_node_id setter is deprecated, " + "use AccountInfo.staking_info instead", + DeprecationWarning, + stacklevel=2, + ) + if self.staking_info is None: + object.__setattr__(self, 'staking_info', StakingInfo(staked_node_id=value)) + else: + # Reconstruct StakingInfo with updated field, clearing staked_account_id oneof conflict + object.__setattr__(self, 'staking_info', StakingInfo( + pending_reward=self.staking_info.pending_reward, + staked_to_me=self.staking_info.staked_to_me, + stake_period_start=self.staking_info.stake_period_start, + staked_account_id=None, # Clear oneof conflict + staked_node_id=value, + decline_reward=self.staking_info.decline_reward, + )) + + @property + def decline_staking_reward(self): + """Deprecated: use staking_info.decline_reward instead.""" + warnings.warn( + "AccountInfo.decline_staking_reward is deprecated, " + "use AccountInfo.staking_info.decline_reward instead", + DeprecationWarning, + stacklevel=2, + ) + return self.staking_info.decline_reward if self.staking_info else None + + @decline_staking_reward.setter + def decline_staking_reward(self, value): + """Deprecated setter: use staking_info.decline_reward instead.""" + warnings.warn( + "AccountInfo.decline_staking_reward setter is deprecated, " + "use AccountInfo.staking_info instead", + DeprecationWarning, + stacklevel=2, + ) + if self.staking_info is None: + object.__setattr__(self, 'staking_info', StakingInfo(decline_reward=value)) + else: + # Reconstruct StakingInfo with updated field + object.__setattr__(self, 'staking_info', StakingInfo( + pending_reward=self.staking_info.pending_reward, + staked_to_me=self.staking_info.staked_to_me, + stake_period_start=self.staking_info.stake_period_start, + staked_account_id=self.staking_info.staked_account_id, + staked_node_id=self.staking_info.staked_node_id, + decline_reward=value, + )) + + +# --------------------------------------------------------------------------- +# Backwards-compatible constructor: accept legacy staking kwargs +# --------------------------------------------------------------------------- +_ACCOUNT_INFO_INIT_SENTINEL = object() +_orig_account_info_init = AccountInfo.__init__ + + +def _wrapped_account_info_init( + self, + *args, + staked_account_id=_ACCOUNT_INFO_INIT_SENTINEL, + staked_node_id=_ACCOUNT_INFO_INIT_SENTINEL, + decline_staking_reward=_ACCOUNT_INFO_INIT_SENTINEL, + **kwargs, +): + _orig_account_info_init(self, *args, **kwargs) + _legacy = [ + k for k, v in [ + ("staked_account_id", staked_account_id), + ("staked_node_id", staked_node_id), + ("decline_staking_reward", decline_staking_reward), + ] + if v is not _ACCOUNT_INFO_INIT_SENTINEL + ] + if _legacy: + warnings.warn( + f"Passing {', '.join(_legacy)} to AccountInfo() is deprecated; " + "use staking_info=StakingInfo(...) instead.", + DeprecationWarning, + stacklevel=2, + ) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + if staked_account_id is not _ACCOUNT_INFO_INIT_SENTINEL: + self.staked_account_id = staked_account_id + if staked_node_id is not _ACCOUNT_INFO_INIT_SENTINEL: + self.staked_node_id = staked_node_id + if decline_staking_reward is not _ACCOUNT_INFO_INIT_SENTINEL: + self.decline_staking_reward = decline_staking_reward + + +AccountInfo.__init__ = _wrapped_account_info_init \ No newline at end of file diff --git a/tests/integration/account_create_transaction_e2e_test.py b/tests/integration/account_create_transaction_e2e_test.py index 3e4239c88..6b476f1ad 100644 --- a/tests/integration/account_create_transaction_e2e_test.py +++ b/tests/integration/account_create_transaction_e2e_test.py @@ -214,7 +214,7 @@ def test_create_account_with_staked_account_id(env): info = AccountInfoQuery(account_id=account_id).execute(env.client) assert info.account_id == account_id - assert info.staked_account_id == env.operator_id + assert info.staking_info.staked_account_id == env.operator_id def test_create_account_with_staked_node_id(env): @@ -265,8 +265,9 @@ def test_create_account_with_decline_reward(env): info = AccountInfoQuery(account_id=account_id).execute(env.client) assert info.account_id == account_id - assert info.staked_account_id == env.operator_id - assert info.decline_staking_reward is True + assert info.staking_info is not None, "Staking info should be set" + assert info.staking_info.staked_account_id == env.operator_id + assert info.staking_info.decline_reward is True def test_integration_account_create_transaction_can_execute_with_private_key(env): """Test AccountCreateTransaction can be executed when key is a PrivateKey.""" diff --git a/tests/integration/account_update_transaction_e2e_test.py b/tests/integration/account_update_transaction_e2e_test.py index 06aac3c5a..bfb46d810 100644 --- a/tests/integration/account_update_transaction_e2e_test.py +++ b/tests/integration/account_update_transaction_e2e_test.py @@ -369,9 +369,9 @@ def test_integration_account_update_transaction_with_staking_fields(env): # Verify staking info reflects the updated values info = AccountInfoQuery(account_id).execute(env.client) - assert info.staked_account_id == staked_account_id, "Staked account ID should match" - assert info.staked_node_id is None, "Staked node ID should be cleared when staking to an account" - assert info.decline_staking_reward is True, "Decline staking reward should be true" + assert info.staking_info.staked_account_id == staked_account_id, "Staked account ID should match" + assert info.staking_info.staked_node_id is None, "Staked node ID should be cleared when staking to an account" + assert info.staking_info.decline_reward is True, "Decline staking reward should be true" @pytest.mark.integration @@ -403,5 +403,6 @@ def test_integration_account_update_transaction_with_staked_node_id(env): if receipt.status == ResponseCode.SUCCESS: info = AccountInfoQuery(account_id).execute(env.client) - assert info.staked_node_id == 0 - assert info.staked_account_id is None \ No newline at end of file + assert info.staking_info is not None, "Staking info should be set" + assert info.staking_info.staked_node_id == 0 + assert info.staking_info.staked_account_id is None \ No newline at end of file diff --git a/tests/unit/account_info_test.py b/tests/unit/account_info_test.py index 4687731cc..5901b529c 100644 --- a/tests/unit/account_info_test.py +++ b/tests/unit/account_info_test.py @@ -2,6 +2,7 @@ from hiero_sdk_python.account.account_info import AccountInfo from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.staking_info import StakingInfo from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.Duration import Duration @@ -9,6 +10,8 @@ from hiero_sdk_python.tokens.token_relationship import TokenRelationship from hiero_sdk_python.tokens.token_id import TokenId from hiero_sdk_python.hapi.services.crypto_get_info_pb2 import CryptoGetInfoResponse +from hiero_sdk_python.hapi.services.basic_types_pb2 import StakingInfo as StakingInfoProto +from hiero_sdk_python.hapi.services.timestamp_pb2 import Timestamp as TimestampProto pytestmark = pytest.mark.unit @@ -34,6 +37,13 @@ def account_info(): @pytest.fixture def proto_account_info(): public_key = PrivateKey.generate_ed25519().public_key() + staking_info_proto = StakingInfoProto( + decline_reward=True, + pending_reward=500, + staked_to_me=1000, + ) + staking_info_proto.staked_account_id.CopyFrom(AccountId(0, 0, 200)._to_proto()) + staking_info_proto.stake_period_start.CopyFrom(TimestampProto(seconds=1625097600, nanos=0)) proto = CryptoGetInfoResponse.AccountInfo( accountID=AccountId(0, 0, 100)._to_proto(), contractAccountID="0.0.100", @@ -48,6 +58,7 @@ def proto_account_info(): memo="Test account memo", ownedNfts=5, ) + proto.staking_info.CopyFrom(staking_info_proto) return proto @@ -100,6 +111,12 @@ def test_from_proto(proto_account_info): assert account_info.token_relationships == [] assert account_info.account_memo == "Test account memo" assert account_info.owned_nfts == 5 + assert account_info.staking_info is not None + assert account_info.staking_info.decline_reward is True + assert account_info.staking_info.pending_reward.to_tinybars() == 500 + assert account_info.staking_info.staked_to_me.to_tinybars() == 1000 + assert account_info.staking_info.staked_account_id == AccountId(0, 0, 200) + assert account_info.staking_info.stake_period_start == Timestamp(1625097600, 0) def test_from_proto_with_token_relationships(): @@ -127,6 +144,13 @@ def test_from_proto_none_raises_error(): def test_to_proto(account_info): """Test the to_proto method of the AccountInfo class""" + account_info.staking_info = StakingInfo( + pending_reward=Hbar.from_tinybars(500), + staked_to_me=Hbar.from_tinybars(1000), + stake_period_start=Timestamp(1625097600, 0), + staked_account_id=AccountId(0, 0, 200), + decline_reward=True, + ) proto = account_info._to_proto() assert proto.accountID == AccountId(0, 0, 100)._to_proto() @@ -141,6 +165,12 @@ def test_to_proto(account_info): assert proto.tokenRelationships == [] assert proto.memo == "Test account memo" assert proto.ownedNfts == 5 + assert proto.HasField('staking_info') + assert proto.staking_info.decline_reward is True + assert proto.staking_info.pending_reward == 500 + assert proto.staking_info.staked_to_me == 1000 + assert proto.staking_info.HasField('staked_account_id') + assert proto.staking_info.staked_account_id == AccountId(0, 0, 200)._to_proto() def test_to_proto_with_none_values(): @@ -192,6 +222,32 @@ def test_proto_conversion(account_info): ) assert converted_account_info.account_memo == account_info.account_memo assert converted_account_info.owned_nfts == account_info.owned_nfts + assert converted_account_info.staking_info == account_info.staking_info + + +def test_proto_conversion_with_staking_info(): + """Test converting AccountInfo with staking_info to proto and back preserves data""" + original = AccountInfo( + account_id=AccountId(0, 0, 100), + balance=Hbar.from_tinybars(5000000), + key=PrivateKey.generate_ed25519().public_key(), + staking_info=StakingInfo( + pending_reward=Hbar.from_tinybars(300), + staked_to_me=Hbar.from_tinybars(700), + stake_period_start=Timestamp(1625097600, 0), + staked_account_id=AccountId(0, 0, 200), + decline_reward=True, + ), + ) + proto = original._to_proto() + restored = AccountInfo._from_proto(proto) + + assert restored.staking_info is not None + assert restored.staking_info.decline_reward is True + assert restored.staking_info.pending_reward.to_tinybars() == 300 + assert restored.staking_info.staked_to_me.to_tinybars() == 700 + assert restored.staking_info.staked_account_id == AccountId(0, 0, 200) + assert restored.staking_info.stake_period_start == Timestamp(1625097600, 0) def test_str_and_repr(account_info): @@ -211,3 +267,131 @@ def test_str_and_repr(account_info): assert "contract_account_id='0.0.100'" in info_repr assert "account_memo='Test account memo'" in info_repr + +# --------------------------------------------------------------------------- +# Deprecated flat property tests on AccountInfo +# --------------------------------------------------------------------------- + +class TestDeprecatedProperties: + """Ensure the deprecated flat staking properties still work with warnings.""" + + def _make_info(self): + si = StakingInfo( + staked_account_id=AccountId(0, 0, 10), + decline_reward=True, + ) + return AccountInfo(staking_info=si) + + def test_staked_account_id_deprecated(self): + import warnings + info = self._make_info() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + val = info.staked_account_id + assert val == AccountId(0, 0, 10) + assert len(w) == 1 + assert issubclass(w[0].category, DeprecationWarning) + assert "staked_account_id" in str(w[0].message) + + def test_staked_node_id_deprecated(self): + import warnings + info = self._make_info() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + val = info.staked_node_id + assert val is None + assert issubclass(w[0].category, DeprecationWarning) + + def test_decline_staking_reward_deprecated(self): + import warnings + info = self._make_info() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + val = info.decline_staking_reward + assert val is True + assert issubclass(w[0].category, DeprecationWarning) + + def test_deprecated_properties_return_none_without_staking_info(self): + import warnings + info = AccountInfo() + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + assert info.staked_account_id is None + assert info.staked_node_id is None + assert info.decline_staking_reward is None + + def test_staked_account_id_setter(self): + import warnings + info = AccountInfo() + new_id = AccountId(0, 0, 50) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + info.staked_account_id = new_id + assert info.staking_info is not None + assert info.staking_info.staked_account_id == new_id + assert any(issubclass(x.category, DeprecationWarning) for x in w) + + def test_staked_node_id_setter(self): + import warnings + info = AccountInfo() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + info.staked_node_id = 42 + assert info.staking_info is not None + assert info.staking_info.staked_node_id == 42 + assert any(issubclass(x.category, DeprecationWarning) for x in w) + + def test_decline_staking_reward_setter(self): + import warnings + info = AccountInfo() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + info.decline_staking_reward = True + assert info.staking_info is not None + assert info.staking_info.decline_reward is True + assert any(issubclass(x.category, DeprecationWarning) for x in w) + + def test_setters_preserve_other_staking_fields(self): + import warnings + si = StakingInfo( + pending_reward=Hbar.from_tinybars(100), + staked_to_me=Hbar.from_tinybars(200), + staked_account_id=AccountId(0, 0, 10), + decline_reward=True, + ) + info = AccountInfo(staking_info=si) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + # Setting staked_node_id clears staked_account_id due to oneof constraint + info.staked_node_id = 5 + # Other non-oneof fields should be preserved + assert info.staking_info.pending_reward.to_tinybars() == 100 + assert info.staking_info.staked_to_me.to_tinybars() == 200 + assert info.staking_info.decline_reward is True + # Conflicting field cleared, new value set + assert info.staking_info.staked_account_id is None + assert info.staking_info.staked_node_id == 5 + + def test_constructor_legacy_kwargs_deprecated(self): + """Legacy staking kwargs in AccountInfo() emit DeprecationWarning but still work.""" + import warnings + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + info = AccountInfo(staked_account_id=AccountId(0, 0, 1)) + assert info.staking_info is not None + assert info.staking_info.staked_account_id == AccountId(0, 0, 1) + assert any(issubclass(x.category, DeprecationWarning) for x in w) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + info = AccountInfo(staked_node_id=7) + assert info.staking_info.staked_node_id == 7 + assert any(issubclass(x.category, DeprecationWarning) for x in w) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + info = AccountInfo(decline_staking_reward=True) + assert info.staking_info.decline_reward is True + assert any(issubclass(x.category, DeprecationWarning) for x in w) + From 7de4f349d5e1741842fb4560ee3b56cec9b962c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 10:06:12 +0000 Subject: [PATCH 03/60] chore(deps): bump codecov/codecov-action from 5.5.2 to 5.5.3 (#2003) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr-check-codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-check-codecov.yml b/.github/workflows/pr-check-codecov.yml index 767a1ee5d..3d262b3e7 100644 --- a/.github/workflows/pr-check-codecov.yml +++ b/.github/workflows/pr-check-codecov.yml @@ -47,7 +47,7 @@ jobs: - name: Upload coverage to Codecov if: github.repository_owner == 'hiero-ledger' - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml From 15ac433992818a08b3c10869fb79b7f77a34f6a0 Mon Sep 17 00:00:00 2001 From: Manish Dait <90558243+manishdait@users.noreply.github.com> Date: Fri, 20 Mar 2026 00:19:38 +0530 Subject: [PATCH 04/60] feat(tck): Impl Tck endpoint for createAccount (#1978) Signed-off-by: Manish Dait --- .codacy.yml | 3 + .github/workflows/tck-test.yml | 52 ++ CHANGELOG.md | 4 +- codecov.yml | 3 + .../account/account_create_transaction.py | 37 +- src/hiero_sdk_python/contract/contract_id.py | 12 +- .../contract/delegate_contract_id.py | 81 +++ src/hiero_sdk_python/crypto/evm_address.py | 9 +- src/hiero_sdk_python/crypto/key.py | 117 ++++ src/hiero_sdk_python/crypto/key_list.py | 157 +++++ src/hiero_sdk_python/crypto/private_key.py | 14 +- src/hiero_sdk_python/crypto/public_key.py | 15 +- src/hiero_sdk_python/hbar.py | 8 +- tck/__init__.py | 2 + tck/__main__.py | 5 +- tck/errors.py | 92 ++- tck/handlers/__init__.py | 18 +- tck/handlers/account.py | 68 ++ tck/handlers/key.py | 142 ++++ tck/handlers/registry.py | 70 +- tck/handlers/sdk.py | 155 ++--- tck/param/account.py | 40 ++ tck/param/base.py | 18 + tck/param/common.py | 56 ++ tck/param/key.py | 25 + tck/param/sdk.py | 24 + tck/protocol.py | 51 +- tck/response/account.py | 7 + tck/response/key.py | 7 + tck/response/sdk.py | 11 + tck/server.py | 54 +- .../client_utils.py} | 24 +- tck/util/constants.py | 1 + tck/util/key_utils.py | 46 ++ tck/util/param_utils.py | 36 ++ .../account_create_transaction_e2e_test.py | 16 + tests/{unit => }/tck/__init__.py | 0 .../client_utils_test.py} | 6 +- tests/tck/common_params_test.py | 70 ++ tests/{unit => }/tck/conftest.py | 0 tests/tck/handlers_test.py | 286 ++++++++ tests/{unit => }/tck/protocol_test.py | 0 tests/unit/account_create_transaction_test.py | 29 + tests/unit/contract_id_test.py | 12 + tests/unit/delegate_contract_id.py | 609 ++++++++++++++++++ tests/unit/evm_address_test.py | 11 + tests/unit/key_list_test.py | 213 ++++++ tests/unit/key_test.py | 186 ++++++ tests/unit/keys_private_test.py | 36 ++ tests/unit/keys_public_test.py | 39 +- tests/unit/tck/handlers_test.py | 359 ----------- 51 files changed, 2722 insertions(+), 614 deletions(-) create mode 100644 .github/workflows/tck-test.yml create mode 100644 src/hiero_sdk_python/contract/delegate_contract_id.py create mode 100644 src/hiero_sdk_python/crypto/key.py create mode 100644 src/hiero_sdk_python/crypto/key_list.py create mode 100644 tck/handlers/account.py create mode 100644 tck/handlers/key.py create mode 100644 tck/param/account.py create mode 100644 tck/param/base.py create mode 100644 tck/param/common.py create mode 100644 tck/param/key.py create mode 100644 tck/param/sdk.py create mode 100644 tck/response/account.py create mode 100644 tck/response/key.py create mode 100644 tck/response/sdk.py rename tck/{client_manager.py => util/client_utils.py} (55%) create mode 100644 tck/util/constants.py create mode 100644 tck/util/key_utils.py create mode 100644 tck/util/param_utils.py rename tests/{unit => }/tck/__init__.py (100%) rename tests/{unit/tck/client_manager_test.py => tck/client_utils_test.py} (97%) create mode 100644 tests/tck/common_params_test.py rename tests/{unit => }/tck/conftest.py (100%) create mode 100644 tests/tck/handlers_test.py rename tests/{unit => }/tck/protocol_test.py (100%) create mode 100644 tests/unit/delegate_contract_id.py create mode 100644 tests/unit/key_list_test.py create mode 100644 tests/unit/key_test.py delete mode 100644 tests/unit/tck/handlers_test.py diff --git a/.codacy.yml b/.codacy.yml index da9235fdf..d83fecdd1 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -1,4 +1,7 @@ --- +exclude_paths: + - "tck/**" + engines: bandit: enabled: true diff --git a/.github/workflows/tck-test.yml b/.github/workflows/tck-test.yml new file mode 100644 index 000000000..fb74e33a6 --- /dev/null +++ b/.github/workflows/tck-test.yml @@ -0,0 +1,52 @@ +name: TCK Unit Test + +on: + push: + branches: + - "main" + paths: + - "tck/**" + - "tests/tck/**" + pull_request: + paths: + - "tck/**" + - "tests/tck/**" + workflow_dispatch: {} + +permissions: + contents: read + + +jobs: + tck-unit-test: + name: Tck Unit Test + runs-on: hl-sdk-py-lin-md + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.14" + + - name: Install uv + uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --all-extras --dev + + - name: Generate Proto Files + run: uv run generate_proto.py + + - name: Run unit tests + run: | + uv run pytest tests/tck -v diff --git a/CHANGELOG.md b/CHANGELOG.md index 932a41260..3eb783a11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,12 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ### Src - Fix the TransactionGetReceiptQuery to raise ReceiptStatusError for the non-retryable and non success receipt status - Refactor `AccountInfo` to use the existing `StakingInfo` wrapper class instead of flattened staking fields. Access is now via `info.staking_info.staked_account_id`, `info.staking_info.staked_node_id`, and `info.staking_info.decline_reward`. The old flat accessors (`info.staked_account_id`, `info.staked_node_id`, `info.decline_staking_reward`) are still available as deprecated properties and will emit a `DeprecationWarning`. (#1366) - +- Added abstract `Key` supper class to handle various proto Keys. ### Examples ### Tests - +- Added TCK endpoint for the createAccount method ### Docs diff --git a/codecov.yml b/codecov.yml index 622d0b98b..34b3fa451 100644 --- a/codecov.yml +++ b/codecov.yml @@ -10,5 +10,8 @@ coverage: target: 92% threshold: 2% +ignore: +- "tck/**" + comment: layout: "reach, diff, flags" diff --git a/src/hiero_sdk_python/account/account_create_transaction.py b/src/hiero_sdk_python/account/account_create_transaction.py index 43c33f4f9..02e8b0cb4 100644 --- a/src/hiero_sdk_python/account/account_create_transaction.py +++ b/src/hiero_sdk_python/account/account_create_transaction.py @@ -2,23 +2,23 @@ AccountCreateTransaction class. """ +import ctypes from typing import Optional, Union import warnings from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.channels import _Channel from hiero_sdk_python.crypto.evm_address import EvmAddress -from hiero_sdk_python.crypto.public_key import PublicKey +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.Duration import Duration from hiero_sdk_python.executable import _Method -from hiero_sdk_python.hapi.services import crypto_create_pb2, duration_pb2, transaction_pb2, basic_types_pb2 +from hiero_sdk_python.hapi.services import crypto_create_pb2, duration_pb2, transaction_pb2 from hiero_sdk_python.hapi.services.schedulable_transaction_body_pb2 import ( SchedulableTransactionBody, ) from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.transaction.transaction import Transaction -from hiero_sdk_python.utils.key_utils import Key, key_to_proto AUTO_RENEW_PERIOD = Duration(7890000) # around 90 days in seconds DEFAULT_TRANSACTION_FEE = Hbar(3).to_tinybars() # 3 Hbars @@ -258,12 +258,12 @@ def set_staked_account_id( """ self._require_not_frozen() if isinstance(account_id, str): - self.staked_account_id = AccountId.from_string(account_id) - elif isinstance(account_id, AccountId): - self.staked_account_id = account_id - else: + account_id = AccountId.from_string(account_id) + elif not isinstance(account_id, AccountId): raise TypeError("account_id must be of type str or AccountId") - + + self.staked_account_id = account_id + self.staked_node_id = None return self def set_staked_node_id(self, node_id: int) -> "AccountCreateTransaction": @@ -281,6 +281,7 @@ def set_staked_node_id(self, node_id: int) -> "AccountCreateTransaction": raise TypeError("node_id must be of type int") self.staked_node_id = node_id + self.staked_account_id = None return self def set_decline_staking_reward( @@ -315,8 +316,6 @@ def _build_proto_body(self) -> crypto_create_pb2.CryptoCreateTransactionBody: ValueError: If required fields are missing. TypeError: If initial_balance is an invalid type. """ - if not self.key: - raise ValueError("Key must be set before building the transaction.") if isinstance(self.initial_balance, Hbar): initial_balance_tinybars = self.initial_balance.to_tinybars() @@ -324,12 +323,15 @@ def _build_proto_body(self) -> crypto_create_pb2.CryptoCreateTransactionBody: initial_balance_tinybars = self.initial_balance else: raise TypeError("initial_balance must be Hbar or int (tinybars).") - - proto_key = key_to_proto(self.key) + + # Check for overflow + if initial_balance_tinybars >= (2**64): + raise OverflowError(f"Value {initial_balance_tinybars} exceeds 64-bit unsigned integer limit.") proto_body = crypto_create_pb2.CryptoCreateTransactionBody( - key=proto_key, - initialBalance=initial_balance_tinybars, + key=self.key.to_proto_key() if self.key is not None else None, + # triggers an INVALID_INITIAL_BALANCE pre-check error instead of a local error. + initialBalance=ctypes.c_uint64(initial_balance_tinybars).value, receiverSigRequired=self.receiver_signature_required, autoRenewPeriod=duration_pb2.Duration(seconds=self.auto_renew_period.seconds), memo=self.account_memo, @@ -338,9 +340,12 @@ def _build_proto_body(self) -> crypto_create_pb2.CryptoCreateTransactionBody: decline_reward=self.decline_staking_reward ) - if self.staked_account_id: + if self.staked_node_id is not None and self.staked_account_id is not None: + raise ValueError("Specify either staked_node_id or staked_account_id, not both.") + + if self.staked_account_id is not None: proto_body.staked_account_id.CopyFrom(self.staked_account_id._to_proto()) - elif self.staked_node_id: + elif self.staked_node_id is not None: proto_body.staked_node_id = self.staked_node_id return proto_body diff --git a/src/hiero_sdk_python/contract/contract_id.py b/src/hiero_sdk_python/contract/contract_id.py index 97ed95c9b..2e41b787b 100644 --- a/src/hiero_sdk_python/contract/contract_id.py +++ b/src/hiero_sdk_python/contract/contract_id.py @@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Optional from hiero_sdk_python.crypto.evm_address import EvmAddress +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.hapi.services import basic_types_pb2 from hiero_sdk_python.utils.entity_id_helper import ( parse_from_string, @@ -26,7 +27,7 @@ @dataclass(frozen=True) -class ContractId: +class ContractId(Key): """ Represents a unique contract ID on the Hedera network. @@ -90,6 +91,15 @@ def _to_proto(self): contractNum=self.contract, evm_address=self.evm_address, ) + + def to_proto_key(self) -> basic_types_pb2.Key: + """ + Convert the ContractId instance to a protobuf Key object. + + Returns: + basic_types_pb2.Key: The protobuf object of Key + """ + return basic_types_pb2.Key(contractID=self._to_proto()) @classmethod def from_string(cls, contract_id_str: str) -> "ContractId": diff --git a/src/hiero_sdk_python/contract/delegate_contract_id.py b/src/hiero_sdk_python/contract/delegate_contract_id.py new file mode 100644 index 000000000..69aa2ea57 --- /dev/null +++ b/src/hiero_sdk_python/contract/delegate_contract_id.py @@ -0,0 +1,81 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from hiero_sdk_python.contract.contract_id import ContractId +from hiero_sdk_python.hapi.services import basic_types_pb2 +from hiero_sdk_python.utils.entity_id_helper import format_to_string_with_checksum + +if TYPE_CHECKING: + from hiero_sdk_python.client.client import Client + +@dataclass(frozen=True) +class DelegateContractId(ContractId): + """ + Represents a delegatable contract identifier used as a key in the Hiero network. + + A DelegateContractId is a permissive key type that designates a smart contract + authorized to sign a transaction if it is the recipient of the active message + frame. Unlike a standard ContractID, this key type does not require the code + executing in the current frame to belong to the specified contract. + """ + + def to_proto_key(self) -> basic_types_pb2.Key: + return basic_types_pb2.Key(delegatable_contract_id=self._to_proto()) + + + + def __str__(self) -> str: + """ + Returns the string representation of the DelegateContractId. + + Format will be 'shard.realm.contract' or 'shard.realm.evm_address_hex' + if evm_address is set. Does not include a checksum. + + Returns: + str: The string representation of the ContractId. + """ + if self.evm_address is not None: + return f"{self.shard}.{self.realm}.{self.evm_address.hex()}" + + return f"{self.shard}.{self.realm}.{self.contract}" + + def __repr__(self) -> str: + """ + Returns a detailed string representation of the ContractId for debugging. + + Returns: + str: DelegateContractId(shard=X, realm=Y, contract=Z) or + DelegateContractId(shard=X, realm=Y, evm_address=...) if evm_address is set. + """ + if self.evm_address is not None: + return f"DelegateContractId(shard={self.shard}, realm={self.realm}, evm_address={self.evm_address.hex()})" + + return f"DelegateContractId(shard={self.shard}, realm={self.realm}, contract={self.contract})" + + + def to_string_with_checksum(self, client: "Client") -> str: + """ + Generates a string representation with a network-specific checksum. + + Format: 'shard.realm.contract-checksum' (e.g., "0.0.123-vfmkw"). + + Args: + client (Client): The client instance used to generate the + network-specific checksum. + + Returns: + str: The string representation with checksum. + + Raises: + ValueError: If the DelegateContractId has an `evm_address` set, + as checksums cannot be applied to EVM addresses. + """ + if self.evm_address is not None: + raise ValueError( + "to_string_with_checksum cannot be applied to DelegateContractId with evm_address" + ) + + return format_to_string_with_checksum( + self.shard, self.realm, self.contract, client + ) + diff --git a/src/hiero_sdk_python/crypto/evm_address.py b/src/hiero_sdk_python/crypto/evm_address.py index 1e396d189..596356859 100644 --- a/src/hiero_sdk_python/crypto/evm_address.py +++ b/src/hiero_sdk_python/crypto/evm_address.py @@ -1,4 +1,7 @@ -class EvmAddress: +from hiero_sdk_python.crypto.key import Key + + +class EvmAddress(Key): """ Represents a 20-byte EVM address derived from the rightmost 20 bytes of 32 byte Keccak-256 hash of an ECDSA public key. @@ -34,6 +37,10 @@ def from_string(cls, evm_address: str) -> "EvmAddress": def from_bytes(cls, address_bytes: "bytes") -> "EvmAddress": """Create an EvmAddress from raw bytes.""" return cls(address_bytes) + + + def to_proto_key(self): + raise RuntimeError("to_proto_key() not implemented for EvmAddress") def to_string(self) -> str: """Return the EVM address as a hex string""" diff --git a/src/hiero_sdk_python/crypto/key.py b/src/hiero_sdk_python/crypto/key.py new file mode 100644 index 000000000..d08b63620 --- /dev/null +++ b/src/hiero_sdk_python/crypto/key.py @@ -0,0 +1,117 @@ +from abc import ABC, abstractmethod + +from hiero_sdk_python.hapi.services import basic_types_pb2 + + +class Key(ABC): + """ + Abstract base class representing a Hiero cryptographic key. + This class defines the common interface for all supported key types. + + Concrete implementations must implement to_proto_key. + """ + + @classmethod + def from_proto_key(cls, proto: basic_types_pb2.Key) -> "Key": + """ + Convert a protobuf Key message into the appropriate SDK Key object. + + The method inspects the oneof key field in the protobuf message + and constructs the corresponding SDK key implementation. + + Supported types: + - ed25519 + - ECDSA_secp256k1 + - contractID + - delegatable_contract_id + - keyList + - thresholdKey + + Args: + proto (basic_types_pb2.Key): The protobuf Key message. + + Returns: + Key: The corresponding Key implementation. + + Raises: + TypeError: If proto is not a Key protobuf message. + ValueError: If the key type is unknown. + """ + from hiero_sdk_python.crypto.public_key import PublicKey + from hiero_sdk_python.crypto.evm_address import EvmAddress + from hiero_sdk_python.contract.contract_id import ContractId + from hiero_sdk_python.contract.delegate_contract_id import DelegateContractId + from hiero_sdk_python.crypto.key_list import KeyList + + if not isinstance(proto, basic_types_pb2.Key): + raise TypeError("proto must be an instance of basic_types_pb2.Key") + + key_type = proto.WhichOneof("key") + + match key_type: + case "ed25519": + return PublicKey._from_bytes_ed25519(proto.ed25519) + + case "ECDSA_secp256k1": + if len(proto.ECDSA_secp256k1) == 20: + return EvmAddress.from_bytes(proto.ECDSA_secp256k1) + + return PublicKey.from_bytes_ecdsa(proto.ECDSA_secp256k1) + + case "contractID": + return ContractId._from_proto(proto.contractID) + + case "delegatable_contract_id": + return DelegateContractId._from_proto(proto.delegatable_contract_id) + + case "keyList": + return KeyList.from_proto(proto=proto.keyList) + + case "thresholdKey": + return KeyList.from_proto( + proto=proto.thresholdKey.keys, + threshold=proto.thresholdKey.threshold, + ) + + case _: + raise ValueError(f"Unknown key type: {key_type}") + + @classmethod + def from_bytes(cls, data: bytes) -> "Key": + """ + Deserialize a Key object from protobuf-encoded bytes. + + Args: + data (bytes): Serialized protobuf Key bytes. + + Returns: + Key: The reconstructed Key instance. + + Raises: + TypeError: If data is not bytes. + """ + if not isinstance(data, (bytes, bytearray)): + raise TypeError("data must be bytes") + + key = basic_types_pb2.Key() + key.ParseFromString(data) + return cls.from_proto_key(key) + + @abstractmethod + def to_proto_key(self) -> basic_types_pb2.Key: + """ + Convert this key into its protobuf representation. + + Returns: + basic_types_pb2.Key: The protobuf Key message. + """ + pass + + def to_bytes(self) -> bytes: + """ + Serialize this Key into protobuf bytes. + + Returns: + bytes: The serialized protobuf Key. + """ + return self.to_proto_key().SerializeToString() diff --git a/src/hiero_sdk_python/crypto/key_list.py b/src/hiero_sdk_python/crypto/key_list.py new file mode 100644 index 000000000..c0bf8ae00 --- /dev/null +++ b/src/hiero_sdk_python/crypto/key_list.py @@ -0,0 +1,157 @@ +from typing import List + +from hiero_sdk_python.crypto.key import Key +from hiero_sdk_python.hapi.services import basic_types_pb2 + + +class KeyList(Key): + """ + Represents a list of cryptographic keys that can sign a transaction. + + The list may optionally define a threshold, specifying how many + keys must sign for the transaction to be valid. + """ + + def __init__(self, keys: List[Key] = None, threshold: int | None = None) -> None: + """ + Initialize a KeyList. + + Args: + keys (List[Key], optional): A list of keys that belong to this KeyList. + threshold (int, optional): The minimum number of keys required to sign. + + Raises: + TypeError: If keys are not a list of Key objects or threshold not an int. + """ + if keys is not None: + if not isinstance(keys, list): + raise TypeError("keys must be a list of Key objects") + + for key in keys: + if not isinstance(key, Key): + raise TypeError("All elements in keys must be instances of Key") + + if threshold is not None and not isinstance(threshold, int): + raise TypeError("threshold must be an integer") + + self.keys: List[Key] = keys or [] + self.threshold: int | None = threshold + + def set_keys(self, keys: List[Key]) -> "KeyList": + """ + Set the keys contained in this KeyList. + + Args: + keys (List[Key]): The new list of keys. + + Returns: + KeyList: The current instance for method chaining. + + Raises: + TypeError: If keys is not a list of Key objects. + """ + if not isinstance(keys, list): + raise TypeError("keys must be a list of Key objects") + + for key in keys: + if not isinstance(key, Key): + raise TypeError("All elements in keys must be instances of Key") + + self.keys = keys + return self + + def add_key(self, key: Key) -> "KeyList": + """ + Add a key to the KeyList. + + Args: + key (Key): The key to add. + + Returns: + KeyList: The current instance for method chaining. + + Raises: + TypeError: If key is not an instance of Key. + """ + if not isinstance(key, Key): + raise TypeError("All elements in keys must be instances of Key") + + self.keys.append(key) + return self + + def set_threshold(self, threshold: int | None) -> "KeyList": + """ + Set the threshold for this KeyList. + + Args: + threshold (int | None): The minimum number of keys required to sign. + + Returns: + KeyList: The current instance for method chaining. + + Raises: + TypeError: If threshold is not an integer. + """ + if threshold is not None and not isinstance(threshold, int): + raise TypeError("threshold must be an integer") + + self.threshold = threshold + return self + + @classmethod + def from_proto( + cls, proto: basic_types_pb2.KeyList, threshold: int = None + ) -> "KeyList": + """ + Create a KeyList from a protobuf representation. + + Args: + proto (basic_types_pb2.KeyList): The protobuf KeyList. + threshold (int, optional): Optional threshold value for the KeyList. + + Returns: + KeyList: The constructed KeyList instance. + """ + keys = [] + for key in proto.keys: + keys.append(Key.from_proto_key(key)) + + return cls(keys=keys, threshold=threshold) + + def to_proto(self) -> basic_types_pb2.KeyList: + """ + Convert this KeyList to its protobuf representation. + + Returns: + basic_types_pb2.KeyList: The protobuf KeyList object. + """ + proto_keys = [] + for key in self.keys: + proto_keys.append(key.to_proto_key()) + + return basic_types_pb2.KeyList(keys=proto_keys) + + def to_proto_key(self) -> basic_types_pb2.Key: + """ + Convert this KeyList to a protobuf Key object. + + If a threshold is defined, the result will be a ThresholdKey. + Otherwise, it will be a standard KeyList. + + Returns: + basic_types_pb2.Key: The protobuf Key representation. + """ + proto_key_list = [] + + for key in self.keys: + proto_key_list.append(key.to_proto_key()) + + if self.threshold is not None: + threshold_key = basic_types_pb2.ThresholdKey( + threshold=self.threshold, + keys=basic_types_pb2.KeyList(keys=proto_key_list), + ) + return basic_types_pb2.Key(thresholdKey=threshold_key) + + key_list = basic_types_pb2.KeyList(keys=proto_key_list) + return basic_types_pb2.Key(keyList=key_list) diff --git a/src/hiero_sdk_python/crypto/private_key.py b/src/hiero_sdk_python/crypto/private_key.py index 6b97a38ae..5f65ade14 100644 --- a/src/hiero_sdk_python/crypto/private_key.py +++ b/src/hiero_sdk_python/crypto/private_key.py @@ -5,13 +5,15 @@ from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ed25519, ec from cryptography.hazmat.primitives.asymmetric import utils as asym_utils +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.crypto.public_key import PublicKey +from hiero_sdk_python.hapi.services import basic_types_pb2 from hiero_sdk_python.utils.crypto_utils import keccak256 _LEGACY_ECDSA_PRIVATE_KEY_PREFIX = "3030020100300706052b8104000a04220420" -class PrivateKey: +class PrivateKey(Key): """ Represents a private key that can be either Ed25519 or ECDSA (secp256k1). Can load from raw 32-byte seeds/scalars or DER-encoded keys. @@ -470,6 +472,15 @@ def _parse_legacy_ecdsa_der_key(key_bytes: bytes) -> "ec.EllipticCurvePrivateKey except Exception as exc: raise ValueError(f"Failed to derive ECDSA private key: {exc}") from exc + def to_proto_key(self) -> basic_types_pb2.Key: + """ + Convert the instance of PrivateKey to the protobuf object of Key. + + Returns: + basic_types_pb2.Key: The protobuf object of Key. + """ + return self.public_key().to_proto_key() + def __eq__(self, other: object) -> bool: """ Compare two PrivateKey objects for equality. @@ -486,3 +497,4 @@ def __eq__(self, other: object) -> bool: def __hash__(self) -> int: """Returns the hash value for the private key.""" return hash((self.is_ed25519(), self.to_bytes_raw())) + \ No newline at end of file diff --git a/src/hiero_sdk_python/crypto/public_key.py b/src/hiero_sdk_python/crypto/public_key.py index e7f55ee1f..7142d7b11 100644 --- a/src/hiero_sdk_python/crypto/public_key.py +++ b/src/hiero_sdk_python/crypto/public_key.py @@ -7,7 +7,7 @@ from cryptography.hazmat.primitives.asymmetric import utils as asym_utils from hiero_sdk_python.contract.contract_id import ContractId from hiero_sdk_python.crypto.evm_address import EvmAddress -from hiero_sdk_python.hapi.services.basic_types_pb2 import Key +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.hapi.services import basic_types_pb2 from hiero_sdk_python.utils.crypto_utils import keccak256 @@ -19,7 +19,7 @@ def _warn_ed25519_ambiguity(caller_name: str) -> None: stacklevel=3 ) -class PublicKey: +class PublicKey(Key): """ Represents a public key. Supports multiple key formats: raw bytes, DER-encoded keys, @@ -307,7 +307,7 @@ def from_string(cls, hex_str: str) -> "PublicKey": # @classmethod - def _from_proto(cls, proto: Key) -> "PublicKey": + def _from_proto(cls, proto: basic_types_pb2.Key) -> "PublicKey": """ Load a public key from a protobuf Key message. """ @@ -342,6 +342,15 @@ def _to_proto(self) -> Key: if self.is_ed25519(): return basic_types_pb2.Key(ed25519=pub_bytes) return basic_types_pb2.Key(ECDSA_secp256k1=pub_bytes) + + def to_proto_key(self) -> basic_types_pb2.Key: + """ + Convert the instance of PublicKey to the protobuf object of Key. + + Returns: + basic_types_pb2.Key: The protobuf object of Key. + """ + return self._to_proto() # # --------------------------------- diff --git a/src/hiero_sdk_python/hbar.py b/src/hiero_sdk_python/hbar.py index cc080b66e..fbdc2ad57 100644 --- a/src/hiero_sdk_python/hbar.py +++ b/src/hiero_sdk_python/hbar.py @@ -45,6 +45,8 @@ def __init__( if isinstance(amount, float) and not math.isfinite(amount): raise ValueError("Hbar amount must be finite") + if isinstance(amount, Decimal) and not amount.is_finite(): + raise ValueError("Hbar amount must be finite") if unit == HbarUnit.TINYBAR: if not isinstance(amount, int): @@ -56,7 +58,7 @@ def __init__( amount = Decimal(str(amount)) tinybar = amount * Decimal(unit.tinybar) - if tinybar % 1 != 0: + if tinybar != tinybar.to_integral_value(): raise ValueError("Fractional tinybar value not allowed") self._amount_in_tinybar = int(tinybar) @@ -67,7 +69,7 @@ def to(self, unit: HbarUnit) -> float: def to_tinybars(self) -> int: """Return the amount of hbars in tinybars.""" - return int(self.to(HbarUnit.TINYBAR)) + return self._amount_in_tinybar def to_hbars(self) -> float: """ @@ -220,4 +222,4 @@ def __ge__(self, other: object) -> bool: Hbar.ZERO = Hbar(0) Hbar.MAX = Hbar(50_000_000_000) -Hbar.MIN = Hbar(-50_000_000_000) \ No newline at end of file +Hbar.MIN = Hbar(-50_000_000_000) diff --git a/tck/__init__.py b/tck/__init__.py index 7e31d063a..f34a652cb 100644 --- a/tck/__init__.py +++ b/tck/__init__.py @@ -1,3 +1,5 @@ """tck package initialization.""" + from .server import start_server + __all__ = ["start_server"] diff --git a/tck/__main__.py b/tck/__main__.py index 44f1394b7..85a432b8d 100644 --- a/tck/__main__.py +++ b/tck/__main__.py @@ -1,9 +1,12 @@ """Main module to start the TCK server.""" + from . import start_server + def main(): """Main function to start the TCK server.""" start_server() + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/tck/errors.py b/tck/errors.py index 12dd4ea9d..781a6758a 100644 --- a/tck/errors.py +++ b/tck/errors.py @@ -1,15 +1,25 @@ - """Error code constants for the TCK server.""" + +from functools import wraps +import logging + +from hiero_sdk_python.exceptions import MaxAttemptsError, PrecheckError, ReceiptStatusError +from hiero_sdk_python.response_code import ResponseCode + +# These constants can be used throughout the codebase to represent specific error conditions. PARSE_ERROR = -32700 INVALID_REQUEST = -32600 METHOD_NOT_FOUND = -32601 INVALID_PARAMS = -32602 INTERNAL_ERROR = -32603 HIERO_ERROR = -32001 -# These constants can be used throughout the codebase to represent specific error conditions. + +logger = logging.getLogger(__name__) + class JsonRpcError(Exception): """Class representing a JSON-RPC error.""" + def __init__(self, code: int, message: str, data=None) -> None: super().__init__(message) self.code = code @@ -18,71 +28,107 @@ def __init__(self, code: int, message: str, data=None) -> None: def to_dict(self): """Convert the error to a dictionary representation.""" - error = { - "code": self.code, - "message": self.message - } + error = {"code": self.code, "message": self.message} if self.data is not None: error["data"] = self.data return error - @classmethod def parse_error(cls, data=None, message: str = "Parse error") -> "JsonRpcError": """Create a Parse Error JSON-RPC error. - + Args: data: Optional error details message: Optional custom error message (defaults to 'Parse error') """ return cls(PARSE_ERROR, message, data) - + @classmethod - def invalid_request_error(cls, data=None, message: str = "Invalid Request") -> "JsonRpcError": + def invalid_request_error( + cls, data=None, message: str = "Invalid Request" + ) -> "JsonRpcError": """Create an Invalid Request JSON-RPC error. - + Args: data: Optional error details message: Optional custom error message (defaults to 'Invalid Request') """ return cls(INVALID_REQUEST, message, data) - + @classmethod - def method_not_found_error(cls, data=None, message: str = "Method not found") -> "JsonRpcError": + def method_not_found_error( + cls, data=None, message: str = "Method not found" + ) -> "JsonRpcError": """Create a Method Not Found JSON-RPC error. - + Args: data: Optional error details message: Optional custom error message (defaults to 'Method not found') """ return cls(METHOD_NOT_FOUND, message, data) - + @classmethod - def invalid_params_error(cls, data=None, message: str = "Invalid params") -> "JsonRpcError": + def invalid_params_error( + cls, data=None, message: str = "Invalid params" + ) -> "JsonRpcError": """Create an Invalid Params JSON-RPC error. - + Args: data: Optional error details message: Optional custom error message (defaults to 'Invalid params') """ return cls(INVALID_PARAMS, message, data) - + @classmethod - def internal_error(cls, data=None, message: str = "Internal error") -> "JsonRpcError": + def internal_error( + cls, data=None, message: str = "Internal error" + ) -> "JsonRpcError": """Create an Internal Error JSON-RPC error. - + Args: data: Optional error details message: Optional custom error message (defaults to 'Internal error') """ return cls(INTERNAL_ERROR, message, data) - + @classmethod def hiero_error(cls, data=None, message: str = "Hiero error") -> "JsonRpcError": """Create a Hiero-specific JSON-RPC error. - + Args: data: Optional error details message: Optional custom error message (defaults to 'Hiero error') """ - return cls(HIERO_ERROR, message, data) \ No newline at end of file + return cls(HIERO_ERROR, message, data) + + +def handle_sdk_errors(func): + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except JsonRpcError: + raise + except PrecheckError as e: + logger.error(f"PrecheckError (status: {ResponseCode(e.status).name}, method: {func.__name__})") + raise JsonRpcError.hiero_error( + {"status": ResponseCode(e.status).name} + ) + + except ReceiptStatusError as e: + logger.error(f"ReceiptStatusError (status: {ResponseCode(e.status).name}, method: {func.__name__})") + raise JsonRpcError.hiero_error( + {"status": ResponseCode(e.status).name} + ) + + except MaxAttemptsError as e: + logger.error(f"MaxAttemptsError (method: {func.__name__})") + raise JsonRpcError.hiero_error( + message= str(e) + ) + + except Exception as e: + logger.exception("Unhandled error in RPC handler") + raise JsonRpcError.internal_error(message="Internal error") + + return wrapper diff --git a/tck/handlers/__init__.py b/tck/handlers/__init__.py index 2facc2b42..93dad61ea 100644 --- a/tck/handlers/__init__.py +++ b/tck/handlers/__init__.py @@ -1,9 +1,19 @@ """TCK handlers - auto-import all handler modules.""" # Import registry functions first to make them available -from .registry import get_handler, get_all_handlers, safe_dispatch, validate_request_params +from .registry import ( + get_handler, + get_all_handlers, + safe_dispatch, +) -# Import all handler modules to trigger @register_handler decorators -from . import sdk # setup, reset +# Import all handler modules to trigger @rpc_method decorators +from . import sdk # setup, reset +from . import key +from . import account -__all__ = ["get_handler", "get_all_handlers", "safe_dispatch", "validate_request_params"] \ No newline at end of file +__all__ = [ + "get_handler", + "get_all_handlers", + "safe_dispatch", +] diff --git a/tck/handlers/account.py b/tck/handlers/account.py new file mode 100644 index 000000000..521a6633e --- /dev/null +++ b/tck/handlers/account.py @@ -0,0 +1,68 @@ +from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction +from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.hbar import Hbar +from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt +from tck.handlers.registry import rpc_method +from tck.param.account import CreateAccountParams +from tck.response.account import CreateAccountResponse +from tck.util.client_utils import get_client +from tck.util.constants import DEFAULT_GRPC_TIMEOUT +from tck.util.key_utils import get_key_from_string + + +def _build_create_account_transaction(params: CreateAccountParams) -> AccountCreateTransaction: + transaction = AccountCreateTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT) + + if params.key is not None: + transaction.set_key_without_alias(get_key_from_string(params.key)) + + if params.initialBalance is not None: + transaction.set_initial_balance(Hbar.from_tinybars(params.initialBalance)) + + if params.receiverSignatureRequired is not None: + transaction.set_receiver_signature_required(params.receiverSignatureRequired) + + if params.maxAutoTokenAssociations is not None: + transaction.set_max_automatic_token_associations( + params.maxAutoTokenAssociations + ) + + if params.stakedAccountId is not None: + transaction.set_staked_account_id(AccountId.from_string(params.stakedAccountId)) + + if params.stakedNodeId is not None: + transaction.set_staked_node_id(params.stakedNodeId) + + if params.declineStakingReward is not None: + transaction.set_decline_staking_reward(params.declineStakingReward) + + if params.memo is not None: + transaction.set_account_memo(params.memo) + + if params.autoRenewPeriod is not None: + transaction.set_auto_renew_period(params.autoRenewPeriod) + + if params.alias is not None: + transaction.set_alias(params.alias) + + return transaction + + +@rpc_method("createAccount") +def create_account(params: CreateAccountParams) -> CreateAccountResponse: + client = get_client(params.sessionId) + + transaction = _build_create_account_transaction(params) + + if params.commonTransactionParams is not None: + params.commonTransactionParams.apply_common_params(transaction, client) + + response = transaction.execute(client, wait_for_receipt=False) + receipt: TransactionReceipt = response.get_receipt(client, validate_status=True) + + account_id = "" + if receipt.status == ResponseCode.SUCCESS: + account_id = str(receipt.account_id) + + return CreateAccountResponse(account_id, ResponseCode(receipt.status).name) diff --git a/tck/handlers/key.py b/tck/handlers/key.py new file mode 100644 index 000000000..a93c596bd --- /dev/null +++ b/tck/handlers/key.py @@ -0,0 +1,142 @@ +from hiero_sdk_python.crypto.key_list import KeyList +from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.crypto.public_key import PublicKey +from hiero_sdk_python.hapi.services.basic_types_pb2 import Key +from tck.errors import JsonRpcError +from tck.handlers.registry import rpc_method +from tck.param.key import KeyGenerationParams +from tck.response.key import KeyGenerationResponse +from tck.util.key_utils import KeyType, get_key_from_string + + +@rpc_method("generateKey") +def generate_key(params: KeyGenerationParams) -> KeyGenerationResponse: + if params.fromKey and params.type not in { + KeyType.ED25519_PUBLIC_KEY, + KeyType.ECDSA_SECP256K1_PUBLIC_KEY, + KeyType.EVM_ADDRESS_KEY, + }: + raise JsonRpcError.invalid_params_error( + "invalid parameters: fromKey is only allowed for " + "ed25519PublicKey, ecdsaSecp256k1PublicKey, or evmAddress types." + ) + + if params.threshold is not None and params.type != KeyType.THRESHOLD_KEY: + raise JsonRpcError.invalid_params_error( + "invalid parameters: threshold is only allowed for thresholdKey types." + ) + + if params.type == KeyType.THRESHOLD_KEY and params.threshold is None: + raise JsonRpcError.invalid_params_error( + "invalid request: threshold is required for generating a ThresholdKey type." + ) + + if params.keys and params.type not in {KeyType.THRESHOLD_KEY, KeyType.LIST_KEY}: + raise JsonRpcError.invalid_params_error( + "invalid parameters: keys are only allowed for keyList or thresholdKey types." + ) + + if params.type in {KeyType.THRESHOLD_KEY, KeyType.LIST_KEY} and not params.keys: + raise JsonRpcError.invalid_params_error( + "invalid parameters: keys must be provided for keyList or thresholdKey types." + ) + + response = KeyGenerationResponse() + response.key = _process_key_recursively( + params=params, response=response, is_list=False + ) + + return response + + +def _handle_private_key( + params: KeyGenerationParams, response: KeyGenerationResponse, is_list: bool +) -> str: + if params.type == KeyType.ED25519_PRIVATE_KEY: + private_key = PrivateKey.generate_ed25519() + else: + private_key = PrivateKey.generate_ecdsa() + + private_key_string = private_key.to_string_der() + + if is_list: + response.privateKeys.append(private_key_string) + + return private_key_string + + +def _handle_public_key( + params: KeyGenerationParams, response: KeyGenerationResponse, is_list: bool +) -> str: + if params.fromKey: + return PrivateKey.from_string(params.fromKey).public_key().to_string_der() + + if params.type == KeyType.ED25519_PUBLIC_KEY: + private_key = PrivateKey.generate_ed25519() + else: + private_key = PrivateKey.generate_ecdsa() + + if is_list: + response.privateKeys.append(private_key.to_string_der()) + + return private_key.public_key().to_string_der() + + +def _handle_key_list( + params: KeyGenerationParams, response: KeyGenerationResponse, is_list: bool +) -> str: + key_list = KeyList() + + for key_params in params.keys: + key_string = _process_key_recursively( + params=key_params, response=response, is_list=True + ) + key_list.add_key(get_key_from_string(key_string)) + + if params.type == KeyType.THRESHOLD_KEY: + key_list.set_threshold(int(params.threshold)) + + return key_list.to_bytes().hex() + + +def _handle_evm_address( + params: KeyGenerationParams, response: KeyGenerationResponse, is_list: bool +) -> str: + if params.fromKey: + key = get_key_from_string(params.fromKey) + + if isinstance(key, PrivateKey): + return str(key.public_key().to_evm_address()) + + elif isinstance(key, PublicKey): + return str(key.to_evm_address()) + + else: + raise JsonRpcError.invalid_params_error( + "invalid parameters: fromKey for evmAddress is not ECDSAsecp256k1." + ) + + return str(PrivateKey.generate_ecdsa().public_key().to_evm_address()) + + +def _process_key_recursively( + params: KeyGenerationParams, response: KeyGenerationResponse, is_list: bool +) -> str: + if params.type in { + KeyType.ED25519_PRIVATE_KEY, + KeyType.ECDSA_SECP256K1_PRIVATE_KEY, + }: + return _handle_private_key(params, response, is_list) + elif params.type in { + KeyType.ED25519_PUBLIC_KEY, + KeyType.ECDSA_SECP256K1_PUBLIC_KEY, + }: + return _handle_public_key(params, response, is_list) + elif params.type in {KeyType.LIST_KEY, KeyType.THRESHOLD_KEY}: + return _handle_key_list(params, response, is_list) + elif params.type == KeyType.EVM_ADDRESS_KEY: + return _handle_evm_address(params, response, is_list) + else: + raise JsonRpcError.invalid_params_error( + "invalid request: key type not recognized." + ) diff --git a/tck/handlers/registry.py b/tck/handlers/registry.py index 87da68145..1960a2c4f 100644 --- a/tck/handlers/registry.py +++ b/tck/handlers/registry.py @@ -1,69 +1,79 @@ -"""Build a flexible registry-based method routing system that can dispatch +"""Build a flexible registry-based method routing system that can dispatch requests to handlers and transform exceptions into JSON-RPC errors.""" + +from dataclasses import asdict +import inspect from typing import Any, Dict, Optional, Union, Callable -from tck.errors import ( - JsonRpcError -) +from tck.errors import JsonRpcError, handle_sdk_errors from tck.protocol import build_json_rpc_error_response -from hiero_sdk_python.exceptions import PrecheckError, ReceiptStatusError, MaxAttemptsError - # A global _HANDLERS dict to store method name -> handler function mappings _HANDLERS: Dict[str, Callable] = {} -def register_handler(method_name: str): + +def rpc_method(method_name: str): """Register a handler function for a given method name.""" + def decorator(func: Callable) -> Callable: """Decorator to register a handler function for a given method name.""" - _HANDLERS[method_name] = func + _HANDLERS[method_name] = handle_sdk_errors(func) return func + return decorator + def get_handler(method_name: str) -> Optional[Callable]: """Retrieve a handler by method name.""" return _HANDLERS.get(method_name) + def get_all_handlers() -> Dict[str, Callable]: """Get all registered handlers.""" return _HANDLERS.copy() -def dispatch(method_name: str, params: Any, session_id: Optional[str]) -> Any: +def dispatch(method_name: str, params: Any) -> Any: """Dispatch the request to the appropriate handler based on method_name.""" handler = get_handler(method_name) - + if handler is None: - raise JsonRpcError.method_not_found_error(message=f'Method not found: {method_name}') + raise JsonRpcError.method_not_found_error( + message=f"Method not found: {method_name}" + ) + try: - if session_id is not None: - return handler(params, session_id) - return handler(params) + signature = inspect.signature(handler) + parameters = list(signature.parameters.values()) + param_type = parameters[0].annotation + + try: + params = param_type.parse_json_params(params) + except (TypeError, ValueError) as e: + raise JsonRpcError.invalid_params_error(data=str(e)) from e + + result = handler(params) + + return parse_result(result) + except JsonRpcError: raise - except (PrecheckError, ReceiptStatusError, MaxAttemptsError) as e: - raise JsonRpcError.hiero_error(data=str(e)) from e except Exception as e: raise JsonRpcError.internal_error(data=str(e)) from e -def safe_dispatch(method_name: str, - params: Any, - session_id: Optional[str], - request_id: Optional[Union[str, int]]) -> Union[Any, Dict[str, Any]]: + +def safe_dispatch( + method_name: str, params: Any, request_id: Optional[Union[str, int]] +) -> Union[Any, Dict[str, Any]]: """Safely dispatch the request and handle exceptions.""" try: - return dispatch(method_name, params, session_id) + return dispatch(method_name, params) except JsonRpcError as e: return build_json_rpc_error_response(e, request_id) - except Exception as e: # Defensive fallback for any uncaught exceptions + except Exception as e: error = JsonRpcError.internal_error(data=str(e)) return build_json_rpc_error_response(error, request_id) -def validate_request_params(params: Any, required_fields: Dict[str, type]) -> None: - """Validate that required fields are present in params with correct types.""" - if not isinstance(params, dict): - raise JsonRpcError.invalid_params_error(message='Invalid params: expected object') - - for field, field_type in required_fields.items(): - if field not in params or not isinstance(params[field], field_type): - raise JsonRpcError.invalid_params_error(message=f'Invalid params: missing or incorrect type for {field}') +def parse_result(result: Any) -> dict: + """Parse the result from the methods to dict containing non none key:values""" + return {k: v for k, v in asdict(result).items() if v is not None} diff --git a/tck/handlers/sdk.py b/tck/handlers/sdk.py index 1bef18e06..964cd37d9 100644 --- a/tck/handlers/sdk.py +++ b/tck/handlers/sdk.py @@ -1,121 +1,56 @@ -from typing import Any, Dict, Optional, List from hiero_sdk_python.node import _Node from hiero_sdk_python import Client, AccountId, PrivateKey, Network -from tck.handlers.registry import register_handler, validate_request_params -from tck.client_manager import store_client, remove_client -from tck.errors import JsonRpcError - - -def _create_node_objects(node_addresses: List[str]) -> List[_Node]: - """Convert node addresses to _Node objects with generated AccountIds. - Args: - node_addresses: List of node address strings - - Returns: - List of _Node objects with sequential AccountIds starting from 0.0.3 - """ - node_objects = [] - for idx, node_address in enumerate(node_addresses): - account_id = AccountId(0, 0, 3 + idx) - node_obj = _Node(account_id, node_address, None) - node_objects.append(node_obj) - return node_objects - - -def _create_custom_network_client(network_config: Dict[str, Any]) -> Client: - """Create a client with custom network configuration. - Args: - network_config: Dictionary containing 'nodes' key with list of node addresses - - Returns: - Configured Client instance - - Raises: - JsonRpcError: If nodes configuration is invalid - """ - nodes = network_config.get('nodes') - if not isinstance(nodes, list) or len(nodes) == 0 or not all(isinstance(node, str) for node in nodes): - raise JsonRpcError.invalid_params_error(message='Invalid params: nodes must be a non-empty list of strings') - - node_objects = _create_node_objects(nodes) - network = Network(nodes=node_objects) - return Client(network) - - -def _create_client(network_param: Optional[Any]) -> Client: - """Create and return the appropriate Client based on network parameter. - Args: - network_param: Network specification (None/'testnet' for testnet, 'mainnet', or custom dict) - - Returns: - Configured Client instance - - Raises: - JsonRpcError: If network specification is invalid - """ - if network_param is None or network_param == 'testnet': - return Client.for_testnet() - - if network_param == 'mainnet': - return Client.for_mainnet() - - if isinstance(network_param, dict) and 'nodes' in network_param: - return _create_custom_network_client(network_param) - - raise JsonRpcError.invalid_params_error(message='Invalid params: unknown network specification') - - -def _parse_operator_credentials(params: Dict[str, Any]) -> tuple[AccountId, PrivateKey]: - """Parse and validate operator credentials from parameters. - - Args: - params: Request parameters containing operatorAccountId and operatorPrivateKey - - Returns: - Tuple of (AccountId, PrivateKey) - Raises: - JsonRpcError: If credentials cannot be parsed - """ - operator_account_id_str = params['operatorAccountId'] - operator_private_key_str = params['operatorPrivateKey'] +from tck.handlers.registry import rpc_method +from tck.param.base import BaseParams +from tck.param.sdk import SetupParams +from tck.response.sdk import SetupResponse +from tck.util.client_utils import get_client, remove_client, store_client + + +@rpc_method("setup") +def setup_handler(params: SetupParams) -> SetupResponse: + operator_account_id = AccountId.from_string(params.operatorAccountId) + operator_private_key = PrivateKey.from_string(params.operatorPrivateKey) + + if params.nodeIp and params.nodeAccountId and params.mirrorNetworkIp: + client = Client() + client.network = Network( + nodes=[ + _Node(AccountId.from_string(params.nodeAccountId), params.nodeIp, None) + ], + mirror_address=params.mirrorNetworkIp, + ) + client.set_operator(operator_account_id, operator_private_key) + + client_type = "custom" + store_client(params.sessionId, client) + else: + client = Client.for_testnet() + client_type = "testnet" + store_client(params.sessionId, client) + + client = get_client(params.sessionId) + client.set_operator(operator_account_id, operator_private_key) - try: - operator_account_id = AccountId.from_string(operator_account_id_str) - operator_private_key = PrivateKey.from_string(operator_private_key_str) - except Exception as e: - raise JsonRpcError.invalid_params_error(message=f'Invalid params: invalid operatorAccountId/operatorPrivateKey format - {str(e)}') from e + return SetupResponse(f"Successfully setup {client_type} client") - return operator_account_id, operator_private_key +@rpc_method("setOperator") +def set_operator(params: SetupParams) -> SetupResponse: + operator_account_id = AccountId.from_string(params.operatorAccountId) + operator_private_key = PrivateKey.from_string(params.operatorPrivateKey) -@register_handler("setup") -def setup_handler(params: Dict[str, Any], session_id: Optional[str] = None) -> Dict[str, Any]: - """Setup handler to initialize SDK clients with operator credentials and network configuration.""" - # Validate required parameters - required_fields = { - 'operatorAccountId': str, - 'operatorPrivateKey': str - } - validate_request_params(params, required_fields) + client = get_client(params.sessionId) + client.set_operator(operator_account_id, operator_private_key) - # Parse operator credentials - operator_account_id, operator_private_key = _parse_operator_credentials(params) + return SetupResponse("") - # Create client based on network configuration - client = _create_client(params.get('network')) - client.set_operator(operator_account_id, operator_private_key) - # Store the initialized client - effective_session_id = session_id or "default" - store_client(effective_session_id, client) +@rpc_method("reset") +def reset_handler(params: BaseParams) -> SetupResponse: + client = remove_client(params.sessionId) - return {"status": "success", "sessionId": effective_session_id} + if client is not None: + client.close() -@register_handler("reset") -def reset_handler(params: Dict[str, Any], session_id: Optional[str] = None) -> Dict[str, Any]: - """Reset handler to close connections and clear client state.""" - target_session_id = params.get('sessionId') or session_id - if target_session_id is None: - target_session_id = "default" - remove_client(target_session_id) - return {"status": "reset completed"} + return SetupResponse("Successfully reset client") diff --git a/tck/param/account.py b/tck/param/account.py new file mode 100644 index 000000000..5682e41a7 --- /dev/null +++ b/tck/param/account.py @@ -0,0 +1,40 @@ +from dataclasses import dataclass + +from tck.param.base import BaseTransactionParams +from tck.util.param_utils import ( + parse_common_transaction_params, + parse_session_id, + to_bool, + to_int, +) + + +@dataclass +class CreateAccountParams(BaseTransactionParams): + key: str | None = None + initialBalance: int | None = None + receiverSignatureRequired: bool | None = None + maxAutoTokenAssociations: int | None = None + stakedAccountId: str | None = None + stakedNodeId: int | None = None + declineStakingReward: bool | None = None + memo: str | None = None + autoRenewPeriod: int | None = None + alias: str | None = None + + @classmethod + def parse_json_params(cls, params: dict) -> "CreateAccountParams": + return cls( + key=params.get("key"), + initialBalance=to_int(params.get("initialBalance")), + receiverSignatureRequired=to_bool(params.get("receiverSignatureRequired")), + maxAutoTokenAssociations=to_int(params.get("maxAutoTokenAssociations")), + stakedAccountId=params.get("stakedAccountId"), + stakedNodeId=to_int(params.get("stakedNodeId")), + declineStakingReward=to_bool(params.get("declineStakingReward")), + memo=params.get("memo"), + autoRenewPeriod=to_int(params.get("autoRenewPeriod")), + alias=params.get("alias"), + sessionId=parse_session_id(params), + commonTransactionParams=parse_common_transaction_params(params), + ) diff --git a/tck/param/base.py b/tck/param/base.py new file mode 100644 index 000000000..4ca35089e --- /dev/null +++ b/tck/param/base.py @@ -0,0 +1,18 @@ +from dataclasses import dataclass + +from tck.param.common import CommonTransactionParams +from tck.util.param_utils import parse_session_id + + +@dataclass +class BaseParams: + sessionId: str = None + + @classmethod + def parse_json_params(cls, params: dict) -> "BaseParams": + return cls(parse_session_id(params)) + + +@dataclass +class BaseTransactionParams(BaseParams): + commonTransactionParams: CommonTransactionParams | None = None diff --git a/tck/param/common.py b/tck/param/common.py new file mode 100644 index 000000000..b9e3da87c --- /dev/null +++ b/tck/param/common.py @@ -0,0 +1,56 @@ +from dataclasses import dataclass + +from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.client.client import Client +from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.transaction.transaction import Transaction +from hiero_sdk_python.transaction.transaction_id import TransactionId +from tck.util.param_utils import to_bool, to_int + + +@dataclass +class CommonTransactionParams: + transactionId: str | None = None + maxTransactionFee: int | None = None + validTransactionDuration: int | None = None + memo: str | None = None + regenerateTransactionId: bool | None = None + signers: list[str] | None = None + + @classmethod + def parse_json_params(cls, params: dict) -> "CommonTransactionParams": + return cls( + transactionId=params.get("transactionId"), + maxTransactionFee=to_int(params.get("maxTransactionFee")), + validTransactionDuration=to_int(params.get("validTransactionDuration")), + memo=params.get("memo"), + regenerateTransactionId=to_bool(params.get("regenerateTransactionId")), + signers=[signer for signer in params.get("signers", [])], + ) + + def apply_common_params(self, transaction: Transaction, client: Client) -> None: + """Apply commonTransactionParams to a given transaction.""" + if self.transactionId is not None: + try: + transaction.set_transaction_id( + TransactionId.from_string(self.transactionId) + ) + except: + transaction.set_transaction_id( + TransactionId.generate(AccountId.from_string(self.transactionId)) + ) + + # TODO add a max_transaction_fee sdk missing func + + if self.validTransactionDuration is not None: + transaction.set_transaction_valid_duration(self.validTransactionDuration) + + if self.memo is not None: + transaction.set_transaction_memo(self.memo) + + # TODO add regenerate_transaction_id sdk missing func + + if self.signers: + transaction.freeze_with(client) + for signer in self.signers: + transaction.sign(PrivateKey.from_string(signer)) diff --git a/tck/param/key.py b/tck/param/key.py new file mode 100644 index 000000000..cf7998699 --- /dev/null +++ b/tck/param/key.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass +from typing import List + +from tck.util.key_utils import KeyType + + +@dataclass +class KeyGenerationParams: + type: KeyType = None + fromKey: str | None = None + threshold: int | None = None + keys: List["KeyGenerationParams"] | None = None + + @classmethod + def parse_json_params(cls, params: dict) -> "KeyGenerationParams": + key_list = params.get("keys") or [] + + return cls( + type=( + KeyType.from_string(params.get("type")) if params.get("type") else None + ), + fromKey=params.get("fromKey"), + threshold=params.get("threshold"), + keys=[cls.parse_json_params(k) for k in key_list], + ) diff --git a/tck/param/sdk.py b/tck/param/sdk.py new file mode 100644 index 000000000..b9ce539aa --- /dev/null +++ b/tck/param/sdk.py @@ -0,0 +1,24 @@ +from dataclasses import dataclass + +from tck.param.base import BaseParams +from tck.util.param_utils import parse_session_id + + +@dataclass +class SetupParams(BaseParams): + operatorAccountId: str = None + operatorPrivateKey: str = None + nodeIp: str | None = None + nodeAccountId: str | None = None + mirrorNetworkIp: str | None = None + + @classmethod + def parse_json_params(cls, params: dict) -> "SetupParams": + return cls( + operatorAccountId=params.get("operatorAccountId"), + operatorPrivateKey=params.get("operatorPrivateKey"), + nodeIp=params.get("nodeIp"), + nodeAccountId=params.get("nodeAccountId"), + mirrorNetworkIp=params.get("mirrorNetworkIp"), + sessionId=parse_session_id(params), + ) diff --git a/tck/protocol.py b/tck/protocol.py index dcbffdcb4..a5e5a938d 100644 --- a/tck/protocol.py +++ b/tck/protocol.py @@ -7,7 +7,7 @@ def _normalize_request_input(request_in: Any) -> Union[Dict[str, Any], JsonRpcEr """Normalize request input to a dictionary Args: request_in: Either a JSON string or a pre-parsed dict - + Returns: Parsed dictionary or JsonRpcError if parsing fails """ @@ -34,17 +34,17 @@ def _validate_json_rpc_structure(request: Dict[str, Any]) -> Optional[JsonRpcErr if not isinstance(request, dict): return JsonRpcError.invalid_request_error() - if request.get('jsonrpc') != '2.0': + if request.get("jsonrpc") != "2.0": return JsonRpcError.invalid_request_error() - if 'id' not in request: + if "id" not in request: return JsonRpcError.invalid_request_error() - method = request.get('method') + method = request.get("method") if not isinstance(method, str): return JsonRpcError.invalid_request_error() - params = request.get('params', {}) + params = request.get("params", {}) if not (isinstance(params, (dict, list)) or params is None): return JsonRpcError.invalid_request_error() @@ -55,12 +55,12 @@ def _extract_session_id(params: Any) -> Optional[str]: """Extract session ID from params if present. Args: params: Request parameters (dict, list, or None) - + Returns: Session ID if present, None otherwise """ if isinstance(params, dict): - return params.get('sessionId') + return params.get("sessionId") return None @@ -80,34 +80,39 @@ def parse_json_rpc_request(request_in: Any) -> Union[Dict[str, Any], JsonRpcErro return validation_error # Extract session ID from params - params = request.get('params', {}) + params = request.get("params", {}) session_id = _extract_session_id(params) return { - 'jsonrpc': '2.0', - 'method': request['method'], - 'params': params, - 'id': request['id'], - 'sessionId': session_id + "jsonrpc": "2.0", + "method": request["method"], + "params": params, + "id": request["id"], + "sessionId": session_id, } -def build_json_rpc_success_response(result: Any, request_id: Optional[Union[str, int]]) -> Dict[str, Any]: + +def build_json_rpc_success_response( + result: Any, request_id: Optional[Union[str, int]] +) -> Dict[str, Any]: """Build a JSON-RPC 2.0 success response.""" response = { - 'jsonrpc': '2.0', - 'id': request_id, - 'result': result, + "jsonrpc": "2.0", + "id": request_id, + "result": result, } return response -def build_json_rpc_error_response(error: JsonRpcError, - request_id: Optional[Union[str, int]]) -> Dict[str, Any]: + +def build_json_rpc_error_response( + error: JsonRpcError, request_id: Optional[Union[str, int]] +) -> Dict[str, Any]: """Build a JSON-RPC 2.0 error response.""" error_obj = error.to_dict() - + response = { - 'jsonrpc': '2.0', - 'id': request_id, - 'error': error_obj, + "jsonrpc": "2.0", + "id": request_id, + "error": error_obj, } return response diff --git a/tck/response/account.py b/tck/response/account.py new file mode 100644 index 000000000..8cd41a469 --- /dev/null +++ b/tck/response/account.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + + +@dataclass +class CreateAccountResponse: + accountId: str | None = None + status: str | None = None diff --git a/tck/response/key.py b/tck/response/key.py new file mode 100644 index 000000000..4413ef89d --- /dev/null +++ b/tck/response/key.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass, field + + +@dataclass +class KeyGenerationResponse: + key: str = None + privateKeys: list[str] = field(default_factory=list) diff --git a/tck/response/sdk.py b/tck/response/sdk.py new file mode 100644 index 000000000..3bfa835ab --- /dev/null +++ b/tck/response/sdk.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + + +@dataclass +class SetupResponse: + message: str = None + status: str = None + + def __init__(self, message: str): + self.message = message + self.status = "SUCCESS" diff --git a/tck/server.py b/tck/server.py index fa195ddd4..6d57fc9a1 100644 --- a/tck/server.py +++ b/tck/server.py @@ -1,66 +1,79 @@ """TCK Server implementation using Flask.""" + import os from dataclasses import dataclass, field import logging from flask import Flask, jsonify, request from tck.errors import JsonRpcError from tck.handlers import safe_dispatch -from tck.protocol import build_json_rpc_error_response, build_json_rpc_success_response, parse_json_rpc_request +from tck.protocol import ( + build_json_rpc_error_response, + build_json_rpc_success_response, + parse_json_rpc_request, +) + @dataclass class ServerConfig: """Configuration for the TCK server.""" + host: str = field(default_factory=lambda: os.getenv("TCK_HOST", "localhost")) - port: int = field(default_factory=lambda: _parse_port(os.getenv("TCK_PORT", "8544"))) + port: int = field( + default_factory=lambda: _parse_port(os.getenv("TCK_PORT", "8544")) + ) + def _parse_port(port_str: str) -> int: try: port = int(port_str) except ValueError as exc: - raise ValueError(f"TCK_PORT must be a valid integer, got: '{port_str}'") from exc + raise ValueError( + f"TCK_PORT must be a valid integer, got: '{port_str}'" + ) from exc if not (1 <= port <= 65535): raise ValueError(f"TCK_PORT must be between 1 and 65535, got: {port}") return port + app = Flask(__name__) logger = logging.getLogger(__name__) -@app.route("/", methods=['POST']) + +@app.route("/", methods=["POST"]) def json_rpc_endpoint(): """JSON-RPC 2.0 endpoint to handle requests.""" - if request.mimetype != 'application/json': - error = JsonRpcError.parse_error(message='Parse error: Content-Type must be application/json') + if request.mimetype != "application/json": + error = JsonRpcError.parse_error( + message="Parse error: Content-Type must be application/json" + ) return jsonify(build_json_rpc_error_response(error, None)) + try: request_json = request.get_json(force=True) except Exception: - # Malformed JSON - return parse error error = JsonRpcError.parse_error() return jsonify(build_json_rpc_error_response(error, None)) - + # Parse and validate the JSON-RPC request parsed_request = parse_json_rpc_request(request_json) if isinstance(parsed_request, JsonRpcError): # Use request id if available, else None per JSON-RPC 2.0 spec - request_id = request_json.get('id') if isinstance(request_json, dict) else None + request_id = request_json.get("id") if isinstance(request_json, dict) else None return jsonify(build_json_rpc_error_response(parsed_request, request_id)) + method_name = parsed_request["method"] + params = parsed_request["params"] + request_id = parsed_request["id"] - method_name = parsed_request['method'] - params = parsed_request['params'] - request_id = parsed_request['id'] - session_id = parsed_request.get('sessionId') - - # Safely dispatch the request to the appropriate handler - response = safe_dispatch(method_name, params, session_id, request_id) + response = safe_dispatch(method_name, params, request_id) # If the response is already an error response, return it directly - if isinstance(response, dict) and 'jsonrpc' in response and 'error' in response: + if isinstance(response, dict) and "jsonrpc" in response and "error" in response: return jsonify(response) - - # Build and return the success response + return jsonify(build_json_rpc_success_response(response, request_id)) + def start_server(config: ServerConfig | None = None): """Start the JSON-RPC server using Flask.""" if config is None: @@ -69,6 +82,5 @@ def start_server(config: ServerConfig | None = None): app.run(host=config.host, port=config.port) - if __name__ == "__main__": - start_server() \ No newline at end of file + start_server() diff --git a/tck/client_manager.py b/tck/util/client_utils.py similarity index 55% rename from tck/client_manager.py rename to tck/util/client_utils.py index 87753f833..b8ab1c8ef 100644 --- a/tck/client_manager.py +++ b/tck/util/client_utils.py @@ -1,26 +1,28 @@ -from typing import Optional, Dict from hiero_sdk_python import Client import threading -_clients: Dict[str, Client] = {} -_lock = threading.Lock() +_CLIENTS: dict[str, Client] = {} +_LOCK = threading.Lock() + def store_client(session_id: str, client: Client) -> None: """Store a client instance associated with a session ID.""" - with _lock: - old_client = _clients.get(session_id) - _clients[session_id] = client + with _LOCK: + old_client = _CLIENTS.get(session_id) + _CLIENTS[session_id] = client if old_client is not None: old_client.close() -def get_client(session_id: str) -> Optional[Client]: + +def get_client(session_id: str) -> Client | None: """Retrieve a client instance by session ID.""" - with _lock: - return _clients.get(session_id) + with _LOCK: + return _CLIENTS.get(session_id) + def remove_client(session_id: str) -> None: """Remove and close the client instance associated with a session ID.""" - with _lock: - client = _clients.pop(session_id, None) + with _LOCK: + client = _CLIENTS.pop(session_id, None) if client is not None: client.close() diff --git a/tck/util/constants.py b/tck/util/constants.py new file mode 100644 index 000000000..f200c1a67 --- /dev/null +++ b/tck/util/constants.py @@ -0,0 +1 @@ +DEFAULT_GRPC_TIMEOUT=30 diff --git a/tck/util/key_utils.py b/tck/util/key_utils.py new file mode 100644 index 000000000..dbc9eef84 --- /dev/null +++ b/tck/util/key_utils.py @@ -0,0 +1,46 @@ +from enum import Enum + +from hiero_sdk_python.crypto.key import Key +from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.crypto.public_key import PublicKey + + +class KeyType(Enum): + ED25519_PRIVATE_KEY = "ed25519PrivateKey" + ED25519_PUBLIC_KEY = "ed25519PublicKey" + ECDSA_SECP256K1_PRIVATE_KEY = "ecdsaSecp256k1PrivateKey" + ECDSA_SECP256K1_PUBLIC_KEY = "ecdsaSecp256k1PublicKey" + LIST_KEY = "keyList" + THRESHOLD_KEY = "thresholdKey" + EVM_ADDRESS_KEY = "evmAddress" + + @classmethod + def from_string(cls, key_type_str: str): + """Helper to get KeyType from string.""" + for key_type in cls: + if key_type.value == key_type_str: + return key_type + + raise ValueError(f"Unknown key type: {key_type_str}") + + +def get_key_from_string(key_string: str) -> Key: + """Helper to convert the str value to Key.""" + key_bytes = bytes.fromhex(key_string) + + try: + return Key.from_bytes(key_bytes) + except Exception: + pass + + try: + return PublicKey.from_string_der(key_string) + except Exception: + pass + + try: + return PrivateKey.from_string_der(key_string) + except Exception: + pass + + raise ValueError("Invalid key string") diff --git a/tck/util/param_utils.py b/tck/util/param_utils.py new file mode 100644 index 000000000..0be500259 --- /dev/null +++ b/tck/util/param_utils.py @@ -0,0 +1,36 @@ +def parse_session_id(params: dict) -> str: + """Parse sessionId from the json rpc params.""" + session_id = params.get("sessionId") + + if isinstance(session_id, str) and session_id != "": + return session_id + + raise ValueError("sessionId is required and must be a non-empty string") + + +def parse_common_transaction_params(params: dict): + """Parse the commonTransactionParams form json the rpc params.""" + from tck.param.common import CommonTransactionParams + + common_params = params.get("commonTransactionParams") + if common_params is None: + return None + + return CommonTransactionParams.parse_json_params( + params.get("commonTransactionParams") + ) + + +def to_int(value) -> int | None: + """Helper to convert value to int.""" + try: + return int(value) + except (TypeError, ValueError): + return None + + +def to_bool(value) -> bool | None: + """Helper to convert value to bool.""" + if isinstance(value, str): + return value.lower() == "true" + return bool(value) if value is not None else None diff --git a/tests/integration/account_create_transaction_e2e_test.py b/tests/integration/account_create_transaction_e2e_test.py index 6b476f1ad..0a69acacf 100644 --- a/tests/integration/account_create_transaction_e2e_test.py +++ b/tests/integration/account_create_transaction_e2e_test.py @@ -2,6 +2,7 @@ from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.account.account_update_transaction import AccountUpdateTransaction from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.exceptions import PrecheckError from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.query.account_info_query import AccountInfoQuery from hiero_sdk_python.response_code import ResponseCode @@ -368,3 +369,18 @@ def test_proto_excludes_alias_when_not_set(env): assert not tx_body.cryptoCreateAccount.alias # Key must still be present assert tx_body.cryptoCreateAccount.key == account_public_key._to_proto() + + +def test_create_account_with_negative_initial_balance(env): + """Test create_account_transaction raise precheck-error for negative initial balance""" + public_key = PrivateKey.generate_ed25519().public_key() + tx = ( + AccountCreateTransaction() + .set_key_without_alias(public_key) + .set_initial_balance(-1) + ) + + with pytest.raises(PrecheckError) as e: + tx.execute(env.client) + + assert e.value.status == ResponseCode.INVALID_INITIAL_BALANCE diff --git a/tests/unit/tck/__init__.py b/tests/tck/__init__.py similarity index 100% rename from tests/unit/tck/__init__.py rename to tests/tck/__init__.py diff --git a/tests/unit/tck/client_manager_test.py b/tests/tck/client_utils_test.py similarity index 97% rename from tests/unit/tck/client_manager_test.py rename to tests/tck/client_utils_test.py index 9c1d852af..9ec7df376 100644 --- a/tests/unit/tck/client_manager_test.py +++ b/tests/tck/client_utils_test.py @@ -1,16 +1,16 @@ """Unit tests for the client manager module.""" from unittest.mock import MagicMock import pytest -from tck.client_manager import store_client, get_client, remove_client, _clients +from tck.util.client_utils import store_client, get_client, remove_client, _CLIENTS pytestmark = pytest.mark.unit @pytest.fixture(autouse=True) def clear_clients(): """Clear the clients registry before each test.""" - _clients.clear() + _CLIENTS.clear() yield - _clients.clear() + _CLIENTS.clear() class TestClientManager: diff --git a/tests/tck/common_params_test.py b/tests/tck/common_params_test.py new file mode 100644 index 000000000..83ac633b5 --- /dev/null +++ b/tests/tck/common_params_test.py @@ -0,0 +1,70 @@ +from unittest.mock import MagicMock + +import pytest + +from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction +from hiero_sdk_python.client.client import Client +from hiero_sdk_python.crypto.private_key import PrivateKey +from tck.param.common import CommonTransactionParams + + +pytestmark = pytest.mark.unit + + +@pytest.fixture +def json_params_dict(): + return { + 'transactionId': "0.0.90@1773846665.518000566", + "maxTransactionFee": "100000000", + "validTransactionDuration": "120", + "memo": "Test Memo", + "regenerateTransactionId": "False", + "signers": ["30540201010420d0b3d3c266ad9aa414f41e3050d64f4012765abc94a745cbd0607bf41da51a96a00706052b8104000aa124032200037aa11171d538daf5c624f313bc106fff289e4a24768880d0fa71dd302a1fa9e7"] + } + + +def test_parse_common_params(json_params_dict): + """Test the commonTransaction params can be parse form dict""" + params = CommonTransactionParams.parse_json_params(json_params_dict) + + assert isinstance(params.transactionId, str) + assert params.transactionId == "0.0.90@1773846665.518000566" + + assert isinstance(params.maxTransactionFee, int) + assert params.maxTransactionFee == 100000000 + + assert isinstance(params.memo, str) + assert params.memo == "Test Memo" + + assert isinstance(params.validTransactionDuration, int) + assert params.validTransactionDuration == 120 + + assert isinstance(params.regenerateTransactionId, bool) + assert params.regenerateTransactionId == False + + assert isinstance(params.signers, list) + assert len(params.signers) == 1 + assert params.signers[0] == "30540201010420d0b3d3c266ad9aa414f41e3050d64f4012765abc94a745cbd0607bf41da51a96a00706052b8104000aa124032200037aa11171d538daf5c624f313bc106fff289e4a24768880d0fa71dd302a1fa9e7" + + +def test_apply_common_params(json_params_dict): + """Test commonTransactionParams can be apply to transaction""" + params = CommonTransactionParams.parse_json_params(json_params_dict) + tx = ( + AccountCreateTransaction() + .set_key_without_alias(PrivateKey.generate()) + ) + + client = MagicMock(spec=Client) + + tx.freeze_with = MagicMock() + tx.sign = MagicMock() + + params.apply_common_params(tx, client) + + assert tx.memo == "Test Memo" + assert tx.transaction_valid_duration == 120 + assert str(tx.transaction_id) == "0.0.90@1773846665.518000566" + + tx.freeze_with.assert_called_once_with(client) + assert tx.sign.call_count == 1 \ No newline at end of file diff --git a/tests/unit/tck/conftest.py b/tests/tck/conftest.py similarity index 100% rename from tests/unit/tck/conftest.py rename to tests/tck/conftest.py diff --git a/tests/tck/handlers_test.py b/tests/tck/handlers_test.py new file mode 100644 index 000000000..06e2b7882 --- /dev/null +++ b/tests/tck/handlers_test.py @@ -0,0 +1,286 @@ +"""Test cases for the Hiero SDK TCK handlers registry and dispatch functionality.""" + +import pytest +from unittest.mock import MagicMock +from dataclasses import dataclass + +from hiero_sdk_python.exceptions import ( + PrecheckError, + ReceiptStatusError, + MaxAttemptsError, +) + +from tck.handlers.registry import ( + rpc_method, + get_handler, + get_all_handlers, + dispatch, + safe_dispatch, + parse_result, +) + +from tck.errors import ( + JsonRpcError, + METHOD_NOT_FOUND, + INTERNAL_ERROR, + HIERO_ERROR, + INVALID_PARAMS, +) + +from tck.handlers import registry + +pytestmark = pytest.mark.unit + + +@pytest.fixture(autouse=True) +def clear_handlers(): + """Clear the handlers registry before each test.""" + registry._HANDLERS.clear() + yield + registry._HANDLERS.clear() + + +class DummyParams: + """Mock params class used for dispatch tests.""" + + @classmethod + def parse_json_params(cls, params): + return params + + +@dataclass +class DummyResult: + """Dataclass used to test parse_result.""" + value: int | None = None + other: str | None = None + + +class TestHandlerRegistration: + + def test_handler_registration_via_decorator(self): + """Test that @rpc_method decorator registers handler.""" + + @rpc_method("test_method") + def handler(params: DummyParams): + return {"status": "ok"} + + handler_fn = get_handler("test_method") + + assert handler_fn is not None, "Expected handler to be registered" + + result = handler_fn({}) + assert result == {"status": "ok"}, "Expected handler result" + + + def test_get_all_handlers(self): + """Test retrieving all handlers.""" + + @rpc_method("method1") + def handler1(params: DummyParams): + return "result1" + + @rpc_method("method2") + def handler2(params: DummyParams): + return "result2" + + handlers = get_all_handlers() + + assert len(handlers) == 2, "Expected two handlers" + assert "method1" in handlers, "method1 missing" + assert "method2" in handlers, "method2 missing" + + + def test_get_nonexistent_handler_returns_none(self): + """Nonexistent handler should return None.""" + assert get_handler("missing") is None, "Expected None" + + + def test_handler_override(self): + """Second registration should overwrite first.""" + + @rpc_method("override") + def first(params: DummyParams): + return "first" + + @rpc_method("override") + def second(params: DummyParams): + return "second" + + handler = get_handler("override") + + assert handler({}) == "second", "Override failed" + + + def test_get_all_handlers_returns_copy(self): + """Returned dict should not modify registry.""" + + @rpc_method("protected") + def handler(params: DummyParams): + return "ok" + + handlers = get_all_handlers() + + handlers["protected"] = lambda: "bad" + handlers["injected"] = lambda: "bad" + + assert get_handler("protected")({}) == "ok", "Registry mutated" + assert get_handler("injected") is None, "Unexpected handler injected" + + +class TestDispatch: + + def test_dispatch_success(self): + """Dispatch should call registered handler.""" + + @rpc_method("setup") + def handler(params: DummyParams): + return DummyResult(value=1, other="value") + + params = DummyParams() + result = dispatch("setup", params) + + + assert result == {"value": 1, "other": "value"}, "Dispatch failed" + + + def test_dispatch_unknown_method(self): + """Unknown method should raise METHOD_NOT_FOUND.""" + + with pytest.raises(JsonRpcError) as excinfo: + dispatch("missing", DummyParams()) + + assert excinfo.value.code == METHOD_NOT_FOUND, "Expected METHOD_NOT_FOUND" + + + def test_dispatch_reraises_json_rpc_error(self): + """JsonRpcError should pass through.""" + + @rpc_method("error") + def handler(params: DummyParams): + raise JsonRpcError.invalid_params_error() + + with pytest.raises(JsonRpcError) as excinfo: + dispatch("error", DummyParams()) + + assert excinfo.value.code == INVALID_PARAMS, "Expected INVALID_PARAMS" + + + def test_dispatch_converts_generic_exception(self): + """Generic exception → INTERNAL_ERROR.""" + + @rpc_method("crash") + def handler(params: DummyParams): + raise ValueError("Boom") + + with pytest.raises(JsonRpcError) as excinfo: + dispatch("crash", DummyParams()) + + assert excinfo.value.code == INTERNAL_ERROR, "Expected INTERNAL_ERROR" + + + def test_dispatch_converts_precheck_error(self): + """PrecheckError → HIERO_ERROR.""" + + @rpc_method("precheck") + def handler(params: DummyParams): + raise PrecheckError( + status=1, + transaction_id="0.0.1", + message="failure", + ) + + with pytest.raises(JsonRpcError) as excinfo: + dispatch("precheck", DummyParams()) + + assert excinfo.value.code == HIERO_ERROR, "Expected HIERO_ERROR" + + + def test_dispatch_converts_receipt_status_error(self): + """ReceiptStatusError → HIERO_ERROR.""" + + @rpc_method("receipt") + def handler(params: DummyParams): + raise ReceiptStatusError( + status=1, + transaction_id=None, + transaction_receipt=MagicMock(), + message="fail", + ) + + with pytest.raises(JsonRpcError) as excinfo: + dispatch("receipt", DummyParams()) + + assert excinfo.value.code == HIERO_ERROR, "Expected HIERO_ERROR" + + + def test_dispatch_converts_max_attempts_error(self): + """MaxAttemptsError → HIERO_ERROR.""" + + @rpc_method("max_attempts") + def handler(params: DummyParams): + raise MaxAttemptsError("fail", node_id="0.0.1") + + with pytest.raises(JsonRpcError) as excinfo: + dispatch("max_attempts", DummyParams()) + + assert excinfo.value.code == HIERO_ERROR, "Expected HIERO_ERROR" + + +class TestSafeDispatch: + + def test_safe_dispatch_success(self): + """safe_dispatch should return raw result.""" + + @rpc_method("success") + def handler(params: DummyParams): + return DummyResult(value=1, other="value") + + result = safe_dispatch("success", {}, 1) + + assert result == {"value":1, "other":"value"}, "Unexpected result" + + + def test_safe_dispatch_json_error(self): + """JsonRpcError should produce JSON-RPC error response.""" + + @rpc_method("json_error") + def handler(params: DummyParams): + raise JsonRpcError.invalid_params_error(data="field") + + response = safe_dispatch("json_error", {}, 10) + + assert response["error"]["code"] == INVALID_PARAMS, "Expected INVALID_PARAMS" + + + def test_safe_dispatch_generic_exception(self): + """Generic exception → INTERNAL_ERROR.""" + + @rpc_method("generic_error") + def handler(params: DummyParams): + raise RuntimeError("unexpected") + + response = safe_dispatch("generic_error", {}, 2) + + assert response["error"]["code"] == INTERNAL_ERROR, "Expected INTERNAL_ERROR" + + + +class TestParseResult: + + def test_parse_result_dataclass(self): + """Dataclass should convert to dict.""" + + result = DummyResult(value=10, other="other") + + parsed = parse_result(result) + + assert parsed == {"value": 10, "other": "other"}, "Expected filtered dataclass result" + + def test_parse_result_dataclass_ignore_none(self): + """Dataclass should convert to dict without None values.""" + + result = DummyResult(value=10, other=None) + + parsed = parse_result(result) + + assert parsed == {"value": 10}, "Expected filtered dataclass result" \ No newline at end of file diff --git a/tests/unit/tck/protocol_test.py b/tests/tck/protocol_test.py similarity index 100% rename from tests/unit/tck/protocol_test.py rename to tests/tck/protocol_test.py diff --git a/tests/unit/account_create_transaction_test.py b/tests/unit/account_create_transaction_test.py index 1c894d9a7..ac8d2365d 100644 --- a/tests/unit/account_create_transaction_test.py +++ b/tests/unit/account_create_transaction_test.py @@ -544,3 +544,32 @@ def test_set_key_with_alias_then_without_alias_overrides_alias(): tx.set_key_without_alias(account_private_key) assert tx.alias is None + +def test_set_stack_node_id_reset_stake_account_id(): + """Test set_node_id reset stake_account_id if stake account id is set.""" + tx = ( + AccountCreateTransaction() + .set_staked_account_id(AccountId(0,0,1)) + ) + + assert tx.staked_account_id == AccountId(0,0,1) + assert tx.staked_node_id == None + + tx.set_staked_node_id(1) + + assert tx.staked_node_id == 1 + assert tx.staked_account_id is None + +def test_set_stake_account_id_reset_stake_node_id(): + """Test set_account_id reset stake_node_id if node id is set.""" + tx = ( + AccountCreateTransaction() + .set_staked_node_id(1) + ) + + assert tx.staked_node_id == 1 + assert tx.staked_account_id is None + + tx.set_staked_account_id(AccountId(0,0,1)) + assert tx.staked_account_id == AccountId(0,0,1) + assert tx.staked_node_id is None diff --git a/tests/unit/contract_id_test.py b/tests/unit/contract_id_test.py index 67cec8a33..4886d569c 100644 --- a/tests/unit/contract_id_test.py +++ b/tests/unit/contract_id_test.py @@ -7,6 +7,7 @@ import pytest from hiero_sdk_python.contract.contract_id import ContractId +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.hapi.services import basic_types_pb2 pytestmark = pytest.mark.unit @@ -568,3 +569,14 @@ def test_populate_contract_num_invalid_mirror_response(client): ValueError, match="Mirror node response missing 'contract_id'" ): contract_id.populate_contract_num(client) + +def test_to_proto_key(): + """Test to_proto_key returns the Key protobuf.""" + contract_id = ContractId(shard=0, realm=0, contract=1) + key = contract_id.to_proto_key() + + assert key is not None + assert key.contractID is not None + assert key.contractID.shardNum == contract_id.shard + assert key.contractID.realmNum == contract_id.realm + assert key.contractID.contractNum == contract_id.contract diff --git a/tests/unit/delegate_contract_id.py b/tests/unit/delegate_contract_id.py new file mode 100644 index 000000000..4c14920ac --- /dev/null +++ b/tests/unit/delegate_contract_id.py @@ -0,0 +1,609 @@ +""" +Unit tests for the DelegateContractId class. +""" + +import struct +from unittest.mock import patch +import pytest + +from hiero_sdk_python.contract.delegate_contract_id import DelegateContractId +from hiero_sdk_python.crypto.key import Key +from hiero_sdk_python.hapi.services import basic_types_pb2 + +pytestmark = pytest.mark.unit + + +@pytest.fixture +def client(mock_client): + mock_client.network.ledger_id = bytes.fromhex("00") # mainnet ledger id + return mock_client + + +def test_default_initialization(): + """Test DelegateContractId initialization with default values.""" + contract_id = DelegateContractId() + + assert isinstance(contract_id, DelegateContractId) + assert contract_id.shard == 0 + assert contract_id.realm == 0 + assert contract_id.contract == 0 + assert contract_id.evm_address is None + assert contract_id.checksum is None + + +def test_custom_initialization(): + """Test DelegateContractId initialization with custom values.""" + contract_id = DelegateContractId(shard=1, realm=2, contract=3) + + assert isinstance(contract_id, DelegateContractId) + assert contract_id.shard == 1 + assert contract_id.realm == 2 + assert contract_id.contract == 3 + assert contract_id.evm_address is None + assert contract_id.checksum is None + + +def test_str_representation(): + """Test string representation of DelegateContractId.""" + contract_id = DelegateContractId(shard=1, realm=2, contract=3) + + assert isinstance(contract_id, DelegateContractId) + assert str(contract_id) == "1.2.3" + assert contract_id.evm_address is None + assert contract_id.checksum is None + + +def test_str_representation_default(): + """Test string representation of DelegateContractId with default values.""" + contract_id = DelegateContractId() + + assert isinstance(contract_id, DelegateContractId) + assert str(contract_id) == "0.0.0" + assert contract_id.evm_address is None + assert contract_id.checksum is None + + +@pytest.mark.parametrize( + "contract_str, expected", + [ + ("1.2.101", (1, 2, 101, None, None)), + ("0.0.0", (0, 0, 0, None, None)), + ("1.2.3-abcde", (1, 2, 3, None, "abcde")), + ( + "1.2.abcdef0123456789abcdef0123456789abcdef01", + (1, 2, 0, bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01"), None), + ), + ], +) +def test_from_string_for_valid_str(contract_str, expected): + """Test creating DelegateContractId from valid string format.""" + shard, realm, contract, evm_address, checksum = expected + + contract_id = DelegateContractId.from_string(contract_str) + + assert isinstance(contract_id, DelegateContractId) + assert contract_id.shard == shard + assert contract_id.realm == realm + assert contract_id.contract == contract + assert contract_id.evm_address == evm_address + assert contract_id.checksum == checksum + + +@pytest.mark.parametrize( + "invalid_id", + [ + "1.2", # Too few parts + "1.2.3.4", # Too many parts + "a.b.c", # Non-numeric parts + "", # Empty string + "1.a.3", # Partial numeric + "0.0.-1", + "abc.def.ghi", + "0.0.1-ad", + "0.0.1-addefgh", + "0.0.1 - abcde", + " 0.0.100 ", + " 1.2.abcdef0123456789abcdef0123456789abcdef01 ", + "1.2.0xabcdef0123456789abcdef0123456789abcdef01", + "1.2.001122334455667788990011223344556677", + "1.2.000000000000000000000000000000000000000000", + ], +) +def test_from_string_for_invalid_format(invalid_id): + """Should raise error when creating DelegateContractId from invalid string input.""" + with pytest.raises( + ValueError, + match=f"Invalid contract ID string '{invalid_id}'. Expected format 'shard.realm.contract'.", + ): + DelegateContractId.from_string(invalid_id) + + +@pytest.mark.parametrize("invalid_id", [None, 123, True, object, {}]) +def test_from_string_for_invalid_type(invalid_id): + """Should raise error when creating DelegateContractId from invalid input type.""" + with pytest.raises( + TypeError, + match=f"contract_id_str must be of type str, got {type(invalid_id).__name__}", + ): + DelegateContractId.from_string(invalid_id) + + +def test_to_proto(): + """Test converting DelegateContractId to protobuf format.""" + contract_id = DelegateContractId(shard=1, realm=2, contract=3) + proto = contract_id._to_proto() + + assert isinstance(proto, basic_types_pb2.ContractID) + assert proto.shardNum == 1 + assert proto.realmNum == 2 + assert proto.contractNum == 3 + + +def test_to_proto_default_values(): + """Test converting DelegateContractId with default values to protobuf format.""" + contract_id = DelegateContractId() + proto = contract_id._to_proto() + + assert isinstance(proto, basic_types_pb2.ContractID) + assert proto.shardNum == 0 + assert proto.realmNum == 0 + assert proto.contractNum == 0 + + +def test_from_proto(): + """Test creating DelegateContractId from protobuf format.""" + proto = basic_types_pb2.ContractID(shardNum=1, realmNum=2, contractNum=3) + + contract_id = DelegateContractId._from_proto(proto) + + assert isinstance(contract_id, DelegateContractId) + assert contract_id.shard == 1 + assert contract_id.realm == 2 + assert contract_id.contract == 3 + assert contract_id.evm_address is None + + +def test_from_proto_with_evm_address(): + """Test creating DelegateContractId from protobuf with EVM address set.""" + evm_address = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + proto = basic_types_pb2.ContractID( + shardNum=1, + realmNum=2, + evm_address=evm_address, + ) + + contract_id = DelegateContractId._from_proto(proto) + + assert isinstance(contract_id, DelegateContractId) + assert contract_id.shard == 1 + assert contract_id.realm == 2 + assert contract_id.contract == 0 + assert contract_id.evm_address == evm_address + + +def test_from_proto_zero_values(): + """Test creating DelegateContractId from protobuf format with zero values.""" + proto = basic_types_pb2.ContractID(shardNum=0, realmNum=0, contractNum=0) + + contract_id = DelegateContractId._from_proto(proto) + + assert isinstance(contract_id, DelegateContractId) + assert contract_id.shard == 0 + assert contract_id.realm == 0 + assert contract_id.contract == 0 + assert contract_id.evm_address is None + + +def test_roundtrip_proto_conversion(): + """Test that converting to proto and back preserves values.""" + original = DelegateContractId(shard=5, realm=10, contract=15) + proto = original._to_proto() + reconstructed = DelegateContractId._from_proto(proto) + + assert original.shard == reconstructed.shard + assert original.realm == reconstructed.realm + assert original.contract == reconstructed.contract + + +def test_roundtrip_string_conversion(): + """Test that converting to string and back preserves values.""" + original = DelegateContractId(shard=7, realm=14, contract=21) + string_repr = str(original) + reconstructed = DelegateContractId.from_string(string_repr) + + assert original.shard == reconstructed.shard + assert original.realm == reconstructed.realm + assert original.contract == reconstructed.contract + + +def test_equality(): + """Test DelegateContractId equality comparison.""" + contract_id1 = DelegateContractId(shard=1, realm=2, contract=3) + contract_id2 = DelegateContractId(shard=1, realm=2, contract=3) + contract_id3 = DelegateContractId(shard=1, realm=2, contract=4) + + assert contract_id1 == contract_id2 + assert contract_id1 != contract_id3 + + +def test_evm_address_initialization(): + """Test DelegateContractId initialization with EVM address.""" + evm_address = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + contract_id = DelegateContractId( + shard=1, realm=2, contract=3, evm_address=evm_address + ) + + assert contract_id.shard == 1 + assert contract_id.realm == 2 + assert contract_id.contract == 3 + assert contract_id.evm_address == evm_address + + +def test_evm_address_to_proto(): + """Test converting DelegateContractId with EVM address to protobuf format.""" + evm_address = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + contract_id = DelegateContractId( + shard=1, realm=2, contract=3, evm_address=evm_address + ) + proto = contract_id._to_proto() + + assert isinstance(proto, basic_types_pb2.ContractID) + assert proto.shardNum == 1 + assert proto.realmNum == 2 + assert proto.contractNum == 0 + assert proto.evm_address == evm_address + + +def test_evm_address_to_proto_none(): + """Test converting DelegateContractId with None EVM address to protobuf format.""" + contract_id = DelegateContractId(shard=1, realm=2, contract=3, evm_address=None) + proto = contract_id._to_proto() + + assert isinstance(proto, basic_types_pb2.ContractID) + assert proto.shardNum == 1 + assert proto.realmNum == 2 + assert proto.contractNum == 3 + assert proto.evm_address == b"" + + +def test_evm_address_equality(): + """Test DelegateContractId equality with EVM addresses.""" + evm_address1 = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + evm_address2 = bytes.fromhex("1234567890abcdef1234567890abcdef12345678") + + contract_id1 = DelegateContractId( + shard=1, realm=2, contract=3, evm_address=evm_address1 + ) + contract_id2 = DelegateContractId( + shard=1, realm=2, contract=3, evm_address=evm_address1 + ) + contract_id3 = DelegateContractId( + shard=1, realm=2, contract=3, evm_address=evm_address2 + ) + contract_id4 = DelegateContractId(shard=1, realm=2, contract=3, evm_address=None) + + # Same EVM address should be equal + assert contract_id1 == contract_id2 + + # Different EVM addresses should not be equal + assert contract_id1 != contract_id3 + + # None EVM address should not be equal to one with EVM address + assert contract_id1 != contract_id4 + + +def test_evm_address_hash(): + """Test DelegateContractId hash with EVM addresses.""" + evm_address1 = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + evm_address2 = bytes.fromhex("1234567890abcdef1234567890abcdef12345678") + + contract_id1 = DelegateContractId( + shard=1, realm=2, contract=3, evm_address=evm_address1 + ) + contract_id2 = DelegateContractId( + shard=1, realm=2, contract=3, evm_address=evm_address1 + ) + contract_id3 = DelegateContractId( + shard=1, realm=2, contract=3, evm_address=evm_address2 + ) + + # Same EVM address should have same hash + assert hash(contract_id1) == hash(contract_id2) + + # Different EVM addresses should have different hashes + assert hash(contract_id1) != hash(contract_id3) + + +def test_to_evm_address(): + """Test DelegateContractId.to_evm_address() for both explicit and computed EVM addresses.""" + # Explicit EVM address + evm_address = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + contract_id = DelegateContractId( + shard=1, realm=2, contract=3, evm_address=evm_address + ) + assert contract_id.to_evm_address() == evm_address.hex() + + # Computed EVM address (no explicit evm_address) + contract_id = DelegateContractId(shard=1, realm=2, contract=3) + expected_bytes = struct.pack( + ">iqq", contract_id.shard, contract_id.realm, contract_id.contract + ) + assert contract_id.to_evm_address() == expected_bytes.hex() + + # Default values + contract_id = DelegateContractId() + expected_bytes = struct.pack( + ">iqq", contract_id.shard, contract_id.realm, contract_id.contract + ) + assert contract_id.to_evm_address() == expected_bytes.hex() + + +def test_str_representation_with_checksum(client): + """Should return string representation with checksum""" + contract_id = DelegateContractId.from_string("0.0.1") + assert contract_id.to_string_with_checksum(client) == "0.0.1-dfkxr" + + +def test_str_representation_checksum_with_evm_address(client): + """Should raise error on to_string_with_checksum is called when evm_address is set""" + contract_id = DelegateContractId.from_string( + "0.0.abcdef0123456789abcdef0123456789abcdef01" + ) + + with pytest.raises( + ValueError, + match="to_string_with_checksum cannot be applied to DelegateContractId with evm_address", + ): + contract_id.to_string_with_checksum(client) + + +def test_validate_checksum_success(client): + """Should pass checksum validation when checksum is correct.""" + contract_id = DelegateContractId.from_string("0.0.1-dfkxr") + contract_id.validate_checksum(client) + + +def test_validate_checksum_failure(client): + """Should raise ValueError if checksum validation fails.""" + contract_id = DelegateContractId.from_string("0.0.1-wronx") + + with pytest.raises(ValueError, match="Checksum mismatch for 0.0.1"): + contract_id.validate_checksum(client) + + +def test_str_representation_with_evm_address(): + """Should return str representing with evm_address""" + contract_id = DelegateContractId.from_string( + "0.0.abcdef0123456789abcdef0123456789abcdef01" + ) + assert contract_id.__str__() == "0.0.abcdef0123456789abcdef0123456789abcdef01" + + +def test_contract_id_repr_numeric(): + """Test __repr__ output for numeric contract ID.""" + contract_id = DelegateContractId(0, 0, 12345) + expected = "DelegateContractId(shard=0, realm=0, contract=12345)" + assert repr(contract_id) == expected + + +def test_contract_id_repr_evm_address(): + """Test __repr__ output for EVM-based contract ID.""" + evm_bytes = bytes.fromhex("a" * 40) + contract_id = DelegateContractId(1, 2, evm_address=evm_bytes) + expected = f"DelegateContractId(shard=1, realm=2, evm_address={evm_bytes.hex()})" + assert repr(contract_id) == expected + + +def test_to_string_with_checksum_missing_ledger_id(mock_client): + """Should raise error if client has no ledger ID.""" + mock_client.network.ledger_id = None + contract_id = DelegateContractId.from_string("0.0.1") + + with pytest.raises(ValueError, match="Missing ledger ID"): + contract_id.to_string_with_checksum(mock_client) + + +@pytest.mark.parametrize( + "evm_address_str, expected", + [ + ( + "abcdef0123456789abcdef0123456789abcdef01", + (0, 0, 0, bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01")), + ), + ( + "0xabcdef0123456789abcdef0123456789abcdef01", + (0, 0, 0, bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01")), + ), + ], +) +def test_from_evm_address_valid_params(evm_address_str, expected): + """Test from_evm_address with valid EVM address strings.""" + shard, realm, contract, evm_address = expected + + contract_id = DelegateContractId.from_evm_address(0, 0, evm_address_str) + + assert isinstance(contract_id, DelegateContractId) + assert contract_id.shard == shard + assert contract_id.realm == realm + assert contract_id.contract == contract + assert contract_id.evm_address == evm_address + assert contract_id.checksum is None + + +@pytest.mark.parametrize( + "invalid_address", + [ + "abcdef0123456789abcdef0123456789abcdef", # less than 20 bytes + "abcdef0123456789abcdef0123456789abcdef1010101", # greater than 20 bytes + "abcd-123sjd", # invalid format + "0xZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ", # invalid hex + ], +) +def test_from_evm_address_invalid_evm_address_str(invalid_address): + """Test from_evm_address raise error for invalid EVM address strings.""" + with pytest.raises(ValueError, match=f"Invalid EVM address: {invalid_address}"): + DelegateContractId.from_evm_address(0, 0, invalid_address) + + +@pytest.mark.parametrize( + "invalid_address", + [None, 1234, True, object, {}], +) +def test_from_evm_address_invalid_evm_address_type(invalid_address): + """Test from_evm_address raise error for non-string EVM address inputs.""" + with pytest.raises( + TypeError, + match=f"evm_address must be of type str, got {type(invalid_address).__name__}", + ): + DelegateContractId.from_evm_address(0, 0, invalid_address) + + +@pytest.mark.parametrize( + "invalid_shard", + [None, "123", True, object, {}], +) +def test_from_evm_address_invalid_shard_type(invalid_shard): + """Test from_evm_address raise error for invalid shard types.""" + with pytest.raises( + TypeError, match=f"shard must be int, got {type(invalid_shard).__name__}" + ): + DelegateContractId.from_evm_address( + invalid_shard, 0, "abcdef0123456789abcdef0123456789abcdef01" + ) + + +def test_from_evm_address_negative_shard_value(): + """Test from_evm_address raise error for negative shard values.""" + with pytest.raises(ValueError, match="shard must be a non-negative integer"): + DelegateContractId.from_evm_address( + -1, 0, "abcdef0123456789abcdef0123456789abcdef01" + ) + + +@pytest.mark.parametrize( + "invalid_realm", + [None, "123", True, object, {}], +) +def test_from_evm_address_invalid_realm_type(invalid_realm): + """Test from_evm_address raise error for invalid realm types.""" + with pytest.raises( + TypeError, match=f"realm must be int, got {type(invalid_realm).__name__}" + ): + DelegateContractId.from_evm_address( + 0, invalid_realm, "abcdef0123456789abcdef0123456789abcdef01" + ) + + +def test_from_evm_address_negative_realm_value(): + """Test from_evm_address raise error for negative realm values.""" + with pytest.raises(ValueError, match="realm must be a non-negative integer"): + DelegateContractId.from_evm_address( + 0, -1, "abcdef0123456789abcdef0123456789abcdef01" + ) + + +def test_from_bytes_success(): + """Should deserialize DelegateContractId correctly from protobuf bytes.""" + original = DelegateContractId(shard=1, realm=2, contract=3) + data = original.to_bytes() + + reconstructed = DelegateContractId.from_bytes(data) + + assert reconstructed == original + + +@pytest.mark.parametrize("invalid_data", [None, "abc", 123, object()]) +def test_from_bytes_invalid_type(invalid_data): + """Should raise TypeError when from_bytes receives non-bytes input.""" + with pytest.raises(TypeError, match="data must be bytes"): + DelegateContractId.from_bytes(invalid_data) + + +def test_from_bytes_invalid_payload(): + """Should raise ValueError when protobuf deserialization fails.""" + with pytest.raises(ValueError, match="Failed to deserialize ContractId from bytes"): + DelegateContractId.from_bytes(b"\x00\x01\x02") + + +def test_populate_contract_num_success(client): + """Should populate contract number using mirror node response.""" + evm_address = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + contract_id = DelegateContractId(shard=0, realm=0, evm_address=evm_address) + + with patch( + "hiero_sdk_python.contract.contract_id.perform_query_to_mirror_node", + return_value={"contract_id": "0.0.123"}, + ): + populated = contract_id.populate_contract_num(client) + + assert populated.contract == 123 + assert populated.evm_address == evm_address + + +def test_populate_contract_num_invalid_response(client): + """Should raise error when populating contract number invalid response.""" + evm_address = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + contract_id = DelegateContractId(shard=0, realm=0, evm_address=evm_address) + + with patch( + "hiero_sdk_python.contract.contract_id.perform_query_to_mirror_node", + return_value={"contract_id": "invalid.account.format"}, + ): + with pytest.raises( + ValueError, + match="Invalid contract_id format received: invalid.account.format", + ): + contract_id.populate_contract_num(client) + + +def test_populate_contract_num_query_fails(client): + """Should raise error when populating contract number query fails.""" + evm_address = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + contract_id = DelegateContractId(shard=0, realm=0, evm_address=evm_address) + + with patch( + "hiero_sdk_python.contract.contract_id.perform_query_to_mirror_node", + side_effect=RuntimeError("mirror node query error"), + ): + with pytest.raises( + RuntimeError, + match="Failed to populate contract num from mirror node for evm_address abcdef0123456789abcdef0123456789abcdef01", + ): + contract_id.populate_contract_num(client) + + +def test_populate_contract_num_without_evm_address(client): + """Should raise error when populate_contract_num is called without evm_address.""" + contract_id = DelegateContractId(shard=0, realm=0, contract=1) + + with pytest.raises( + ValueError, match="evm_address is required to populate the contract number" + ): + contract_id.populate_contract_num(client) + + +def test_populate_contract_num_invalid_mirror_response(client): + """Should raise error if mirror node response is missing contract_id.""" + evm_address = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + contract_id = DelegateContractId(shard=0, realm=0, evm_address=evm_address) + + with patch( + "hiero_sdk_python.contract.contract_id.perform_query_to_mirror_node", + return_value={}, + ): + with pytest.raises( + ValueError, match="Mirror node response missing 'contract_id'" + ): + contract_id.populate_contract_num(client) + + +def test_to_proto_key(): + """Test to_proto_key returns the Key protobuf.""" + contract_id = DelegateContractId(shard=0, realm=0, contract=1) + key = contract_id.to_proto_key() + + assert key is not None + assert key.delegatable_contract_id is not None + assert key.delegatable_contract_id.shardNum == contract_id.shard + assert key.delegatable_contract_id.realmNum == contract_id.realm + assert key.delegatable_contract_id.contractNum == contract_id.contract diff --git a/tests/unit/evm_address_test.py b/tests/unit/evm_address_test.py index 82cba0cf5..61c565276 100644 --- a/tests/unit/evm_address_test.py +++ b/tests/unit/evm_address_test.py @@ -1,3 +1,5 @@ +import re + import pytest from hiero_sdk_python.crypto.evm_address import EvmAddress @@ -59,3 +61,12 @@ def test_equality(): addr2 = EvmAddress.from_string("0x" + raw.hex()) assert addr1 == addr2 + + +def test_to_proto_key(): + "Test to_proto_key raises error when call." + raw = bytes(range(20)) + address = EvmAddress.from_bytes(raw) + + with pytest.raises(RuntimeError, match=re.escape("to_proto_key() not implemented for EvmAddress")): + address.to_proto_key() diff --git a/tests/unit/key_list_test.py b/tests/unit/key_list_test.py new file mode 100644 index 000000000..e3b584383 --- /dev/null +++ b/tests/unit/key_list_test.py @@ -0,0 +1,213 @@ +import pytest + +from hiero_sdk_python.crypto.key_list import KeyList +from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.crypto.key import Key +from hiero_sdk_python.hapi.services import basic_types_pb2 + +pytestmark = pytest.mark.unit + + +def make_keys(n=3): + """Helper to generate a list of public keys.""" + keys = [] + for _ in range(n): + priv = PrivateKey.generate("ed25519") + keys.append(priv.public_key()) + return keys + + +def test_create_keylist_no_threshold(): + """Test create a KeyList without a threshold.""" + keys = make_keys(3) + kl = KeyList(keys) + + assert kl.threshold is None + assert len(kl.keys) == 3 + + +def test_create_keylist_with_threshold(): + """Test create a KeyList with a threshold.""" + keys = make_keys(3) + kl = KeyList(keys, threshold=2) + + assert kl.threshold == 2 + assert len(kl.keys) == 3 + + +def test_create_keylist_empty(): + """Test KeyList can be created without providing keys.""" + kl = KeyList() + + assert kl.threshold is None + assert kl.keys == [] + + +def test_constructor_type_check_keys(): + """Test constructor should reject non-list keys.""" + with pytest.raises(TypeError, match="keys must be a list"): + KeyList("not-a-list") + + +def test_constructor_type_check_key_elements(): + """Test constructor should reject non-Key objects in list.""" + with pytest.raises(TypeError, match="instances of Key"): + KeyList([1, 2, 3]) + + +def test_constructor_threshold_type_check(): + """Test constructor should reject non-int threshold.""" + with pytest.raises(TypeError, match="threshold must be an integer"): + KeyList(make_keys(2), threshold="two") + + +def test_add_key(): + """Test add_key appends a key and returns self.""" + kl = KeyList() + + key = PrivateKey.generate("ed25519").public_key() + returned = kl.add_key(key) + + assert returned is kl + assert kl.keys == [key] + + +def test_add_key_type_check(): + """Test add_key should reject non-Key objects.""" + kl = KeyList() + + with pytest.raises(TypeError, match="instances of Key"): + kl.add_key("not-a-key") + + +def test_set_keys(): + """Test set_keys replaces existing keys.""" + kl = KeyList(make_keys(2)) + + new_keys = make_keys(3) + returned = kl.set_keys(new_keys) + + assert returned is kl + assert kl.keys == new_keys + + +def test_set_keys_type_check(): + """Test set_keys should reject non-list input.""" + kl = KeyList() + + with pytest.raises(TypeError, match="keys must be a list"): + kl.set_keys("not-a-list") + + +def test_set_keys_invalid_elements(): + """Test set_keys should reject non-Key elements.""" + kl = KeyList() + + with pytest.raises(TypeError, match="instances of Key"): + kl.set_keys([1, 2]) + + +def test_set_threshold(): + """Test threshold can be updated.""" + kl = KeyList(make_keys(3)) + + returned = kl.set_threshold(2) + + assert returned is kl + assert kl.threshold == 2 + + +def test_set_threshold_none(): + """Test threshold can be reset to None.""" + kl = KeyList(make_keys(3), threshold=2) + + kl.set_threshold(None) + + assert kl.threshold is None + + +def test_set_threshold_type_check(): + """Test set_threshold should reject non-int values.""" + kl = KeyList(make_keys(2)) + + with pytest.raises(TypeError, match="threshold must be an integer"): + kl.set_threshold("bad") + + +def test_to_proto_without_threshold(): + """Test to_proto should produce a protobuf KeyList.""" + keys = make_keys(3) + kl = KeyList(keys) + + proto = kl.to_proto() + + assert isinstance(proto, basic_types_pb2.KeyList) + assert len(proto.keys) == 3 + + +def test_to_proto_with_threshold(): + """Test to_proto_key should produce a ThresholdKey if threshold is set.""" + keys = make_keys(3) + kl = KeyList(keys, threshold=2) + + proto_key = kl.to_proto_key() + + assert proto_key.HasField("thresholdKey") + assert proto_key.thresholdKey.threshold == 2 + assert len(proto_key.thresholdKey.keys.keys) == 3 + + +def test_to_proto_key_without_threshold(): + """Test to_proto_key should produce a standard KeyList if no threshold.""" + kl = KeyList(make_keys(2)) + + proto_key = kl.to_proto_key() + + assert proto_key.HasField("keyList") + assert len(proto_key.keyList.keys) == 2 + + +def test_from_proto_keylist(): + """Test from_proto correctly reconstructs a KeyList.""" + keys = make_keys(3) + kl = KeyList(keys) + + proto = kl.to_proto() + + loaded = KeyList.from_proto(proto) + + assert isinstance(loaded, KeyList) + assert len(loaded.keys) == 3 + assert loaded.threshold is None + + +def test_from_proto_threshold(): + """Test thresholdKey proto loads correctly.""" + keys = make_keys(3) + kl = KeyList(keys, threshold=2) + + proto_key = kl.to_proto_key() + + loaded = KeyList.from_proto( + proto_key.thresholdKey.keys, threshold=proto_key.thresholdKey.threshold + ) + + assert loaded.threshold == 2 + assert len(loaded.keys) == 3 + + +@pytest.mark.parametrize("num_keys", [1, 2, 5]) +def test_proto_roundtrip(num_keys): + """Test roundtrip should preserve key count.""" + kl1 = KeyList(make_keys(num_keys), threshold=None) + + proto = kl1.to_proto() + kl2 = KeyList.from_proto(proto) + + assert len(kl1.keys) == len(kl2.keys) + + +def test_keylist_is_instance_of_key(): + """Test KeyList must inherit from Key.""" + kl = KeyList() + assert isinstance(kl, Key) diff --git a/tests/unit/key_test.py b/tests/unit/key_test.py new file mode 100644 index 000000000..6e35b1bf6 --- /dev/null +++ b/tests/unit/key_test.py @@ -0,0 +1,186 @@ +import pytest + +from hiero_sdk_python.contract.delegate_contract_id import DelegateContractId +from hiero_sdk_python.crypto.key import Key +from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.crypto.public_key import PublicKey +from hiero_sdk_python.crypto.key_list import KeyList +from hiero_sdk_python.crypto.evm_address import EvmAddress +from hiero_sdk_python.contract.contract_id import ContractId +from hiero_sdk_python.hapi.services import basic_types_pb2 + +pytestmark = pytest.mark.unit + + +class DummyKey(Key): + """Helper Key implementation for testing.""" + + def to_proto_key(self) -> basic_types_pb2.Key: + return basic_types_pb2.Key() + + +def test_from_bytes_type_check(): + """Test from_bytes should reject non-bytes.""" + with pytest.raises(TypeError, match="data must be bytes"): + Key.from_bytes("not-bytes") + + +def test_to_bytes_serialization(): + """Test to_bytes should serialize the proto returned by to_proto_key.""" + key = DummyKey() + b = key.to_bytes() + + assert isinstance(b, bytes) + + +def test_from_proto_key_ed25519(): + """Test ed25519 keys are parsed into PublicKey objects.""" + priv = PrivateKey.generate("ed25519") + pub = priv.public_key() + + proto = pub.to_proto_key() + + loaded = Key.from_proto_key(proto) + + assert isinstance(loaded, PublicKey) + + +def test_from_proto_key_ecdsa(): + """Test ECDSA keys are parsed into PublicKey objects.""" + priv = PrivateKey.generate("ecdsa") + pub = priv.public_key() + + proto = pub.to_proto_key() + + loaded = Key.from_proto_key(proto) + + assert isinstance(loaded, PublicKey) + + +def test_from_proto_key_evm_address(): + """Test ECDSA_secp256k1 field is 20 bytes it should produce an EvmAddress.""" + evm = EvmAddress.from_bytes(b"\x11" * 20) + + proto = basic_types_pb2.Key(ECDSA_secp256k1=evm.address_bytes) + loaded = Key.from_proto_key(proto) + + assert isinstance(loaded, EvmAddress) + + +def test_from_proto_key_contract_id(): + """Test contractID should produce a ContractId instance.""" + cid = ContractId(0, 0, 123) + + proto = basic_types_pb2.Key(contractID=cid._to_proto()) + + loaded = Key.from_proto_key(proto) + + assert isinstance(loaded, ContractId) + + +def test_from_proto_key_delegatable_contract_id(): + """Test delegatable_contract_id should produce a ContractId instance.""" + cid = ContractId(0, 0, 123) + + proto = basic_types_pb2.Key(delegatable_contract_id=cid._to_proto()) + + loaded = Key.from_proto_key(proto) + + assert isinstance(loaded, DelegateContractId) + + +def test_from_proto_key_keylist(): + """Test keyList should produce a KeyList instance.""" + priv = PrivateKey.generate("ed25519") + + kl = KeyList([priv.public_key()]) + proto = kl.to_proto_key() + + loaded = Key.from_proto_key(proto) + + assert isinstance(loaded, KeyList) + assert len(loaded.keys) == 1 + + +def test_from_proto_key_threshold_key(): + """Test thresholdKey should produce a KeyList with threshold.""" + priv = PrivateKey.generate("ed25519") + + kl = KeyList([priv.public_key()], threshold=1) + proto = kl.to_proto_key() + + loaded = Key.from_proto_key(proto) + + assert isinstance(loaded, KeyList) + assert loaded.threshold == 1 + + +def test_from_proto_key_unknown_type(): + """Test unknown key type should be raised error.""" + proto = basic_types_pb2.Key() + + with pytest.raises(ValueError, match="Unknown key type"): + Key.from_proto_key(proto) + + +def test_from_proto_key_type_check(): + """Test from_proto_key should reject invalid proto type.""" + with pytest.raises(TypeError, match="proto must be an instance"): + Key.from_proto_key("not-a-proto") + + +def test_from_bytes_roundtrip(): + """Test a key to bytes then load with Key using from_bytes.""" + priv = PrivateKey.generate("ed25519") + pub = priv.public_key() + + b = pub.to_bytes() + + loaded = Key.from_bytes(b) + + assert isinstance(loaded, PublicKey) + + +@pytest.mark.parametrize("key_type", ["ed25519", "ecdsa"]) +def test_from_bytes_multiple_key_types(key_type): + """Test from_bytes supports multiple key algorithms.""" + priv = PrivateKey.generate(key_type) + pub = priv.public_key() + + loaded = Key.from_bytes(pub.to_bytes()) + + assert isinstance(loaded, PublicKey) + + +def test_keylist_bytes_roundtrip(): + """Test should serialize and deserialize Key using from_bytes.""" + keys = [PrivateKey.generate("ed25519").public_key() for _ in range(2)] + + kl = KeyList(keys) + + b = kl.to_bytes() + + loaded = Key.from_bytes(b) + + assert isinstance(loaded, KeyList) + assert len(loaded.keys) == 2 + + +def test_threshold_key_bytes_roundtrip(): + """Test ThresholdKey should serialize and deserialize correctly.""" + keys = [PrivateKey.generate("ed25519").public_key() for _ in range(3)] + + kl = KeyList(keys, threshold=2) + + b = kl.to_bytes() + + loaded = Key.from_bytes(b) + + assert isinstance(loaded, KeyList) + assert loaded.threshold == 2 + + +def test_key_is_abstract(): + """Test key should not be directly instantiable.""" + with pytest.raises(TypeError): + Key() diff --git a/tests/unit/keys_private_test.py b/tests/unit/keys_private_test.py index bbc8589dd..37de97668 100644 --- a/tests/unit/keys_private_test.py +++ b/tests/unit/keys_private_test.py @@ -5,6 +5,7 @@ from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric import ec, ed25519, rsa from cryptography.hazmat.primitives import serialization +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.crypto.public_key import PublicKey from hiero_sdk_python.crypto.private_key import PrivateKey @@ -542,3 +543,38 @@ def test_hash_algorithm_distinction(): assert ed != ec assert hash(ed) != hash(ec) + + +def test_to_proto_key_ecd25519(): + """Test to_proto_key properly convert ed25519 to Key protobuf.""" + pr_key = PrivateKey.generate_ed25519() + proto_key = pr_key.to_proto_key() + + assert proto_key is not None + assert proto_key.ed25519 is not None + assert proto_key.ed25519 == pr_key.public_key().to_bytes_raw() + +def test_to_proto_key_ecdsa(): + """Test to_proto_key properly convert ecdsa to Key protobuf.""" + pr_key = PrivateKey.generate_ecdsa() + proto_key = pr_key.to_proto_key() + + assert proto_key is not None + assert proto_key.ECDSA_secp256k1 is not None + assert proto_key.ECDSA_secp256k1 == pr_key.public_key().to_bytes_raw() + +@pytest.mark.parametrize( + "key", + [PrivateKey.generate_ed25519(), PrivateKey.generate_ecdsa()] +) +def test_protobuf_roundtrip(key): + """Test protobuf roundtrip properly convert to Key protobuf and vice versa.""" + proto = key.to_proto_key() + + pub_key = key.public_key() + loaded = Key.from_proto_key(proto) # Returns public key + + assert isinstance(loaded, PublicKey) + assert loaded.is_ed25519() == pub_key.is_ed25519() + assert loaded.is_ecdsa() == pub_key.is_ecdsa() + assert loaded.to_bytes_raw() == pub_key.to_bytes_raw() diff --git a/tests/unit/keys_public_test.py b/tests/unit/keys_public_test.py index 5b99bd321..bbf0f2049 100644 --- a/tests/unit/keys_public_test.py +++ b/tests/unit/keys_public_test.py @@ -5,8 +5,9 @@ from cryptography.hazmat.primitives.asymmetric import utils as asym_utils from cryptography.exceptions import InvalidSignature from hiero_sdk_python.crypto.evm_address import EvmAddress +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.crypto.private_key import PrivateKey -from hiero_sdk_python.hapi.services.basic_types_pb2 import Key +from hiero_sdk_python.hapi.services import basic_types_pb2 from hiero_sdk_python.crypto.public_key import PublicKey from hiero_sdk_python.utils.crypto_utils import keccak256 @@ -469,7 +470,7 @@ def test_from_proto_ecdsa(ecdsa_keypair): def test_from_proto_unsupported_type(): # Create a Key proto with an unsupported type - proto = Key() + proto = basic_types_pb2.Key() # Set some arbitrary bytes to a RSA_3072 as currently we do not support it proto.RSA_3072 = b"currently unsupported" @@ -711,3 +712,37 @@ def test_equality_with_non_publickey_returns_false(ed25519_keypair, other): assert (key == other) is False +def test_to_proto_key_ecd25519(): + """Test to_proto_key properly convert ed25519 to Key protobuf.""" + pub_key = PrivateKey.generate_ed25519().public_key() + proto_key = pub_key.to_proto_key() + + assert proto_key is not None + assert proto_key.ed25519 is not None + assert proto_key.ed25519 == pub_key.to_bytes_raw() + +def test_to_proto_key_ecdsa(): + """Test to_proto_key properly convert ecdsa to Key protobuf.""" + pub_key = PrivateKey.generate_ecdsa().public_key() + proto_key = pub_key.to_proto_key() + + assert proto_key is not None + assert proto_key.ECDSA_secp256k1 is not None + assert proto_key.ECDSA_secp256k1 == pub_key.to_bytes_raw() + +@pytest.mark.parametrize( + "key", + [PrivateKey.generate_ed25519(), PrivateKey.generate_ecdsa()] +) +def test_protobuf_roundtrip(key): + """Test protobuf roundtrip properly convert to Key protobuf and vice versa.""" + pub_key = key.public_key() + + proto = pub_key.to_proto_key() + loaded = Key.from_proto_key(proto) + + assert isinstance(loaded, PublicKey) + assert loaded.is_ed25519() == pub_key.is_ed25519() + assert loaded.is_ecdsa() == pub_key.is_ecdsa() + assert loaded.to_bytes_raw() == pub_key.to_bytes_raw() + diff --git a/tests/unit/tck/handlers_test.py b/tests/unit/tck/handlers_test.py deleted file mode 100644 index 574e5419c..000000000 --- a/tests/unit/tck/handlers_test.py +++ /dev/null @@ -1,359 +0,0 @@ -"""Test cases for the Hiero SDK TCK handlers registry and dispatch functionality.""" -import pytest -from unittest.mock import MagicMock -from hiero_sdk_python.exceptions import (PrecheckError, ReceiptStatusError, MaxAttemptsError) -from tck.handlers.registry import ( - register_handler, get_handler, get_all_handlers, dispatch, safe_dispatch, - validate_request_params -) -from tck.errors import ( - JsonRpcError, - METHOD_NOT_FOUND, - INTERNAL_ERROR, - HIERO_ERROR, - INVALID_PARAMS) -from tck.handlers import registry - -pytestmark = pytest.mark.unit - -@pytest.fixture(autouse=True) -def clear_handlers(): - """Clear the handlers registry before each test.""" - registry._HANDLERS.clear() - yield - registry._HANDLERS.clear() - - -class TestHandlerRegistration: - """Test handler registration via decorator.""" - - def test_handler_registration_via_decorator(self): - """Test that @register_handler decorator adds handler to registry.""" - @register_handler("test_method") - def test_handler(_params): - return {"status": "ok"} - - handler = get_handler("test_method") - if handler is None: - raise AssertionError("Expected handler to be registered") - if handler({"test": "param"}) != {"status": "ok"}: - raise AssertionError("Expected handler to return ok status") - - def test_get_all_handlers(self): - """Test that get_all_handlers returns copy of all registered handlers.""" - @register_handler("method1") - def handler1(_params): - return "result1" - - @register_handler("method2") - def handler2(_params): - return "result2" - - all_handlers = get_all_handlers() - if len(all_handlers) != 2: - raise AssertionError("Expected two handlers to be registered") - if "method1" not in all_handlers: - raise AssertionError("Expected method1 to be registered") - if "method2" not in all_handlers: - raise AssertionError("Expected method2 to be registered") - - def test_get_nonexistent_handler_returns_none(self): - """Test that getting a non-existent handler returns None.""" - handler = get_handler("nonexistent") - if handler is not None: - raise AssertionError("Expected None for nonexistent handler") - - def test_handler_override_behavior(self): - """Test that registering the same method twice overwrites the first handler.""" - @register_handler("override_method") - def first_handler(_params): - return "first" - - @register_handler("override_method") - def second_handler(_params): - return "second" - - handler = get_handler("override_method") - if handler({}) != "second": - raise AssertionError("Second handler should overwrite first") - - def test_get_all_handlers_returns_copy(self): - """Test that get_all_handlers returns a copy that doesn't affect the registry.""" - @register_handler("protected_method") - def protected_handler(_params): - return "protected" - - all_handlers = get_all_handlers() - all_handlers["protected_method"] = lambda: "modified" - all_handlers["injected_method"] = lambda: "injected" - - # Original registry should be unchanged - if get_handler("protected_method")({}) != "protected": - raise AssertionError("Expected protected_method to be unchanged") - if get_handler("injected_method") is not None: - raise AssertionError("Expected injected_method to be absent") - - -class TestDispatch: - """Test method dispatch functionality.""" - - def test_dispatch_registered_method(self): - """Test that dispatch invokes registered handler correctly.""" - @register_handler("setup") - def setup_handler(_params): - return {"ready": True} - - result = dispatch("setup", {"key": "value"}, None) - if result != {"ready": True}: - raise AssertionError("Expected handler result to match") - - def test_dispatch_with_session_id(self): - """Test that dispatch passes session_id to handler when provided.""" - @register_handler("session_method") - def session_handler(params, session_id): - return {"session": session_id, "params": params} - - result = dispatch("session_method", {"data": "test"}, "session123") - if result != {"session": "session123", "params": {"data": "test"}}: - raise AssertionError("Expected session id and params to be passed through") - - def test_dispatch_unknown_method_raises_method_not_found(self): - """Test that dispatching unknown method raises METHOD_NOT_FOUND error.""" - with pytest.raises(JsonRpcError) as excinfo: - dispatch("unknown_method", {}, None) - - if excinfo.value.code != METHOD_NOT_FOUND: - raise AssertionError("Expected METHOD_NOT_FOUND error") - if "Method not found" not in excinfo.value.message: - raise AssertionError("Expected Method not found message") - - def test_dispatch_reraises_json_rpc_error(self): - """Test that dispatch re-raises JsonRpcError exceptions.""" - @register_handler("error_method") - def error_handler(params): - raise JsonRpcError.invalid_params_error() - - with pytest.raises(JsonRpcError) as excinfo: - dispatch("error_method", {}, None) - - if excinfo.value.code != INVALID_PARAMS: - raise AssertionError("Expected INVALID_PARAMS error") - - def test_dispatch_converts_generic_exception_to_internal_error(self): - """Test that dispatch converts generic exceptions to INTERNAL_ERROR.""" - @register_handler("crash_method") - def crash_handler(params): - raise ValueError("Something went wrong") - - with pytest.raises(JsonRpcError) as excinfo: - dispatch("crash_method", {}, None) - - if excinfo.value.code != INTERNAL_ERROR: - raise AssertionError("Expected INTERNAL_ERROR error") - if "Something went wrong" not in excinfo.value.data: - raise AssertionError("Expected error details to include message") - - def test_dispatch_converts_precheck_error_to_hiero_error(self): - """Test that dispatch converts PrecheckError to HIERO_ERROR.""" - @register_handler("precheck_method") - def precheck_handler(_params): - raise PrecheckError(status=123, transaction_id="0.0.456", message="Account does not exist") - - with pytest.raises(JsonRpcError) as excinfo: - dispatch("precheck_method", {}, None) - - if excinfo.value.code != HIERO_ERROR: - raise AssertionError("Expected HIERO_ERROR error") - if "Hiero error" not in excinfo.value.message: - raise AssertionError("Expected Hiero error message") - - def test_dispatch_converts_receipt_status_error_to_hiero_error(self): - """Test that dispatch converts ReceiptStatusError to HIERO_ERROR.""" - @register_handler("receipt_method") - def receipt_handler(_params): - mock_receipt = MagicMock() - raise ReceiptStatusError(status=21, transaction_id=None, transaction_receipt=mock_receipt, message="Transaction failed") - - with pytest.raises(JsonRpcError) as excinfo: - dispatch("receipt_method", {}, None) - - if excinfo.value.code != HIERO_ERROR: - raise AssertionError("Expected HIERO_ERROR error") - if "Hiero error" not in excinfo.value.message: - raise AssertionError("Expected Hiero error message") - - def test_dispatch_converts_max_attempts_error_to_hiero_error(self): - """Test that dispatch converts MaxAttemptsError to HIERO_ERROR.""" - @register_handler("max_attempts_method") - def max_attempts_handler(_params): - raise MaxAttemptsError("Max retries exceeded", node_id="0.0.1") - - with pytest.raises(JsonRpcError) as excinfo: - dispatch("max_attempts_method", {}, None) - - if excinfo.value.code != HIERO_ERROR: - raise AssertionError("Expected HIERO_ERROR error") - if "Hiero error" not in excinfo.value.message: - raise AssertionError("Expected Hiero error message") - - -class TestSafeDispatch: - """Test safe dispatch with error handling.""" - - def test_safe_dispatch_returns_success_response(self): - """Test that safe_dispatch returns result for successful dispatch.""" - @register_handler("success_method") - def success_handler(_params): - return {"success": True} - - # safe_dispatch should return raw result without wrapping - result = safe_dispatch("success_method", {}, None, 1) - if result != {"success": True}: - raise AssertionError("Expected raw handler result from safe_dispatch") - - def test_safe_dispatch_returns_error_response_for_json_rpc_error(self): - """Test that safe_dispatch returns error response for JsonRpcError.""" - @register_handler("json_error_method") - def error_handler(_params): - raise JsonRpcError.invalid_params_error(data="field_name") - - response = safe_dispatch("json_error_method", {}, None, 42) - - if "error" not in response: - raise AssertionError("Expected error in response") - if response["error"]["code"] != INVALID_PARAMS: - raise AssertionError("Expected INVALID_PARAMS error code") - if response["error"]["message"] != "Invalid params": - raise AssertionError("Expected Bad params message") - if response["error"]["data"] != "field_name": - raise AssertionError("Expected error data to match") - if response["id"] != 42: - raise AssertionError("Expected response id to match request id") - - def test_safe_dispatch_transforms_precheck_error(self): - """Test that safe_dispatch transforms PrecheckError to HIERO_ERROR.""" - @register_handler("precheck_method") - def precheck_handler(_params): - raise PrecheckError(status=123, transaction_id="0.0.456", message="Account does not exist") - - response = safe_dispatch("precheck_method", {}, None, 1) - - if "error" not in response: - raise AssertionError("Expected error in response") - if response["error"]["code"] != HIERO_ERROR: - raise AssertionError("Expected HIERO_ERROR error code") - if "Hiero error" not in response["error"]["message"]: - raise AssertionError("Expected Hiero error message") - - def test_safe_dispatch_transforms_receipt_status_error(self): - """Test that safe_dispatch transforms ReceiptStatusError to HIERO_ERROR.""" - @register_handler("receipt_method") - def receipt_handler(params): - mock_receipt = MagicMock() - raise ReceiptStatusError(status=21, transaction_id=None, transaction_receipt=mock_receipt, message="Transaction failed") - response = safe_dispatch("receipt_method", {}, None, 2) - - if "error" not in response: - raise AssertionError("Expected error in response") - if response["error"]["code"] != HIERO_ERROR: - raise AssertionError("Expected HIERO_ERROR error code") - - def test_safe_dispatch_transforms_max_attempts_error(self): - """Test that safe_dispatch transforms MaxAttemptsError to HIERO_ERROR.""" - @register_handler("max_attempts_method") - def max_attempts_handler(params): - raise MaxAttemptsError("Max retries exceeded", node_id="0.0.1") - response = safe_dispatch("max_attempts_method", {}, None, 3) - - if "error" not in response: - raise AssertionError("Expected error in response") - if response["error"]["code"] != HIERO_ERROR: - raise AssertionError("Expected HIERO_ERROR error code") - - def test_safe_dispatch_transforms_generic_exception(self): - """Test that safe_dispatch transforms generic Exception to INTERNAL_ERROR.""" - @register_handler("generic_error_method") - def generic_error_handler(params): - raise RuntimeError("Unexpected error") - - response = safe_dispatch("generic_error_method", {}, None, 4) - - if "error" not in response: - raise AssertionError("Expected error in response") - if response["error"]["code"] != INTERNAL_ERROR: - raise AssertionError("Expected INTERNAL_ERROR error code") - if response["id"] != 4: - raise AssertionError("Expected response id to match request id") - - def test_safe_dispatch_includes_request_id_in_response(self): - """Test that safe_dispatch returns raw result without id field (id is added at server level).""" - @register_handler("test_id_method") - def test_id_handler(_params): - return {"data": "test"} - - # safe_dispatch returns raw result; request_id is added by server layer - response = safe_dispatch("test_id_method", {}, None, "request_id_123") - if response != {"data": "test"}: - raise AssertionError("Expected raw response to be returned") - - -class TestValidateRequestParams: - """Test request parameter validation.""" - - def test_validate_valid_params(self): - """Test that validate_request_params accepts valid params.""" - params = {"memo": "Test topic", "sequence_number": 42} - required = {"memo": str, "sequence_number": int} - - # Should not raise- explicitly verify by calling and asserting no exception - try: - validate_request_params(params, required) - except JsonRpcError: - pytest.fail("validate_request_params raised JsonRpcError unexpectedly") - - def test_validate_missing_required_field(self): - """Test that missing required field raises INVALID_PARAMS.""" - params = {"memo": "Test topic"} - required = {"memo": str, "sequence_number": int} - - with pytest.raises(JsonRpcError) as excinfo: - validate_request_params(params, required) - - if excinfo.value.code != INVALID_PARAMS: - raise AssertionError("Expected INVALID_PARAMS error") - if "sequence_number" not in excinfo.value.message: - raise AssertionError("Expected missing field name in message") - - def test_validate_incorrect_field_type(self): - """Test that incorrect field type raises INVALID_PARAMS.""" - params = {"memo": "Test topic", "sequence_number": "not_a_number"} - required = {"memo": str, "sequence_number": int} - - with pytest.raises(JsonRpcError) as excinfo: - validate_request_params(params, required) - - if excinfo.value.code != INVALID_PARAMS: - raise AssertionError("Expected INVALID_PARAMS error") - if "sequence_number" not in excinfo.value.message: - raise AssertionError("Expected field name in message") - - def test_validate_non_dict_params_raises_invalid_params(self): - """Test that non-dict params raises INVALID_PARAMS.""" - with pytest.raises(JsonRpcError) as excinfo: - validate_request_params(["not", "a", "dict"], {"memo": str}) - - if excinfo.value.code != INVALID_PARAMS: - raise AssertionError("Expected INVALID_PARAMS error") - - def test_validate_none_value_for_required_field(self): - """Test that None value for required field raises INVALID_PARAMS.""" - params = {"memo": None, "sequence_number": 42} - required = {"memo": str, "sequence_number": int} - - with pytest.raises(JsonRpcError) as excinfo: - validate_request_params(params, required) - - if excinfo.value.code != INVALID_PARAMS: - raise AssertionError("Expected INVALID_PARAMS error") - if "memo" not in excinfo.value.message: - raise AssertionError("Expected field name in message") \ No newline at end of file From bd5b8793168450dbe29c2463678a3e8dcb9af23a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:12:58 +0000 Subject: [PATCH 05/60] chore(deps): bump step-security/harden-runner from 2.14.2 to 2.16.0 (#1989) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/bot-advanced-check.yml | 2 +- .github/workflows/bot-assignment-check.yml | 2 +- .github/workflows/bot-beginner-assign-on-comment.yml | 2 +- .github/workflows/bot-coderabbit-plan-trigger.yml | 2 +- .github/workflows/bot-community-calls.yml | 2 +- .github/workflows/bot-gfi-assign-on-comment.yml | 2 +- .github/workflows/bot-gfi-candidate-notification.yaml | 2 +- .github/workflows/bot-inactivity-unassign.yml | 2 +- .github/workflows/bot-intermediate-assignment.yml | 2 +- .github/workflows/bot-issue-reminder-no-pr.yml | 2 +- .github/workflows/bot-linked-issue-enforcer.yml | 2 +- .github/workflows/bot-office-hours.yml | 2 +- .github/workflows/bot-p0-issues-notify-team.yml | 2 +- .github/workflows/bot-pr-draft-explainer.yaml | 2 +- .github/workflows/bot-pr-inactivity-reminder.yml | 2 +- .github/workflows/bot-workflows.yml | 2 +- .github/workflows/cron-check-broken-links.yml | 2 +- .github/workflows/cron-update-spam-list.yml | 2 +- .github/workflows/deps-check.yml | 2 +- .github/workflows/pr-check-broken-links.yml | 2 +- .github/workflows/pr-check-changelog.yml | 2 +- .github/workflows/pr-check-codecov.yml | 2 +- .github/workflows/pr-check-examples.yml | 2 +- .github/workflows/pr-check-test-files.yml | 2 +- .github/workflows/pr-check-test.yml | 4 ++-- .github/workflows/publish.yml | 2 +- .github/workflows/unassign-on-comment.yml | 2 +- .github/workflows/working-on-comment.yml | 2 +- 28 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/bot-advanced-check.yml b/.github/workflows/bot-advanced-check.yml index 3feff4fd0..8c39dc376 100644 --- a/.github/workflows/bot-advanced-check.yml +++ b/.github/workflows/bot-advanced-check.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/bot-assignment-check.yml b/.github/workflows/bot-assignment-check.yml index c733911e9..e282589d3 100644 --- a/.github/workflows/bot-assignment-check.yml +++ b/.github/workflows/bot-assignment-check.yml @@ -12,7 +12,7 @@ jobs: runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/bot-beginner-assign-on-comment.yml b/.github/workflows/bot-beginner-assign-on-comment.yml index ceeacf9e5..312e9b637 100644 --- a/.github/workflows/bot-beginner-assign-on-comment.yml +++ b/.github/workflows/bot-beginner-assign-on-comment.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Harden runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/bot-coderabbit-plan-trigger.yml b/.github/workflows/bot-coderabbit-plan-trigger.yml index de7e59c8e..c932dabc3 100644 --- a/.github/workflows/bot-coderabbit-plan-trigger.yml +++ b/.github/workflows/bot-coderabbit-plan-trigger.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/bot-community-calls.yml b/.github/workflows/bot-community-calls.yml index 4735252fc..4bedbf191 100644 --- a/.github/workflows/bot-community-calls.yml +++ b/.github/workflows/bot-community-calls.yml @@ -27,7 +27,7 @@ jobs: runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc #2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 #2.16.0 with: egress-policy: audit diff --git a/.github/workflows/bot-gfi-assign-on-comment.yml b/.github/workflows/bot-gfi-assign-on-comment.yml index 5ecdb46bf..a431bdb3c 100644 --- a/.github/workflows/bot-gfi-assign-on-comment.yml +++ b/.github/workflows/bot-gfi-assign-on-comment.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Harden runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/bot-gfi-candidate-notification.yaml b/.github/workflows/bot-gfi-candidate-notification.yaml index 68e272df4..125793e11 100644 --- a/.github/workflows/bot-gfi-candidate-notification.yaml +++ b/.github/workflows/bot-gfi-candidate-notification.yaml @@ -21,7 +21,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 with: egress-policy: audit diff --git a/.github/workflows/bot-inactivity-unassign.yml b/.github/workflows/bot-inactivity-unassign.yml index 4456f47df..b77bb2bdf 100644 --- a/.github/workflows/bot-inactivity-unassign.yml +++ b/.github/workflows/bot-inactivity-unassign.yml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 with: egress-policy: audit diff --git a/.github/workflows/bot-intermediate-assignment.yml b/.github/workflows/bot-intermediate-assignment.yml index 43ce00086..60b8f525c 100644 --- a/.github/workflows/bot-intermediate-assignment.yml +++ b/.github/workflows/bot-intermediate-assignment.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/bot-issue-reminder-no-pr.yml b/.github/workflows/bot-issue-reminder-no-pr.yml index 939f72b40..08b634edb 100644 --- a/.github/workflows/bot-issue-reminder-no-pr.yml +++ b/.github/workflows/bot-issue-reminder-no-pr.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 with: egress-policy: audit diff --git a/.github/workflows/bot-linked-issue-enforcer.yml b/.github/workflows/bot-linked-issue-enforcer.yml index 33f8b8b72..5bc32c76b 100644 --- a/.github/workflows/bot-linked-issue-enforcer.yml +++ b/.github/workflows/bot-linked-issue-enforcer.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit - name: Checkout repository diff --git a/.github/workflows/bot-office-hours.yml b/.github/workflows/bot-office-hours.yml index 9724cd5bc..8821d1618 100644 --- a/.github/workflows/bot-office-hours.yml +++ b/.github/workflows/bot-office-hours.yml @@ -26,7 +26,7 @@ jobs: cancel-in-progress: false steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc #2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 #2.16.0 with: egress-policy: audit diff --git a/.github/workflows/bot-p0-issues-notify-team.yml b/.github/workflows/bot-p0-issues-notify-team.yml index a4b202f76..77c03738d 100644 --- a/.github/workflows/bot-p0-issues-notify-team.yml +++ b/.github/workflows/bot-p0-issues-notify-team.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/bot-pr-draft-explainer.yaml b/.github/workflows/bot-pr-draft-explainer.yaml index 705ac1cc4..3a1c031c9 100644 --- a/.github/workflows/bot-pr-draft-explainer.yaml +++ b/.github/workflows/bot-pr-draft-explainer.yaml @@ -29,7 +29,7 @@ jobs: cancel-in-progress: true steps: - name: Harden the runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/bot-pr-inactivity-reminder.yml b/.github/workflows/bot-pr-inactivity-reminder.yml index 79dde45fe..c710ca653 100644 --- a/.github/workflows/bot-pr-inactivity-reminder.yml +++ b/.github/workflows/bot-pr-inactivity-reminder.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit - name: Checkout repository diff --git a/.github/workflows/bot-workflows.yml b/.github/workflows/bot-workflows.yml index 48eee26bf..21e0239ae 100644 --- a/.github/workflows/bot-workflows.yml +++ b/.github/workflows/bot-workflows.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/cron-check-broken-links.yml b/.github/workflows/cron-check-broken-links.yml index 321d1ca11..bde2e6b6b 100644 --- a/.github/workflows/cron-check-broken-links.yml +++ b/.github/workflows/cron-check-broken-links.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden runner (audit outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/cron-update-spam-list.yml b/.github/workflows/cron-update-spam-list.yml index 638b2197e..2f333f014 100644 --- a/.github/workflows/cron-update-spam-list.yml +++ b/.github/workflows/cron-update-spam-list.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden runner (audit outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/deps-check.yml b/.github/workflows/deps-check.yml index 5a9b0af66..a741f2078 100644 --- a/.github/workflows/deps-check.yml +++ b/.github/workflows/deps-check.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/pr-check-broken-links.yml b/.github/workflows/pr-check-broken-links.yml index 173ef74e2..f7c23b6df 100644 --- a/.github/workflows/pr-check-broken-links.yml +++ b/.github/workflows/pr-check-broken-links.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Harden runner (audit outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/pr-check-changelog.yml b/.github/workflows/pr-check-changelog.yml index 83177726a..f4355a3e5 100644 --- a/.github/workflows/pr-check-changelog.yml +++ b/.github/workflows/pr-check-changelog.yml @@ -16,7 +16,7 @@ jobs: fetch-depth: 0 - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/pr-check-codecov.yml b/.github/workflows/pr-check-codecov.yml index 3d262b3e7..4ab1b1bc3 100644 --- a/.github/workflows/pr-check-codecov.yml +++ b/.github/workflows/pr-check-codecov.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/pr-check-examples.yml b/.github/workflows/pr-check-examples.yml index a043c6187..c14fe8a23 100644 --- a/.github/workflows/pr-check-examples.yml +++ b/.github/workflows/pr-check-examples.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/pr-check-test-files.yml b/.github/workflows/pr-check-test-files.yml index d90dc06ce..b2593cd62 100644 --- a/.github/workflows/pr-check-test-files.yml +++ b/.github/workflows/pr-check-test-files.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/pr-check-test.yml b/.github/workflows/pr-check-test.yml index 9a4b69a97..54e0f2826 100644 --- a/.github/workflows/pr-check-test.yml +++ b/.github/workflows/pr-check-test.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit @@ -99,7 +99,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6c24aa547..175e90f9a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,7 @@ jobs: id-token: write steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit diff --git a/.github/workflows/unassign-on-comment.yml b/.github/workflows/unassign-on-comment.yml index a8bceb18c..52ed9e070 100644 --- a/.github/workflows/unassign-on-comment.yml +++ b/.github/workflows/unassign-on-comment.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Harden runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 with: egress-policy: audit diff --git a/.github/workflows/working-on-comment.yml b/.github/workflows/working-on-comment.yml index 27d88bf3a..515e2bda0 100644 --- a/.github/workflows/working-on-comment.yml +++ b/.github/workflows/working-on-comment.yml @@ -26,7 +26,7 @@ jobs: cancel-in-progress: false steps: - name: Harden Runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit From 2898bf9cd5077d47286c17689dd1e06257327bec Mon Sep 17 00:00:00 2001 From: Sri Nishanth Molleti <48158364+NishanthMolleti@users.noreply.github.com> Date: Sat, 21 Mar 2026 16:53:15 -0700 Subject: [PATCH 06/60] chore: add Chocolatey to Windows setup prerequisites (#2009) Signed-off-by: Sri Nishanth Molleti --- CHANGELOG.md | 1 + docs/sdk_developers/setup_windows.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eb783a11..790efe344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Added TCK endpoint for the createAccount method ### Docs +- Add Chocolatey as a prerequisite in the Windows setup guide (#1961) ### .github diff --git a/docs/sdk_developers/setup_windows.md b/docs/sdk_developers/setup_windows.md index c0f82273d..794fe854d 100644 --- a/docs/sdk_developers/setup_windows.md +++ b/docs/sdk_developers/setup_windows.md @@ -25,6 +25,7 @@ Before you begin, ensure you have the following installed on your system: 1. **Git for Windows**: [Download and install Git](https://gitforwindows.org/). 2. **Python 3.10+**: [Download and install Python](https://www.python.org/downloads/windows/). Ensure "Add Python to PATH" is checked during installation. 3. **GitHub Account**: You will need a GitHub account to fork the repository. +4. **Chocolatey**: For Windows 10 users, you must install Chocolatey. [Install Chocolatey](https://chocolatey.org/install). Make sure Chocolatey is added to PATH. --- From 5443cbe4e69d8bc3795b920c99cc421f6dc9ecda Mon Sep 17 00:00:00 2001 From: cheese-cakee Date: Sun, 22 Mar 2026 16:30:54 +0530 Subject: [PATCH 07/60] fix: refactor intermediate issue template to improve PR quality and reduce review burden (#2010) Signed-off-by: cheese-cakee --- .../ISSUE_TEMPLATE/04_intermediate_issue.yml | 364 +++++++++++++----- CHANGELOG.md | 3 +- 2 files changed, 263 insertions(+), 104 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/04_intermediate_issue.yml b/.github/ISSUE_TEMPLATE/04_intermediate_issue.yml index 44e89fcd9..eef1a8fe8 100644 --- a/.github/ISSUE_TEMPLATE/04_intermediate_issue.yml +++ b/.github/ISSUE_TEMPLATE/04_intermediate_issue.yml @@ -1,91 +1,104 @@ name: Intermediate Issue Template -description: Create a well-documented issue for contributors with some familiarity with the codebase +description: Create a well-documented issue for contributors ready to own larger tasks independently title: "[Intermediate]: " labels: ["intermediate"] assignees: [] - body: - type: markdown attributes: value: | --- - ## **Thanks for contributing!** 😊 + **For issue creators:** Intermediate issues sit between Beginner Issues and Advanced Issues. + The contributor should be able to research, implement, and test independently — + but should not need to make architectural decisions or redesign core abstractions. + + If the task can be solved by following examples alone, it belongs in a Beginner Issue. + If it requires cross-module architectural reasoning, it belongs in Advanced. - We truly appreciate your time and effort. - This template is designed to help you create an Intermediate issue. + **All links in this template must be absolute URLs** (starting with `https://`). + Relative links break when rendered in GitHub issues. - The goal is to create an issue for users that have: - - basic familiarity with the Hiero Python SDK codebase - - experience following our contribution workflow - - confidence navigating existing source code and examples --- - type: textarea - id: intro + id: before_you_start attributes: label: 🧩 Intermediate Contributors - description: Who is this issue for? - value: | - This issue is intended for contributors who already have some familiarity with the - [Hiero Python SDK](https://hiero.org) codebase and contribution workflow. - - You should feel comfortable: - - navigating existing source code and examples - - understanding SDK concepts without step-by-step guidance - - following the standard PR workflow without additional onboarding - - If this is your very first contribution to the project, we recommend starting with a few - **Good First Issues** before working on this one. - validations: - required: false - - - type: markdown - attributes: + description: | + THIS SECTION IS PRE-FILLED. LEAVE AS-IS UNLESS YOU NEED TO ADD ISSUE-SPECIFIC PREREQUISITES. value: | + Welcome! This Intermediate Issue is designed for contributors who have completed beginner-level work and are ready to take on more ownership. + + It nudges you to: + + - Research across multiple files and modules + - Own implementation decisions — not just follow a recipe + - Write thorough, meaningful tests + - Deliver a clean, review-ready pull request + + **Prerequisites:** + > [!IMPORTANT] - > ### 🧭 What we consider an *Intermediate Issue* - > - > This issue generally: - > - > - Requires **some knowledge of the existing codebase**, but not deep architectural knowledge - > - Is **narrowly scoped** and well-contained - > - Is **low to medium risk**, not touching highly sensitive or critical DLT logic - > - May involve **refactors or small feature additions** - > - Provides **enough documentation or examples** to reason about a solution - > - Often has **similar patterns elsewhere in the codebase** - > - > **What this issue is NOT:** - > - A beginner-friendly onboarding task - > - A large architectural redesign - > - A change requiring extensive cross-module refactors - > - A breaking API change + > **Before claiming this issue, you must have:** + > - Completed at least 1 Good First Issue **and** 1 Beginner Issue + > - The contribution workflow nailed: signing, rebasing, CHANGELOG, linking issues + > - The ability to operate independently — you may ask high-level architecture questions, but should not need step-by-step guidance + + > [!TIP] + > **You should be comfortable with:** + > - Navigating multiple modules across `src/`, `tests/`, and `examples/` + > - Reading and following [protobuf definitions](https://github.com/hashgraph/hedera-protobufs/tree/main/services) for correct naming, types, and field ordering + > - Writing meaningful tests — not just AI-generated tests of AI-generated code + > - Identifying and avoiding breaking changes to public APIs + > - Self-reviewing your own PR before submitting + + If this feels like too big a step, that is completely fine — try a + [Beginner Issue](https://github.com/hiero-ledger/hiero-sdk-python/issues?q=is%3Aissue+state%3Aopen+label%3Abeginner+no%3Aassignee) + first. You can always come back when you are ready. + + **Support:** A maintainer is available for high-level guidance — implementation approach, test strategy, or design questions. Don't hesitate to ask. + + ⏱️ Typical time to complete: 1 week part-time / 3 days full-time + 🧩 Difficulty: Requires investigation, testing, and polish — most time should go to research and refinement, not coding + 🎓 Best for: Contributors who've completed beginner issues + + --- + + 🏁 **When this issue is complete, you will have:** + + ✅ Researched across multiple files and modules + ✅ Owned implementation decisions + ✅ Written thorough, meaningful tests + ✅ Delivered a clean, review-ready pull request + ✅ Confidence to take on advanced issues + validations: + required: false - type: textarea id: problem attributes: label: 🐞 Problem Description description: | - Describe the problem clearly and precisely. - - You may assume the reader: - - understands the SDK structure - - can navigate `src/`, `examples/`, and `tests/` - - is comfortable reading existing implementations - - Still, explain: - - what is wrong or missing - - where it lives in the codebase - - why it matters + DESCRIBE THE PROBLEM CLEARLY. + YOU MAY ASSUME THE READER CAN NAVIGATE src/, tests/, AND examples/. + EXPLAIN: WHAT IS WRONG OR MISSING, WHERE IT LIVES, AND WHY IT MATTERS. + INCLUDE RELEVANT FILE PATHS AND LINKS. + CONSIDER ADDING: EXPECTED TIME (e.g. 1-2 WEEKS), RELEVANT LABELS, AND A NOTE TO CHECK THE CODERABBIT AI PLAN. value: | - Describe the problem here. + _Replace this with the problem description._ + + **Before claiming:** + - Check the issue labels to see what area this touches + - Read the CodeRabbit AI plan (posted as a comment) for a rough implementation overview + - If this feels like too big a step, try a beginner issue first validations: required: true - type: markdown attributes: value: | - - ## 🐞 Problem – Example + + ## Example — Problem Description The `TransactionGetReceiptQuery` currently exposes the `get_children()` method, but the behavior is inconsistent with how child receipts are returned by the Mirror Node. @@ -107,23 +120,19 @@ body: attributes: label: 💡 Expected Solution description: | - Describe the intended outcome. - - This does NOT need to be a full implementation plan, - but should explain: - - what should change - - what should NOT change - - any constraints or boundaries + BRIEFLY STATE WHAT THE CONTRIBUTOR SHOULD BUILD OR FIX. + THIS SHOULD BE A SHORT SUMMARY OF THE EXPECTED OUTCOME — NOT A DETAILED IMPLEMENTATION GUIDE. + EXAMPLE: "Add a __repr__ method to AccountId that shows shard, realm, and num." value: | - Describe the expected solution here. + _Replace this with a brief solution summary._ validations: required: true - type: markdown attributes: value: | - - ## 💡 Expected Solution – Example + + ## Example — Expected Solution Introduce an optional configuration flag on `TransactionGetReceiptQuery` that allows callers to explicitly request child receipts. @@ -145,67 +154,216 @@ body: ``` - type: textarea - id: implementation + id: research attributes: - label: 🧠 Implementation Notes + label: 🔍 Background Research description: | - Provide technical guidance to help implementation. - - Examples: - - files or modules likely involved - - patterns already used elsewhere - - things to be careful about - - known edge cases + POINT THE CONTRIBUTOR TOWARD RELEVANT FILES, SIMILAR CODE, PROTOBUF DEFINITIONS, OR PATTERNS TO STUDY. + THIS IS THE "INVESTIGATE BEFORE YOU CODE" STEP. value: | - Add implementation notes here. + _Replace this with research pointers._ validations: - required: false + required: true - type: markdown attributes: value: | - - ## 🧠 Implementation Notes – Example + + ## Example — Background Research - Likely steps: + - Open `src/hiero_sdk_python/query/transaction_get_receipt_query.py` and read how the query is built + - Compare with `TransactionGetRecordQuery` which already supports similar flags + - Check the [protobuf definition](https://github.com/hashgraph/hedera-protobufs/blob/main/services/transaction_get_receipt.proto) for the response structure + - Look at how child receipts are structured in the proto response - - Add an optional boolean field (e.g. `_include_children`) to - `TransactionGetReceiptQuery` + - type: textarea + id: implementation + attributes: + label: 🛠️ Implementation + description: | + DESCRIBE WHAT THE CONTRIBUTOR SHOULD BUILD AFTER COMPLETING THEIR RESEARCH. + KEEP IT HIGH-LEVEL — THEY SHOULD FIGURE OUT THE DETAILS FROM THEIR RESEARCH. + value: | + _Replace this with implementation guidance._ + validations: + required: true + + - type: markdown + attributes: + value: | + + ## Example — Implementation + + - Add an optional boolean field (e.g. `_include_children`) to `TransactionGetReceiptQuery` - Ensure the flag is passed to the mirror node request - Update response parsing to include child receipts when present - - Extend the existing example in - `examples/query/transaction_get_receipt_query.py` - to demonstrate the new behavior + - Keep default behavior unchanged (backwards compatible) + - Extend the existing example to demonstrate the new behavior - Similar patterns can be found in other query classes that support - optional response extensions. + - type: textarea + id: testing + attributes: + label: 🧪 Testing Requirements + description: | + EDIT THIS SECTION TO MATCH THE TYPE OF CHANGE. DELETE CATEGORIES THAT DO NOT APPLY. + TESTING IS A MAJOR COMPONENT OF INTERMEDIATE ISSUES — SET CLEAR EXPECTATIONS. + value: | + How you test depends on the type of change. + See the [testing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/testing.md) for full details. + + > [!IMPORTANT] + > At the intermediate level, **testing is a major component** — not an afterthought. + > Each method you implement should be verified. Tests should cover happy paths, + > edge cases, and error handling. Tests must be understandable and reviewable, + > not just generated by AI. + + **Source code changes (e.g. Python files in `src/`):** + - Write unit tests covering happy path, edge cases, and error handling + - Run tests locally: `uv run pytest tests/unit/_test.py -v` + - Verify naming, types, and field ordering match [protobuf definitions](https://github.com/hashgraph/hedera-protobufs/tree/main/services) + - Check consistency with similar classes already in the SDK — use the same patterns + - Integration tests will run automatically when you push + + **GitHub Actions / workflow changes (e.g. files in `.github/`):** + - Test by merging to your fork's `main` and simulating the scenario + - Create test issues or PRs as evidence and link them in your PR + - Include screenshots of workflow runs where applicable + + **Example script changes (e.g. files in `examples/`):** + - Run the example script and confirm output matches expected behavior + - Compare your example with similar existing examples for consistency + - You will need a [Hedera Portal](https://portal.hedera.com/) account for testnet credentials + + **In your PR, include links to test evidence:** screenshots, terminal output, test issue/PRs, or script results. + validations: + required: true - type: textarea - id: acceptance-criteria + id: quality_standards attributes: - label: ✅ Acceptance Criteria - description: Define what "done" means for this issue + label: 🛡️ Quality & Review Standards + description: | + PRE-FILLED GUIDANCE. LEAVE AS-IS. value: | - To merge this issue, the pull request must: + Intermediate PRs cover more code and touch more sensitive areas. This section helps you + deliver a PR that is ready to merge on first or second review. - - [ ] Fully address the problem described above - - [ ] Follow existing project conventions and patterns - - [ ] Include tests or example updates where appropriate - - [ ] Pass all CI checks - - [ ] Include a valid changelog entry - - [ ] be a DCO and GPG key signed as `git commit -S -s -m "chore: my change"` with a GPG key set up + **⚠️ Breaking changes** + - Before changing any function signature, return type, or public API — stop and check + - If a breaking change is unavoidable: add backwards compatibility and get explicit maintainer approval **before** implementing + - Run existing tests to verify nothing breaks: `uv run pytest tests/unit/ -v` + + **🤖 AI-assisted code** + - AI is a powerful tool — but every suggestion must be verified, not just accepted + - Cross-reference AI output against [protobuf definitions](https://github.com/hashgraph/hedera-protobufs/tree/main/services) and similar SDK implementations + - Do not submit AI-generated tests that merely test AI-generated code — tests should verify real behavior and edge cases + - If AI output and existing SDK patterns disagree, trust the SDK + + **📦 PR packaging** + - Self-review your diff line by line before submitting + - Clean git history — no rebase artifacts, merge commits, or unrelated files + - CHANGELOG entry under `[Unreleased]` + - Link test evidence (screenshots, terminal output, test PRs) in your PR description + - Ensure all CI checks pass before requesting review + - Double and triple check — intermediate PRs are time-consuming to review + validations: + required: false + + - type: textarea + id: acceptance + attributes: + label: ✅ Done Checklist + description: | + EDIT OR ADD CRITERIA SPECIFIC TO THIS ISSUE. THE DEFAULTS COVER THE STANDARD REQUIREMENTS. + value: | + Before opening your PR, confirm: + + - [ ] My changes fully address the problem described above + - [ ] I verified naming, types, and patterns against protobuf definitions and similar SDK classes + - [ ] I tested my changes thoroughly (see testing section above) and linked evidence in my PR + - [ ] I checked for breaking changes — no existing tests fail, no public APIs changed without approval + - [ ] I did not modify files unrelated to this issue + - [ ] My commits are signed: `git commit -S -s -m "chore: description"` — [Signing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/signing.md) + - [ ] I added a CHANGELOG.md entry under `[Unreleased]` — [Changelog guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/changelog_entry.md) + - [ ] All CI checks pass + - [ ] I self-reviewed my PR before requesting review validations: required: true - type: textarea - id: additional-info + id: contribution_steps attributes: - label: 📚 Additional Context or Resources + label: 📋 Workflow quick reference description: | - Add any links, references, or extra notes that may help. + PRE-FILLED REFERENCE TABLE. LEAVE AS-IS. value: | - - [SDK Developer Docs](https://github.com/hiero-ledger/hiero-sdk-python/tree/main/docs/sdk_developers) - - [SDK Developer Training](https://github.com/hiero-ledger/hiero-sdk-python/tree/main/docs/sdk_developers/training) + You know the workflow — here are the links if you need them: + + | Step | Guide | + |------|-------| + | Claim this issue | Comment `/assign` below | + | Sync with main | [Rebasing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/rebasing.md) | + | Open a PR and link this issue | [PR guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/11_submit_pull_request.md) · [Linking guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/how_to_link_issues.md) | + | Resolve merge conflicts | [Merge conflicts guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/merge_conflicts.md) | + | Testing | [Testing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/testing.md) | + validations: + required: false + + - type: textarea + id: ai_tips + attributes: + label: 🤖 Tips for using AI tools + description: | + PRE-FILLED GUIDANCE. LEAVE AS-IS. + value: | + Here is how to get the most out of AI tools at the intermediate level: + + - Use [Pylance](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/pylance.md) in VS Code to catch type errors and hallucinated methods in real time + - Always verify AI suggestions against [protobuf definitions](https://github.com/hashgraph/hedera-protobufs/tree/main/services) — AI often gets field names, types, or ordering wrong + - Build your solution in small steps — add print statements or logs to confirm each piece works before moving on + - When AI suggests code, check it against similar classes in the SDK — that is the best source of Python SDK-specific patterns + - If AI output and SDK patterns disagree, trust the SDK + - Write tests that verify real behavior, not tests that test AI-generated code + validations: + required: false + + - type: textarea + id: stuck + attributes: + label: 🆘 Stuck? + description: | + PRE-FILLED GUIDANCE. LEAVE AS-IS. + value: | + Intermediate issues can be challenging — it is completely normal to get blocked. Here is what to do: + + - **Comment on this issue** and describe what you have tried. A maintainer will respond. + - **Ask on [Discord](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md)** in #hiero-python-sdk for quick questions. + - **Join [Office Hours](https://zoom-lfx.platform.linuxfoundation.org/meeting/99912667426?password=5b584a0e-1ed7-49d3-b2fc-dc5ddc888338)** (Wednesdays, 2pm UTC) for live, hands-on help with a maintainer — screen sharing welcome. + + At this level, you are expected to research independently — but asking good, high-level questions is a strength, not a weakness. Don't spend more than an hour blocked without asking. + validations: + required: false + + - type: textarea + id: resources + attributes: + label: 📚 Resources + description: | + ADD ANY TASK-SPECIFIC RESOURCES BELOW — RELEVANT PROTOBUF DEFINITIONS, PYTHON DOCS, OR SPECIFIC FILES IN THE CODEBASE. + value: | + **Project references:** + - [Project structure](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/setup/project_structure.md) + - [CONTRIBUTING.md](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/CONTRIBUTING.md) + - [Browse closed intermediate PRs](https://github.com/hiero-ledger/hiero-sdk-python/pulls?q=is%3Apr+is%3Amerged+label%3Aintermediate) — see how others did it + + **Protobuf references:** + - [Hedera Protobufs](https://github.com/hashgraph/hedera-protobufs/tree/main/services) — source of truth for field names, types, and ordering + - [Protobuf language guide](https://protobuf.dev/programming-guides/proto3/) + **Python references:** + - [Python official docs](https://docs.python.org/3/) + - [Data model (dunder methods)](https://docs.python.org/3/reference/datamodel.html) + - [Type hints](https://docs.python.org/3/library/typing.html) + - [unittest.mock](https://docs.python.org/3/library/unittest.mock.html) — for mocking in tests validations: required: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 790efe344..a0fed5ea4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Changelog +# Changelog All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org). @@ -21,6 +21,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ### .github +- Refactored intermediate issue template with quality standards, testing requirements, breaking change awareness, and protobuf verification guidance to reduce review burden and improve PR quality (#1892) - fix: prevent CodeRabbit from posting comments on closed issues(#1962) From 18768b72c2ce98b61ee3e460bf3044a3a7343c5f Mon Sep 17 00:00:00 2001 From: Edward Yi <41576951+aiedwardyi@users.noreply.github.com> Date: Mon, 23 Mar 2026 01:57:15 +0900 Subject: [PATCH 08/60] chore: rename delegate_contract_id to delegate_contract_id_test (#2014) Signed-off-by: Edward Yi <41576951+aiedwardyi@users.noreply.github.com> --- CHANGELOG.md | 1 + .../{delegate_contract_id.py => delegate_contract_id_test.py} | 0 2 files changed, 1 insertion(+) rename tests/unit/{delegate_contract_id.py => delegate_contract_id_test.py} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0fed5ea4..47ac84621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ### Tests - Added TCK endpoint for the createAccount method +- Renamed `delegate_contract_id.py` to `delegate_contract_id_test.py` (#2004) ### Docs - Add Chocolatey as a prerequisite in the Windows setup guide (#1961) diff --git a/tests/unit/delegate_contract_id.py b/tests/unit/delegate_contract_id_test.py similarity index 100% rename from tests/unit/delegate_contract_id.py rename to tests/unit/delegate_contract_id_test.py From 93af730a2d184e85b49ef001ac7d6aabf2efea8c Mon Sep 17 00:00:00 2001 From: exploreriii <133720349+exploreriii@users.noreply.github.com> Date: Mon, 23 Mar 2026 04:07:49 +0000 Subject: [PATCH 09/60] chore: spam list update (#2017) Signed-off-by: exploreriii <133720349+exploreriii@users.noreply.github.com> --- .github/spam-list.txt | 4 +++- CHANGELOG.md | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/spam-list.txt b/.github/spam-list.txt index 22c767db4..ed4c4b48b 100644 --- a/.github/spam-list.txt +++ b/.github/spam-list.txt @@ -6,4 +6,6 @@ Halbot100 roberthallers SergioChan ndpvt-web -OnlyTerp \ No newline at end of file +OnlyTerp +shixian-HFUT +Yuki9814 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 47ac84621..429c21025 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ### .github - Refactored intermediate issue template with quality standards, testing requirements, breaking change awareness, and protobuf verification guidance to reduce review burden and improve PR quality (#1892) - fix: prevent CodeRabbit from posting comments on closed issues(#1962) +- chore: update spam list #1988 From f639c82eb509a540c1d9ebaf42ba14a835a15ca5 Mon Sep 17 00:00:00 2001 From: Manish Dait <90558243+manishdait@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:35:47 +0530 Subject: [PATCH 10/60] fix: Flaky unit tests for `mock_server` by enforcing non-tls port and adding a mock_tls certificate (#2012) Signed-off-by: Manish Dait --- CHANGELOG.md | 3 ++- tests/unit/mock_server.py | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 429c21025..d40d6dd6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Changelog +# Changelog All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org). @@ -16,6 +16,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ### Tests - Added TCK endpoint for the createAccount method - Renamed `delegate_contract_id.py` to `delegate_contract_id_test.py` (#2004) +- Fix Flaky tests for `mock_server` by enforcing non-tls port and adding a mock_tls certificate ### Docs - Add Chocolatey as a prerequisite in the Windows setup guide (#1961) diff --git a/tests/unit/mock_server.py b/tests/unit/mock_server.py index 50bee264d..805a76711 100644 --- a/tests/unit/mock_server.py +++ b/tests/unit/mock_server.py @@ -130,7 +130,13 @@ def _find_free_port(): with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: s.bind(("", 0)) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - return s.getsockname()[1] + port = s.getsockname()[1] + + # If we get the tls port 50212 port skip it + if port in [50212]: + return port + 1 + + return port class RealRpcError(grpc.RpcError): @@ -167,7 +173,8 @@ def mock_hedera_servers(response_sequences): for i, server in enumerate(servers): node = _Node(AccountId(0, 0, 3 + i), server.address, None) - # force insecure transport + # force insecure transport and mock cert even if we get the tls-port + node._set_root_certificates(b"mock-tls-cert-for-unit-tests") node._apply_transport_security(False) node._set_verify_certificates(False) From 1a6720c270421f333f650685d19d409fe5f9e6d0 Mon Sep 17 00:00:00 2001 From: Rayan Kamdem Date: Tue, 24 Mar 2026 07:22:01 -0400 Subject: [PATCH 11/60] chore: update remaining bot workflows to self-hosted runners (#2020) Signed-off-by: Rayan Kamdem --- .github/workflows/bot-advanced-check.yml | 2 +- .github/workflows/bot-gfi-assign-on-comment.yml | 2 +- .github/workflows/bot-intermediate-assignment.yml | 2 +- .github/workflows/bot-linked-issue-enforcer.yml | 2 +- .github/workflows/unassign-on-comment.yml | 2 +- .github/workflows/working-on-comment.yml | 2 +- CHANGELOG.md | 1 + 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/bot-advanced-check.yml b/.github/workflows/bot-advanced-check.yml index 8c39dc376..3548ef07f 100644 --- a/.github/workflows/bot-advanced-check.yml +++ b/.github/workflows/bot-advanced-check.yml @@ -25,7 +25,7 @@ jobs: ####################################### check-advanced-qualification: # steps are skipped, job completes successfully - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 diff --git a/.github/workflows/bot-gfi-assign-on-comment.yml b/.github/workflows/bot-gfi-assign-on-comment.yml index a431bdb3c..9e3d248c2 100644 --- a/.github/workflows/bot-gfi-assign-on-comment.yml +++ b/.github/workflows/bot-gfi-assign-on-comment.yml @@ -14,7 +14,7 @@ jobs: # Only run on issue comments (not PR comments) if: github.event.issue.pull_request == null - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md # Prevent assignment-limit races across different issues by processing # one GFI assignment workflow at a time for this repository. diff --git a/.github/workflows/bot-intermediate-assignment.yml b/.github/workflows/bot-intermediate-assignment.yml index 60b8f525c..93bcf66b3 100644 --- a/.github/workflows/bot-intermediate-assignment.yml +++ b/.github/workflows/bot-intermediate-assignment.yml @@ -23,7 +23,7 @@ jobs: group: intermediate-guard-${{ github.event.issue.number || github.run_id }} cancel-in-progress: false if: contains(github.event.issue.labels.*.name, 'intermediate') - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 diff --git a/.github/workflows/bot-linked-issue-enforcer.yml b/.github/workflows/bot-linked-issue-enforcer.yml index 5bc32c76b..69fcf9aa4 100644 --- a/.github/workflows/bot-linked-issue-enforcer.yml +++ b/.github/workflows/bot-linked-issue-enforcer.yml @@ -18,7 +18,7 @@ permissions: jobs: pr-linked-issue-checker: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md env: DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} diff --git a/.github/workflows/unassign-on-comment.yml b/.github/workflows/unassign-on-comment.yml index 52ed9e070..7084e98b7 100644 --- a/.github/workflows/unassign-on-comment.yml +++ b/.github/workflows/unassign-on-comment.yml @@ -14,7 +14,7 @@ jobs: # Only run on issue comments (not PR comments) if: github.event.issue.pull_request == null - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md concurrency: group: unassign-${{ github.event.issue.number }} diff --git a/.github/workflows/working-on-comment.yml b/.github/workflows/working-on-comment.yml index 515e2bda0..261e5e754 100644 --- a/.github/workflows/working-on-comment.yml +++ b/.github/workflows/working-on-comment.yml @@ -17,7 +17,7 @@ permissions: jobs: working-command: if: github.event_name == 'workflow_dispatch' || (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/working')) - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md permissions: issues: write contents: read diff --git a/CHANGELOG.md b/CHANGELOG.md index d40d6dd6d..0f12f09e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Refactored intermediate issue template with quality standards, testing requirements, breaking change awareness, and protobuf verification guidance to reduce review burden and improve PR quality (#1892) - fix: prevent CodeRabbit from posting comments on closed issues(#1962) - chore: update spam list #1988 +- chore: Update `bot-advanced-check.yml`, `bot-gfi-assign-on-comment.yml`, `bot-intermediate-assignment.yml`, `bot-linked-issue-enforcer.yml`, `unassign-on-comment.yml`, `working-on-comment.yml` workflow runner configuration From 0994ad23359e01ba00959115f8253eb6b8972dd4 Mon Sep 17 00:00:00 2001 From: notsogod <149138960+Adityarya11@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:14:59 +0530 Subject: [PATCH 12/60] feat: Added support for Keys in the AccUpdateTrxns. (#2007) Signed-off-by: Adityarya11 --- CHANGELOG.md | 1 + .../account/account_update_transaction.py | 23 +-- ...account_update_transaction_with_keylist.py | 136 ++++++++++++++++++ src/hiero_sdk_python/account/account_info.py | 8 +- .../account/account_update_transaction.py | 14 +- .../account_update_transaction_e2e_test.py | 54 +++++++ tests/unit/account_update_transaction_test.py | 42 ++++++ 7 files changed, 249 insertions(+), 29 deletions(-) create mode 100644 examples/account/account_update_transaction_with_keylist.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f12f09e7..777fbc5bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ## [Unreleased] ### Src +- Updated `AccountUpdateTransaction.set_key()` to accept generic `Key` objects (including `KeyList` and threshold keys), rather than strictly requiring a `PublicKey`. - Fix the TransactionGetReceiptQuery to raise ReceiptStatusError for the non-retryable and non success receipt status - Refactor `AccountInfo` to use the existing `StakingInfo` wrapper class instead of flattened staking fields. Access is now via `info.staking_info.staked_account_id`, `info.staking_info.staked_node_id`, and `info.staking_info.decline_reward`. The old flat accessors (`info.staked_account_id`, `info.staked_node_id`, `info.decline_staking_reward`) are still available as deprecated properties and will emit a `DeprecationWarning`. (#1366) - Added abstract `Key` supper class to handle various proto Keys. diff --git a/examples/account/account_update_transaction.py b/examples/account/account_update_transaction.py index b68355767..57c06d113 100644 --- a/examples/account/account_update_transaction.py +++ b/examples/account/account_update_transaction.py @@ -8,34 +8,21 @@ python examples/account/account_update_transaction.py """ import datetime -import os import sys -from dotenv import load_dotenv - -from hiero_sdk_python import AccountId, Client, Duration, Hbar, Network, PrivateKey +from hiero_sdk_python import Client, Duration, Hbar, PrivateKey from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.account.account_update_transaction import AccountUpdateTransaction from hiero_sdk_python.query.account_info_query import AccountInfoQuery from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.timestamp import Timestamp -load_dotenv() - -network_name = os.getenv("NETWORK", "testnet").lower() - -def setup_client(): - """Initialize and set up the client with operator account.""" - network = Network(network_name) - print(f"Connecting to Hedera {network_name} network!") - client = Client(network) - - operator_id = AccountId.from_string(os.getenv("OPERATOR_ID", "")) - operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY", "")) - client.set_operator(operator_id, operator_key) +def setup_client() -> Client: + """Setup Client.""" + client = Client.from_env() + print(f"Network: {client.network.network}") print(f"Client set up with operator id {client.operator_account_id}") - return client diff --git a/examples/account/account_update_transaction_with_keylist.py b/examples/account/account_update_transaction_with_keylist.py new file mode 100644 index 000000000..d63938214 --- /dev/null +++ b/examples/account/account_update_transaction_with_keylist.py @@ -0,0 +1,136 @@ +""" +Example demonstrating account update functionality with key lists (multi-signature threshold keys). + +run with: +uv run examples/account/account_update_transaction_with_keylist.py +python examples/account/account_update_transaction_with_keylist.py + +""" +import sys + +from hiero_sdk_python import Client, Hbar, PrivateKey +from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction +from hiero_sdk_python.account.account_update_transaction import AccountUpdateTransaction +from hiero_sdk_python.crypto.key_list import KeyList +from hiero_sdk_python.query.account_info_query import AccountInfoQuery +from hiero_sdk_python.response_code import ResponseCode + + +def setup_client() -> Client: + """Setup Client.""" + client = Client.from_env() + print(f"Network: {client.network.network}") + print(f"Client set up with operator id {client.operator_account_id}") + return client + + +def create_account(client): + """Create a test account.""" + account_private_key = PrivateKey.generate_ed25519() + account_public_key = account_private_key.public_key() + + receipt = ( + AccountCreateTransaction() + .set_key_without_alias(account_public_key) + .set_initial_balance(Hbar(1)) + .set_account_memo("Test account for update with keylist") + .freeze_with(client) + .sign(account_private_key) + .execute(client) + ) + + if receipt.status != ResponseCode.SUCCESS: + print( + f"Account creation failed with status: {ResponseCode(receipt.status).name}" + ) + sys.exit(1) + + account_id = receipt.account_id + print(f"\nAccount created with ID: {account_id}") + + return account_id, account_private_key + + +def query_account_info(client, account_id): + """Query and display account information.""" + info = AccountInfoQuery(account_id).execute(client) + + print(f"Account ID: {info.account_id}") + print(f"Account Balance: {info.balance}") + print(f"Account Memo: '{info.account_memo}'") + print(f"Public Key: {info.key}") + + +def account_update_with_keylist(): + """ + Demonstrates account update functionality using a threshold key list by: + + 1. Setting up client with operator account + 2. Creating a test account + 3. Updating the account to use a KeyList with threshold + 4. Proving that the new account key is active by signing an update + """ + client = setup_client() + + # Create a test account first + account_id, current_private_key = create_account(client) + + print("\nAccount info before update:") + # Query the account info + query_account_info(client, account_id) + + # Rotate from a single key to a threshold KeyList (2 of 2). + threshold_key_1 = PrivateKey.generate_ed25519() + threshold_key_2 = PrivateKey.generate_ed25519() + threshold_key = KeyList( + [threshold_key_1.public_key(), threshold_key_2.public_key()], threshold=2 + ) + + print("\nRotating account key to a 2-of-2 threshold KeyList...") + key_list_receipt = ( + AccountUpdateTransaction() + .set_account_id(account_id) + .set_key(threshold_key) + .freeze_with(client) + .sign(current_private_key) # Sign with current key + .sign(threshold_key_1) # First signature for threshold=2 + .sign(threshold_key_2) # Second signature for threshold=2 + .execute(client) + ) + + if key_list_receipt.status != ResponseCode.SUCCESS: + print( + f"KeyList rotation failed with status: {ResponseCode(key_list_receipt.status).name}" + ) + sys.exit(1) + + print("\nAccount info after KeyList update:") + query_account_info(client, account_id) + + # Prove the new account key is active by signing with both threshold keys. + print("\nProving the new account key by updating the memo using both threshold keys...") + memo_receipt = ( + AccountUpdateTransaction() + .set_account_id(account_id) + .set_account_memo("Updated account memo with threshold key") + .freeze_with(client) + .sign(threshold_key_1) + .sign(threshold_key_2) + .execute(client) + ) + + if memo_receipt.status != ResponseCode.SUCCESS: + print( + "Memo update with threshold key failed with status: " + f"{ResponseCode(memo_receipt.status).name}" + ) + sys.exit(1) + + print("\nAccount info after memo update:") + query_account_info(client, account_id) + + print("\nThreshold KeyList rotation and follow-up update succeeded.") + + +if __name__ == "__main__": + account_update_with_keylist() \ No newline at end of file diff --git a/src/hiero_sdk_python/account/account_info.py b/src/hiero_sdk_python/account/account_info.py index b97db4998..477da0351 100644 --- a/src/hiero_sdk_python/account/account_info.py +++ b/src/hiero_sdk_python/account/account_info.py @@ -9,7 +9,7 @@ from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.staking_info import StakingInfo -from hiero_sdk_python.crypto.public_key import PublicKey +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.Duration import Duration from hiero_sdk_python.hapi.services.crypto_get_info_pb2 import CryptoGetInfoResponse from hiero_sdk_python.hbar import Hbar @@ -27,7 +27,7 @@ class AccountInfo: contract_account_id (Optional[str]): The contract account ID. is_deleted (Optional[bool]): Whether the account has been deleted. proxy_received (Optional[Hbar]): The total number of tinybars proxy staked to this account. - key (Optional[PublicKey]): The key for this account. + key (Optional[Key]): The key for this account. balance (Optional[Hbar]): The current balance of account in hbar. receiver_signature_required (Optional[bool]): If true, this account's key must sign any transaction depositing into this account. @@ -46,7 +46,7 @@ class AccountInfo: contract_account_id: Optional[str] = None is_deleted: Optional[bool] = None proxy_received: Optional[Hbar] = None - key: Optional[PublicKey] = None + key: Optional[Key] = None balance: Optional[Hbar] = None receiver_signature_required: Optional[bool] = None expiration_time: Optional[Timestamp] = None @@ -81,7 +81,7 @@ def _from_proto(cls, proto: CryptoGetInfoResponse.AccountInfo) -> "AccountInfo": contract_account_id=proto.contractAccountID, is_deleted=proto.deleted, proxy_received=Hbar.from_tinybars(proto.proxyReceived), - key=PublicKey._from_proto(proto.key) if proto.key else None, + key=Key.from_proto_key(proto.key) if proto.key else None, balance=Hbar.from_tinybars(proto.balance), receiver_signature_required=proto.receiverSigRequired, expiration_time=( diff --git a/src/hiero_sdk_python/account/account_update_transaction.py b/src/hiero_sdk_python/account/account_update_transaction.py index 27719679f..acca7e3bb 100644 --- a/src/hiero_sdk_python/account/account_update_transaction.py +++ b/src/hiero_sdk_python/account/account_update_transaction.py @@ -10,7 +10,7 @@ from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.channels import _Channel -from hiero_sdk_python.crypto.public_key import PublicKey +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.Duration import Duration from hiero_sdk_python.executable import _Method from hiero_sdk_python.hapi.services.crypto_update_pb2 import CryptoUpdateTransactionBody @@ -30,7 +30,7 @@ class AccountUpdateParams: Attributes: account_id (Optional[AccountId]): The account ID to update. - key (Optional[PublicKey]): The new key for the account. + key (Optional[Key]): The new key for the account. auto_renew_period (Duration): The new auto-renew period. account_memo (Optional[str]): The new memo for the account. receiver_signature_required (Optional[bool]): Whether receiver signature is required. @@ -46,7 +46,7 @@ class AccountUpdateParams: """ account_id: Optional[AccountId] = None - key: Optional[PublicKey] = None + key: Optional[Key] = None auto_renew_period: Duration = AUTO_RENEW_PERIOD account_memo: Optional[str] = None receiver_signature_required: Optional[bool] = None @@ -101,12 +101,12 @@ def set_account_id(self, account_id: Optional[AccountId]) -> "AccountUpdateTrans self.account_id = account_id return self - def set_key(self, key: Optional[PublicKey]) -> "AccountUpdateTransaction": + def set_key(self, key: Optional[Key]) -> "AccountUpdateTransaction": """ - Sets the new account key (public key) for key rotation. + Sets the new account key (Optional[Key]) for key rotation. Args: - key (Optional[PublicKey]): The new public key for the account. + key (Optional[Key]): The new key for the account. Returns: AccountUpdateTransaction: This transaction instance. @@ -307,7 +307,7 @@ def _build_proto_body(self): proto_body = CryptoUpdateTransactionBody( accountIDToUpdate=self.account_id._to_proto(), - key=self.key._to_proto() if self.key else None, + key=self.key.to_proto_key() if self.key else None, memo=StringValue(value=self.account_memo) if self.account_memo is not None else None, autoRenewPeriod=( self.auto_renew_period._to_proto() if self.auto_renew_period else None diff --git a/tests/integration/account_update_transaction_e2e_test.py b/tests/integration/account_update_transaction_e2e_test.py index bfb46d810..41605fe08 100644 --- a/tests/integration/account_update_transaction_e2e_test.py +++ b/tests/integration/account_update_transaction_e2e_test.py @@ -8,6 +8,7 @@ from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.account.account_update_transaction import AccountUpdateTransaction +from hiero_sdk_python.crypto.key_list import KeyList from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.Duration import Duration from hiero_sdk_python.hbar import Hbar @@ -75,6 +76,59 @@ def test_integration_account_update_transaction_can_execute(env): ), "Auto renew period should be updated" +@pytest.mark.integration +def test_integration_account_update_transaction_set_key_with_threshold_keylist(env): + """Test rotating to a threshold KeyList and authorizing follow-up updates.""" + # Create an account controlled by the operator key (payer signature is automatic). + receipt = ( + AccountCreateTransaction() + .set_key_without_alias(env.operator_key.public_key()) + .set_initial_balance(Hbar(2)) + .execute(env.client) + ) + assert ( + receipt.status == ResponseCode.SUCCESS + ), f"Account creation failed with status: {ResponseCode(receipt.status).name}" + + account_id = receipt.account_id + assert account_id is not None, "Account ID should not be None" + + # Build a 2-of-2 threshold KeyList and rotate the account key to it. + key_1_private = PrivateKey.generate_ed25519() + key_2_private = PrivateKey.generate_ed25519() + threshold_key = KeyList( + [key_1_private.public_key(), key_2_private.public_key()], threshold=2 + ) + + receipt = ( + AccountUpdateTransaction() + .set_account_id(account_id) + .set_key(threshold_key) + .freeze_with(env.client) + .sign(key_1_private) + .sign(key_2_private) + .execute(env.client) + ) + assert ( + receipt.status == ResponseCode.SUCCESS + ), f"Account key rotation to KeyList failed with status: {ResponseCode(receipt.status).name}" + + # Verify a follow-up update can be authorized with both threshold keys. + new_memo = "Updated using threshold KeyList" + receipt = ( + AccountUpdateTransaction() + .set_account_id(account_id) + .set_account_memo(new_memo) + .freeze_with(env.client) + .sign(key_1_private) + .sign(key_2_private) + .execute(env.client) + ) + assert ( + receipt.status == ResponseCode.SUCCESS + ), f"Account update signed by threshold keys failed with status: {ResponseCode(receipt.status).name}" + + @pytest.mark.integration def test_integration_account_update_transaction_fails_with_invalid_account_id(env): """Test that AccountUpdateTransaction fails with an invalid account ID.""" diff --git a/tests/unit/account_update_transaction_test.py b/tests/unit/account_update_transaction_test.py index 62b3c5453..b5e978c0e 100644 --- a/tests/unit/account_update_transaction_test.py +++ b/tests/unit/account_update_transaction_test.py @@ -16,6 +16,7 @@ AccountUpdateParams, AccountUpdateTransaction, ) +from hiero_sdk_python.crypto.key_list import KeyList from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.hapi.services import ( response_header_pb2, @@ -826,3 +827,44 @@ def test_build_transaction_body_with_cleared_staking(mock_account_ids): txn_body = account_tx.build_transaction_body().cryptoUpdateAccount assert txn_body.staked_node_id == -1 assert not txn_body.HasField("staked_account_id") + +def test_set_keylist_multiple_keys(): + """Verify that a generic KeyList is properly converted to protobuf in AccountUpdateTransaction.""" + tx = AccountUpdateTransaction() + tx.set_account_id(AccountId.from_string("0.0.123")) + + key1 = PrivateKey.generate_ed25519().public_key() + key2 = PrivateKey.generate_ed25519().public_key() + + # Create a KeyList with two public keys + key_list = KeyList([key1, key2]) + tx.set_key(key_list) + + proto_body = tx._build_proto_body() + update_body = proto_body + + # Assert that the protobuf key has a keyList set + assert update_body.key.HasField("keyList") + assert len(update_body.key.keyList.keys) == 2 + + +def test_set_keylist_threshold_key(): + """Verify that a KeyList with a threshold is properly converted to a thresholdKey protobuf.""" + tx = AccountUpdateTransaction() + tx.set_account_id(AccountId.from_string("0.0.123")) + + key1 = PrivateKey.generate_ed25519().public_key() + key2 = PrivateKey.generate_ed25519().public_key() + key3 = PrivateKey.generate_ed25519().public_key() + + # Create a threshold key (requires 2 of 3 signatures) + threshold_key = KeyList([key1, key2, key3], threshold=2) + tx.set_key(threshold_key) + + proto_body = tx._build_proto_body() + update_body = proto_body + + # Assert that the protobuf key has a thresholdKey set + assert update_body.key.HasField("thresholdKey") + assert update_body.key.thresholdKey.threshold == 2 + assert len(update_body.key.thresholdKey.keys.keys) == 3 From 6dc812b98b6c6a650ab36dc9e5a6a3201b1c6570 Mon Sep 17 00:00:00 2001 From: exploreriii <133720349+exploreriii@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:24:16 +0000 Subject: [PATCH 13/60] ci: update several runners (#2018) Signed-off-by: exploreriii <133720349+exploreriii@users.noreply.github.com> --- .../workflows/archive/bot-gfi-notify-team.yml | 2 +- .../archive/bot-mentor-assignment.yml | 2 +- .../workflows/archive/bot-merge-conflict.yml | 6 +- .../archive/bot-next-issue-recommendation.yml | 6 +- .../archive/bot-pr-draft-ready-reminder.yaml | 6 +- .../bot-pr-linked-issue-not-assigned.yml | 6 +- .../archive/bot-pr-missing-linked-issue.yml | 6 +- .../archive/bot-verified-commits.yml | 2 +- .github/workflows/archive/pr-check-title.yml | 6 +- .../archived/bot-pr-auto-draft-on-changes.yml | 2 +- .../workflows/bot-issue-reminder-no-pr.yml | 2 +- .github/workflows/bot-office-hours.yml | 2 +- .../workflows/bot-p0-issues-notify-team.yml | 13 ++- .github/workflows/bot-pr-draft-explainer.yaml | 6 +- .../workflows/bot-pr-inactivity-reminder.yml | 11 +-- .github/workflows/bot-workflows.yml | 8 +- .github/workflows/cron-check-broken-links.yml | 8 +- .github/workflows/cron-update-spam-list.yml | 2 +- .github/workflows/pr-check-broken-links.yml | 10 +- .github/workflows/pr-check-changelog.yml | 8 +- .github/workflows/pr-check-codecov.yml | 92 +++++++++---------- .github/workflows/pr-check-test-files.yml | 2 +- .github/workflows/publish.yml | 4 +- CHANGELOG.md | 1 + docs/github/04_workflow_documentation.md | 4 +- 25 files changed, 108 insertions(+), 109 deletions(-) diff --git a/.github/workflows/archive/bot-gfi-notify-team.yml b/.github/workflows/archive/bot-gfi-notify-team.yml index 4f06782a0..472cdb869 100644 --- a/.github/workflows/archive/bot-gfi-notify-team.yml +++ b/.github/workflows/archive/bot-gfi-notify-team.yml @@ -11,7 +11,7 @@ permissions: jobs: gfi_notify_team: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md if: > github.event.issue.labels && contains( diff --git a/.github/workflows/archive/bot-mentor-assignment.yml b/.github/workflows/archive/bot-mentor-assignment.yml index cf45b81f3..c495c45ca 100644 --- a/.github/workflows/archive/bot-mentor-assignment.yml +++ b/.github/workflows/archive/bot-mentor-assignment.yml @@ -13,7 +13,7 @@ permissions: jobs: assign-mentor: if: contains(github.event.issue.labels.*.name, 'Good First Issue') - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 diff --git a/.github/workflows/archive/bot-merge-conflict.yml b/.github/workflows/archive/bot-merge-conflict.yml index 32be65009..5452a8ccd 100644 --- a/.github/workflows/archive/bot-merge-conflict.yml +++ b/.github/workflows/archive/bot-merge-conflict.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: inputs: dry_run: - description: 'Run in dry-run mode (no comments or status updates)' + description: "Run in dry-run mode (no comments or status updates)" type: boolean default: true @@ -25,7 +25,7 @@ concurrency: jobs: check-conflicts: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Checkout code uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 @@ -47,4 +47,4 @@ jobs: const scriptPath = path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/bot-merge-conflict.js') console.log(`Loading script from: ${scriptPath}`) const script = require(scriptPath) - await script({github, context, core}) \ No newline at end of file + await script({github, context, core}) diff --git a/.github/workflows/archive/bot-next-issue-recommendation.yml b/.github/workflows/archive/bot-next-issue-recommendation.yml index d8ae657e2..bf509e820 100644 --- a/.github/workflows/archive/bot-next-issue-recommendation.yml +++ b/.github/workflows/archive/bot-next-issue-recommendation.yml @@ -15,9 +15,9 @@ concurrency: jobs: recommend-next-issue: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md if: github.event.pull_request.merged == true - + steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 @@ -26,7 +26,7 @@ jobs: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - + - name: Recommend next issue uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # 8.0.0 with: diff --git a/.github/workflows/archive/bot-pr-draft-ready-reminder.yaml b/.github/workflows/archive/bot-pr-draft-ready-reminder.yaml index f3640de6b..a965907a0 100644 --- a/.github/workflows/archive/bot-pr-draft-ready-reminder.yaml +++ b/.github/workflows/archive/bot-pr-draft-ready-reminder.yaml @@ -12,7 +12,7 @@ permissions: jobs: draft-ready-reminder: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md concurrency: group: draft-ready-reminder-${{ github.event.pull_request.number }} cancel-in-progress: true @@ -32,5 +32,5 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const script = require('./.github/scripts/bot-pr-draft-ready-reminder.js'); - await script({ github, context }); \ No newline at end of file + const script = require('./.github/scripts/bot-pr-draft-ready-reminder.js'); + await script({ github, context }); diff --git a/.github/workflows/archive/bot-pr-linked-issue-not-assigned.yml b/.github/workflows/archive/bot-pr-linked-issue-not-assigned.yml index 37af8d0a2..f1d960429 100644 --- a/.github/workflows/archive/bot-pr-linked-issue-not-assigned.yml +++ b/.github/workflows/archive/bot-pr-linked-issue-not-assigned.yml @@ -9,11 +9,11 @@ on: workflow_dispatch: inputs: pr_number: - description: 'PR number to check' + description: "PR number to check" required: true type: number dry_run: - description: 'Dry run (only log, no comments)' + description: "Dry run (only log, no comments)" required: false type: boolean default: true @@ -25,7 +25,7 @@ permissions: jobs: check-linked-issue-assignment: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md concurrency: group: bot-pr-linked-issue-not-assigned-${{ github.event.pull_request.number || github.event.inputs.pr_number }} diff --git a/.github/workflows/archive/bot-pr-missing-linked-issue.yml b/.github/workflows/archive/bot-pr-missing-linked-issue.yml index 9bb5c32dc..f6bfc01b0 100644 --- a/.github/workflows/archive/bot-pr-missing-linked-issue.yml +++ b/.github/workflows/archive/bot-pr-missing-linked-issue.yml @@ -9,11 +9,11 @@ on: workflow_dispatch: inputs: pr_number: - description: 'PR number to check' + description: "PR number to check" required: true type: number dry_run: - description: 'Dry run (only log, no comments)' + description: "Dry run (only log, no comments)" required: false type: boolean default: true @@ -25,7 +25,7 @@ permissions: jobs: check-linked-issue: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md concurrency: group: bot-pr-missing-linked-issue-${{ github.event.pull_request.number || github.event.inputs.pr_number }} diff --git a/.github/workflows/archive/bot-verified-commits.yml b/.github/workflows/archive/bot-verified-commits.yml index 316a0e693..893196baa 100644 --- a/.github/workflows/archive/bot-verified-commits.yml +++ b/.github/workflows/archive/bot-verified-commits.yml @@ -34,7 +34,7 @@ concurrency: jobs: verify-commits: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md # ========================================================================= # CONFIGURATION - All customizable values are defined here as env vars diff --git a/.github/workflows/archive/pr-check-title.yml b/.github/workflows/archive/pr-check-title.yml index bbcf511cb..a20328746 100644 --- a/.github/workflows/archive/pr-check-title.yml +++ b/.github/workflows/archive/pr-check-title.yml @@ -1,4 +1,4 @@ -name: 'PR Formatting' +name: "PR Formatting" on: workflow_dispatch: pull_request_target: @@ -24,14 +24,14 @@ concurrency: jobs: title-check: name: Title Check - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md if: ${{ !github.event.pull_request.base.repo.fork }} permissions: checks: write statuses: write steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit diff --git a/.github/workflows/archived/bot-pr-auto-draft-on-changes.yml b/.github/workflows/archived/bot-pr-auto-draft-on-changes.yml index 619a3a5c3..2c806093d 100644 --- a/.github/workflows/archived/bot-pr-auto-draft-on-changes.yml +++ b/.github/workflows/archived/bot-pr-auto-draft-on-changes.yml @@ -15,7 +15,7 @@ concurrency: jobs: convert-to-draft: if: ${{ github.event.review.state == 'changes_requested' }} - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner (Audit all outbound calls) diff --git a/.github/workflows/bot-issue-reminder-no-pr.yml b/.github/workflows/bot-issue-reminder-no-pr.yml index 08b634edb..dd35eb490 100644 --- a/.github/workflows/bot-issue-reminder-no-pr.yml +++ b/.github/workflows/bot-issue-reminder-no-pr.yml @@ -18,7 +18,7 @@ permissions: jobs: reminder: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner diff --git a/.github/workflows/bot-office-hours.yml b/.github/workflows/bot-office-hours.yml index 8821d1618..8334b157c 100644 --- a/.github/workflows/bot-office-hours.yml +++ b/.github/workflows/bot-office-hours.yml @@ -20,7 +20,7 @@ permissions: jobs: office-hour-reminder: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md concurrency: group: office-hour-reminder cancel-in-progress: false diff --git a/.github/workflows/bot-p0-issues-notify-team.yml b/.github/workflows/bot-p0-issues-notify-team.yml index 77c03738d..1ac7b14e2 100644 --- a/.github/workflows/bot-p0-issues-notify-team.yml +++ b/.github/workflows/bot-p0-issues-notify-team.yml @@ -1,18 +1,17 @@ # This workflow warns the team about P0 issues immediately when they are created. name: P0 Issue Alert on: - issues: - types: + types: - labeled permissions: issues: write contents: read -jobs: +jobs: p0_notify_team: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md # Only run for issues labeled with 'p0' (case-insensitive) if: > (github.event_name == 'issues' && ( @@ -20,7 +19,7 @@ jobs: ) steps: - - name: Harden the runner + - name: Harden the runner uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 with: egress-policy: audit @@ -29,10 +28,10 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1 - name: Notify team of P0 issues - env: + env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 with: script: | const script = require('./.github/scripts/bot-p0-issues-notify-team.js'); - await script({ github, context}); \ No newline at end of file + await script({ github, context}); diff --git a/.github/workflows/bot-pr-draft-explainer.yaml b/.github/workflows/bot-pr-draft-explainer.yaml index 3a1c031c9..2a4a62267 100644 --- a/.github/workflows/bot-pr-draft-explainer.yaml +++ b/.github/workflows/bot-pr-draft-explainer.yaml @@ -23,7 +23,7 @@ permissions: jobs: pr-draft-explainer: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md concurrency: group: pr-draft-explainer-${{ github.event.pull_request.number || github.event.inputs.pr_number }} cancel-in-progress: true @@ -46,5 +46,5 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 with: script: | - const script = require('./.github/scripts/bot-pr-draft-explainer.js'); - await script({ github, context }); \ No newline at end of file + const script = require('./.github/scripts/bot-pr-draft-explainer.js'); + await script({ github, context }); diff --git a/.github/workflows/bot-pr-inactivity-reminder.yml b/.github/workflows/bot-pr-inactivity-reminder.yml index c710ca653..ab6098d11 100644 --- a/.github/workflows/bot-pr-inactivity-reminder.yml +++ b/.github/workflows/bot-pr-inactivity-reminder.yml @@ -4,23 +4,22 @@ name: PR Inactivity Reminder Bot on: schedule: - - cron: '0 11 * * *' + - cron: "0 11 * * *" workflow_dispatch: inputs: dry_run: description: 'If true, do not post comments (dry run). Accepts "true" or "false". Default true for manual runs.' required: false - default: 'true' + default: "true" permissions: pull-requests: write issues: write contents: read - jobs: remind_inactive_prs: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md env: DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} @@ -30,7 +29,7 @@ jobs: with: egress-policy: audit - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Remind authors of inactive PRs env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -39,4 +38,4 @@ jobs: with: script: | const script = require('./.github/scripts/pr_inactivity_reminder.js') - await script({ github, context }); \ No newline at end of file + await script({ github, context }); diff --git a/.github/workflows/bot-workflows.yml b/.github/workflows/bot-workflows.yml index 21e0239ae..79fcf164b 100644 --- a/.github/workflows/bot-workflows.yml +++ b/.github/workflows/bot-workflows.yml @@ -1,5 +1,5 @@ name: PythonBot - Workflow Failure Notifier -on: +on: workflow_dispatch: workflow_run: workflows: @@ -18,7 +18,7 @@ concurrency: jobs: notify-pr: if: ${{ github.event.workflow_run.conclusion == 'failure' }} - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 @@ -34,11 +34,11 @@ jobs: HEAD_BRANCH=$(gh run view ${{ github.event.workflow_run.id }} \ --repo ${{ github.repository }} \ --json headBranch --jq '.headBranch') - + # Find the PR number for this branch (only open PRs) PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --state open --head "$HEAD_BRANCH" --json number --jq '.[0].number') echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV - + - name: Comment on PR if: env.PR_NUMBER != '' env: diff --git a/.github/workflows/cron-check-broken-links.yml b/.github/workflows/cron-check-broken-links.yml index bde2e6b6b..2ddc61712 100644 --- a/.github/workflows/cron-check-broken-links.yml +++ b/.github/workflows/cron-check-broken-links.yml @@ -2,11 +2,11 @@ name: Cron – Check Broken Markdown Links on: schedule: - - cron: '0 0 1 * *' + - cron: "0 0 1 * *" workflow_dispatch: inputs: dry_run: - description: 'Run without creating issues? (true/false)' + description: "Run without creating issues? (true/false)" required: true default: true type: boolean @@ -17,7 +17,7 @@ permissions: jobs: cron-check-broken-links: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Harden runner (audit outbound calls) uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 @@ -30,7 +30,7 @@ jobs: - name: Check Markdown links (Lychee) id: lychee uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2.8.0 - continue-on-error: true + continue-on-error: true with: args: --verbose --no-progress './**/*.md' fail: true diff --git a/.github/workflows/cron-update-spam-list.yml b/.github/workflows/cron-update-spam-list.yml index 2f333f014..e5a611665 100644 --- a/.github/workflows/cron-update-spam-list.yml +++ b/.github/workflows/cron-update-spam-list.yml @@ -20,7 +20,7 @@ env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: update-spam-list: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Harden runner (audit outbound calls) uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 diff --git a/.github/workflows/pr-check-broken-links.yml b/.github/workflows/pr-check-broken-links.yml index f7c23b6df..869da8618 100644 --- a/.github/workflows/pr-check-broken-links.yml +++ b/.github/workflows/pr-check-broken-links.yml @@ -3,14 +3,14 @@ name: PR Check – Broken Markdown Links on: pull_request: paths: - - '**/*.md' + - "**/*.md" permissions: contents: read jobs: pr-check-broken-links: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Harden runner (audit outbound calls) @@ -24,6 +24,6 @@ jobs: - name: Check Markdown links uses: tcort/github-action-markdown-link-check@e7c7a18363c842693fadde5d41a3bd3573a7a225 # v1.1.2 with: - use-quiet-mode: 'yes' - check-modified-files-only: 'yes' - base-branch: 'main' + use-quiet-mode: "yes" + check-modified-files-only: "yes" + base-branch: "main" diff --git a/.github/workflows/pr-check-changelog.yml b/.github/workflows/pr-check-changelog.yml index f4355a3e5..17294b252 100644 --- a/.github/workflows/pr-check-changelog.yml +++ b/.github/workflows/pr-check-changelog.yml @@ -1,4 +1,4 @@ -name: 'PR Changelog Check' +name: "PR Changelog Check" on: pull_request: @@ -9,7 +9,7 @@ permissions: jobs: changelog-check: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: @@ -22,7 +22,7 @@ jobs: - name: Run local changelog check env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | chmod +x .github/scripts/pr-check-changelog.sh - bash .github/scripts/pr-check-changelog.sh \ No newline at end of file + bash .github/scripts/pr-check-changelog.sh diff --git a/.github/workflows/pr-check-codecov.yml b/.github/workflows/pr-check-codecov.yml index 4ab1b1bc3..cf2c67ff2 100644 --- a/.github/workflows/pr-check-codecov.yml +++ b/.github/workflows/pr-check-codecov.yml @@ -1,54 +1,54 @@ name: Code Coverage on: - push: - branches: [main] - pull_request: + push: + branches: [main] + pull_request: permissions: contents: read jobs: - coverage: - runs-on: ubuntu-latest - - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 - with: - egress-policy: audit - - - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - with: - fetch-depth: 2 - - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: "3.11" - - - name: Install uv - uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 - - - name: Install dependencies - run: | - uv sync --all-extras --dev - - - name: Generate Proto Files - run: uv run python generate_proto.py - - - name: Run unit tests and generate coverage report - run: | - uv run pytest tests/unit \ - --cov=src \ - --cov-report=xml \ - --cov-report=term-missing - - - name: Upload coverage to Codecov - if: github.repository_owner == 'hiero-ledger' - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: coverage.xml - fail_ci_if_error: true + coverage: + runs-on: hl-sdk-py-lin-md + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 2 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.11" + + - name: Install uv + uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 + + - name: Install dependencies + run: | + uv sync --all-extras --dev + + - name: Generate Proto Files + run: uv run python generate_proto.py + + - name: Run unit tests and generate coverage report + run: | + uv run pytest tests/unit \ + --cov=src \ + --cov-report=xml \ + --cov-report=term-missing + + - name: Upload coverage to Codecov + if: github.repository_owner == 'hiero-ledger' + uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml + fail_ci_if_error: true diff --git a/.github/workflows/pr-check-test-files.yml b/.github/workflows/pr-check-test-files.yml index b2593cd62..feb4d917a 100644 --- a/.github/workflows/pr-check-test-files.yml +++ b/.github/workflows/pr-check-test-files.yml @@ -24,7 +24,7 @@ jobs: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - + - name: Set up Hiero SDK Upstream run: | git remote set-url origin https://github.com/hiero-ledger/hiero-sdk-python.git diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 175e90f9a..f777ab7d6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,14 +3,14 @@ name: Publish to PyPI on: push: tags: - - 'v*.*.*' + - "v*.*.*" permissions: contents: read jobs: build-and-publish: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md environment: name: pypi url: https://pypi.org/p/hiero-sdk-python diff --git a/CHANGELOG.md b/CHANGELOG.md index 777fbc5bc..cc7b1e4c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ### .github +- chore: update several ubuntu runners to hl-sdk-py-lin-md (#1480) - Refactored intermediate issue template with quality standards, testing requirements, breaking change awareness, and protobuf verification guidance to reduce review burden and improve PR quality (#1892) - fix: prevent CodeRabbit from posting comments on closed issues(#1962) - chore: update spam list #1988 diff --git a/docs/github/04_workflow_documentation.md b/docs/github/04_workflow_documentation.md index 4687b7572..239137d99 100644 --- a/docs/github/04_workflow_documentation.md +++ b/docs/github/04_workflow_documentation.md @@ -63,7 +63,7 @@ permissions: jobs: gfi-assign: if: github.event.issue.pull_request == null - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md concurrency: group: gfi-assign-${{ github.event.issue.number }} @@ -100,7 +100,7 @@ permissions: jobs: changelog-check: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Checkout repository From d2b19dc9264381bd9c030ca13b00c8730c6d3834 Mon Sep 17 00:00:00 2001 From: Itssubhraneelbruv <66625261+Itssubhraneelbruv@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:16:54 -0700 Subject: [PATCH 14/60] Feat/1653 repr token (#2024) Signed-off-by: Itssubhraneelbruv <66625261+Itssubhraneelbruv@users.noreply.github.com> --- CHANGELOG.md | 3 +++ src/hiero_sdk_python/tokens/token_id.py | 9 +++++++++ tests/unit/token_id_test.py | 9 +++++++++ 3 files changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc7b1e4c1..120943170 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ## [Unreleased] +### Added +- Add `__repr__` method to `TokenId` class for cleaner debugging output (#1653) + ### Src - Updated `AccountUpdateTransaction.set_key()` to accept generic `Key` objects (including `KeyList` and threshold keys), rather than strictly requiring a `PublicKey`. - Fix the TransactionGetReceiptQuery to raise ReceiptStatusError for the non-retryable and non success receipt status diff --git a/src/hiero_sdk_python/tokens/token_id.py b/src/hiero_sdk_python/tokens/token_id.py index 8defa71f5..39ca06d9c 100644 --- a/src/hiero_sdk_python/tokens/token_id.py +++ b/src/hiero_sdk_python/tokens/token_id.py @@ -170,6 +170,15 @@ def __str__(self) -> str: str: The token ID string in 'shard.realm.num' format. """ return format_to_string(self.shard, self.realm, self.num) + + def __repr__(self) -> str: + """ + Returns a detailed representation of the TokenId suitable for debugging. + + Returns: + str: A string in constructor format 'TokenId(shard=X, realm=Y, num=Z)'. + """ + return f"TokenId(shard={self.shard}, realm={self.realm}, num={self.num})" def __hash__(self) -> int: """Generates a hash based on the shard, realm, and num. diff --git a/tests/unit/token_id_test.py b/tests/unit/token_id_test.py index 910e75da0..6b8189f88 100644 --- a/tests/unit/token_id_test.py +++ b/tests/unit/token_id_test.py @@ -69,3 +69,12 @@ def test_validate_checksum_failure(mock_client): with pytest.raises(ValueError): token_id.validate_checksum(client) + +def test_token_id_repr(): + """Should return constructor-style representation without checksum.""" + token_id = TokenId(0, 0, 123) + assert repr(token_id) == "TokenId(shard=0, realm=0, num=123)" + + # Test with different values + token_id2 = TokenId(1, 2, 456) + assert repr(token_id2) == "TokenId(shard=1, realm=2, num=456)" \ No newline at end of file From 0a71cd371e41d687a6971230b741318e618aa8e7 Mon Sep 17 00:00:00 2001 From: AntonioCeppellini <128388022+AntonioCeppellini@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:48:49 +0100 Subject: [PATCH 15/60] feat: add basic fuzz testing (#1896) Signed-off-by: Antonio Ceppellini --- .gitignore | 5 +- CHANGELOG.md | 4 +- pyproject.toml | 1 + pytest.ini | 3 +- tests/fuzz/__init__.py | 1 + tests/fuzz/conftest.py | 32 + tests/fuzz/support/__init__.py | 20 + tests/fuzz/support/classes.py | 61 ++ tests/fuzz/support/helpers.py | 65 ++ tests/fuzz/support/profiles.py | 31 + tests/fuzz/support/registry.py | 558 ++++++++++++++++++ .../test_contract_function_parameters_fuzz.py | 61 ++ tests/fuzz/test_entity_id_fuzz.py | 184 ++++++ tests/fuzz/test_hbar_fuzz.py | 60 ++ tests/fuzz/test_keys_fuzz.py | 112 ++++ .../fuzz/test_transaction_from_bytes_fuzz.py | 52 ++ tests/unit/hbar_test.py | 29 + 17 files changed, 1275 insertions(+), 4 deletions(-) create mode 100644 tests/fuzz/__init__.py create mode 100644 tests/fuzz/conftest.py create mode 100644 tests/fuzz/support/__init__.py create mode 100644 tests/fuzz/support/classes.py create mode 100644 tests/fuzz/support/helpers.py create mode 100644 tests/fuzz/support/profiles.py create mode 100644 tests/fuzz/support/registry.py create mode 100644 tests/fuzz/test_contract_function_parameters_fuzz.py create mode 100644 tests/fuzz/test_entity_id_fuzz.py create mode 100644 tests/fuzz/test_hbar_fuzz.py create mode 100644 tests/fuzz/test_keys_fuzz.py create mode 100644 tests/fuzz/test_transaction_from_bytes_fuzz.py diff --git a/.gitignore b/.gitignore index c85306d25..3ab60d4c1 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,10 @@ src/hiero_sdk_python/hapi # Pytest .pytest_cache +# Hypothesis state +.hypothesis/ + # Lock files uv.lock pdm.lock -pubkey.asc \ No newline at end of file +pubkey.asc diff --git a/CHANGELOG.md b/CHANGELOG.md index 120943170..b3285d7ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Added TCK endpoint for the createAccount method - Renamed `delegate_contract_id.py` to `delegate_contract_id_test.py` (#2004) - Fix Flaky tests for `mock_server` by enforcing non-tls port and adding a mock_tls certificate +- Implement basic fuzz testing [#1872](https://github.com/hiero-ledger/hiero-sdk-python/issues/1872) + ### Docs - Add Chocolatey as a prerequisite in the Windows setup guide (#1961) @@ -121,8 +123,6 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - docs: Clarify issues need to be assigned in template files. (#1884) - doc: Fix testnet link in README.md. (#1879) - - ### Tests - Format `tests/unit/endpoint_test.py` using black. (`#1792`) - Implement TCK JSON-RPC server with request handling and error management diff --git a/pyproject.toml b/pyproject.toml index 8f0981172..9b11b7f45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ dev = [ "grpcio-tools>=1.76.0,<2", "pytest>=8.3.4,<10", "pytest-cov>=7.0.0,<8", + "hypothesis>=6.137.2" ] lint = [ diff --git a/pytest.ini b/pytest.ini index ff9c1ad03..0fb601499 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,4 +4,5 @@ pythonpath = src markers = integration: mark a test as an integration test. - unit: mark a test as a unit test. \ No newline at end of file + unit: mark a test as a unit test. + fuzz: mark a test as fuzz test. diff --git a/tests/fuzz/__init__.py b/tests/fuzz/__init__.py new file mode 100644 index 000000000..2e839778b --- /dev/null +++ b/tests/fuzz/__init__.py @@ -0,0 +1 @@ +# with <3 from Anto \ No newline at end of file diff --git a/tests/fuzz/conftest.py b/tests/fuzz/conftest.py new file mode 100644 index 000000000..60048d26c --- /dev/null +++ b/tests/fuzz/conftest.py @@ -0,0 +1,32 @@ +"""Shared Hypothesis setup, fixtures, and compatibility re-exports for fuzz tests.""" + +from tests.fuzz.support.classes import ( + AccountIdAliasCase, + ContractValueCase, + EntityIdCase, + HbarConstructorCase, + HbarStringCase, + InvalidContractValueCase, +) +from tests.fuzz.support.profiles import load_hypothesis_profile +from tests.fuzz.support.registry import ( + FUZZ_STRATEGIES, + fuzz_strategies_fixture, + get_strategy, + get_strategy_fixture, +) + +load_hypothesis_profile() + +__all__ = [ + "AccountIdAliasCase", + "ContractValueCase", + "EntityIdCase", + "FUZZ_STRATEGIES", + "HbarConstructorCase", + "HbarStringCase", + "InvalidContractValueCase", + "fuzz_strategies_fixture", + "get_strategy", + "get_strategy_fixture", +] diff --git a/tests/fuzz/support/__init__.py b/tests/fuzz/support/__init__.py new file mode 100644 index 000000000..fe7052834 --- /dev/null +++ b/tests/fuzz/support/__init__.py @@ -0,0 +1,20 @@ +from tests.fuzz.support.classes import ( + AccountIdAliasCase, + ContractValueCase, + EntityIdCase, + HbarConstructorCase, + HbarStringCase, + InvalidContractValueCase, +) +from tests.fuzz.support.registry import FUZZ_STRATEGIES, get_strategy + +__all__ = [ + "AccountIdAliasCase", + "ContractValueCase", + "EntityIdCase", + "FUZZ_STRATEGIES", + "HbarConstructorCase", + "HbarStringCase", + "InvalidContractValueCase", + "get_strategy", +] diff --git a/tests/fuzz/support/classes.py b/tests/fuzz/support/classes.py new file mode 100644 index 000000000..28e076888 --- /dev/null +++ b/tests/fuzz/support/classes.py @@ -0,0 +1,61 @@ +from dataclasses import dataclass +from decimal import Decimal +from typing import Any + +from hiero_sdk_python import HbarUnit + + +@dataclass(frozen=True) +class EntityIdCase: + """A parsed entity ID expectation for public string parsers.""" + + text: str + shard: int + realm: int + value: int + checksum: str | None = None + + +@dataclass(frozen=True) +class AccountIdAliasCase: + """A valid account alias or EVM-address input.""" + + text: str + shard: int + realm: int + alias_hex: str | None = None + evm_hex: str | None = None + + +@dataclass(frozen=True) +class HbarStringCase: + """A valid public Hbar string and its exact tinybar value.""" + + text: str + tinybars: int + + +@dataclass(frozen=True) +class HbarConstructorCase: + """A valid Hbar constructor input and its exact tinybar value.""" + + amount: int | float | Decimal + unit: HbarUnit + tinybars: int + + +@dataclass(frozen=True) +class ContractValueCase: + """A valid contract parameter case routed to an explicit public add_* method.""" + + method_name: str + value: Any + + +@dataclass(frozen=True) +class InvalidContractValueCase: + """An invalid contract parameter case with a precise expected exception.""" + + method_name: str + value: Any + expected_exception: type[BaseException] diff --git a/tests/fuzz/support/helpers.py b/tests/fuzz/support/helpers.py new file mode 100644 index 000000000..fa7453133 --- /dev/null +++ b/tests/fuzz/support/helpers.py @@ -0,0 +1,65 @@ +from decimal import Decimal + +from hypothesis import strategies as st +from hypothesis.strategies import SearchStrategy + +from hiero_sdk_python import AccountId, HbarUnit, PrivateKey, TransactionId, TransferTransaction + +from tests.fuzz.support.classes import HbarConstructorCase, HbarStringCase + + +def sized_hex(byte_length: int) -> SearchStrategy[str]: + """Return a fixed-length hex string strategy.""" + return st.binary(min_size=byte_length, max_size=byte_length).map(bytes.hex) + + +def with_optional_0x(hex_strategy: SearchStrategy[str]) -> SearchStrategy[str]: + """Allow a hex string with or without the `0x` prefix.""" + return st.one_of(hex_strategy, hex_strategy.map(lambda value: f"0x{value}")) + + +def decimal_string(value: Decimal) -> str: + """Format a Decimal without trailing zeros.""" + text = format(value, "f") + if "." in text: + text = text.rstrip("0").rstrip(".") + return text or "0" + + +def hbar_string_case(unit: HbarUnit, tinybars: int) -> HbarStringCase: + """Build a valid Hbar string case from an exact tinybar amount.""" + amount = Decimal(tinybars) / Decimal(unit.tinybar) + if unit == HbarUnit.HBAR: + return HbarStringCase(text=decimal_string(amount), tinybars=tinybars) + return HbarStringCase(text=f"{decimal_string(amount)} {unit.symbol}", tinybars=tinybars) + + +def hbar_constructor_case(unit: HbarUnit, tinybars: int) -> HbarConstructorCase: + """Build a valid Hbar constructor case from an exact tinybar amount.""" + if unit == HbarUnit.TINYBAR: + amount: int | float | Decimal = tinybars + else: + amount = Decimal(tinybars) / Decimal(unit.tinybar) + return HbarConstructorCase(amount=amount, unit=unit, tinybars=tinybars) + + +def build_valid_transaction_bytes() -> tuple[bytes, bytes]: + """Build one valid unsigned and one valid signed transaction payload.""" + operator_id = AccountId.from_string("0.0.1234") + node_id = AccountId.from_string("0.0.3") + receiver_id = AccountId.from_string("0.0.5678") + + tx = ( + TransferTransaction() + .add_hbar_transfer(operator_id, -100_000_000) + .add_hbar_transfer(receiver_id, 100_000_000) + ) + tx.transaction_id = TransactionId.generate(operator_id) + tx.node_account_id = node_id + tx.freeze() + unsigned_bytes = tx.to_bytes() + + signed_tx = TransferTransaction.from_bytes(unsigned_bytes) + signed_tx.sign(PrivateKey.from_string_ed25519("02" * 32)) + signed_bytes = signed_tx.to_bytes() + return unsigned_bytes, signed_bytes diff --git a/tests/fuzz/support/profiles.py b/tests/fuzz/support/profiles.py new file mode 100644 index 000000000..5554550f2 --- /dev/null +++ b/tests/fuzz/support/profiles.py @@ -0,0 +1,31 @@ +import os + +from hypothesis import HealthCheck, settings + + +def load_hypothesis_profile() -> None: + """Register and load the active Hypothesis profile.""" + settings.register_profile( + "ci", + settings( + derandomize=True, + max_examples=300, + deadline=750, + suppress_health_check=[HealthCheck.too_slow], + ), + ) + settings.register_profile( + "local", + settings( + derandomize=False, + max_examples=1000, + deadline=None, + ), + ) + + requested = os.getenv("HYPOTHESIS_PROFILE") + if requested: + settings.load_profile(requested) + return + + settings.load_profile("ci" if os.getenv("CI") else "local") diff --git a/tests/fuzz/support/registry.py b/tests/fuzz/support/registry.py new file mode 100644 index 000000000..a5b93c1f7 --- /dev/null +++ b/tests/fuzz/support/registry.py @@ -0,0 +1,558 @@ +import string +from decimal import Decimal +from typing import Any, NamedTuple + +import pytest +from eth_abi.exceptions import EncodingTypeError, ValueOutOfBounds +from hypothesis import strategies as st +from hypothesis.strategies import SearchStrategy + +from hiero_sdk_python import HbarUnit, PrivateKey + +from tests.fuzz.support.classes import ( + AccountIdAliasCase, + ContractValueCase, + EntityIdCase, + InvalidContractValueCase, +) +from tests.fuzz.support.helpers import ( + build_valid_transaction_bytes, + hbar_constructor_case, + hbar_string_case, + sized_hex, + with_optional_0x, +) + +MAX_I64 = 2**63 - 1 +MIN_I64 = -(2**63) + + +class _IdentifierPrimitives(NamedTuple): + """Shared scalar strategies reused by the identifier-related builders.""" + + shard: SearchStrategy[int] + realm: SearchStrategy[int] + entity_num: SearchStrategy[int] + checksum: SearchStrategy[str] + + +class _KeySampleValues(NamedTuple): + """Precomputed valid key encodings reused across identifier and key strategies.""" + + ed_private_raw: str + ecdsa_private_raw: str + ed_private_der: str + ecdsa_private_der: str + ed_public_raw: str + ecdsa_public_compressed: str + ecdsa_public_uncompressed: str + ed_public_der: str + ecdsa_public_der: str + ed25519_alias_hex: str + ecdsa_alias_hex: str + evm_hex: str + + +def _build_identifier_primitives() -> _IdentifierPrimitives: + """Build the shared scalar strategies used to compose Hedera-style identifiers.""" + + return _IdentifierPrimitives( + shard=st.integers(min_value=0, max_value=4096), + realm=st.integers(min_value=0, max_value=4096), + entity_num=st.integers(min_value=0, max_value=MAX_I64), + checksum=st.text(alphabet=string.ascii_lowercase, min_size=5, max_size=5), + ) + + +def _build_key_sample_values() -> _KeySampleValues: + """Build canonical valid key samples once so all dependent strategies stay consistent.""" + + ed_private_raw = "01" * 32 + ecdsa_private_raw = "00" * 31 + "01" + + ed_private_key = PrivateKey.from_string_ed25519(ed_private_raw) + ecdsa_private_key = PrivateKey.from_string_ecdsa(ecdsa_private_raw) + ed_public_key = ed_private_key.public_key() + ecdsa_public_key = ecdsa_private_key.public_key() + + return _KeySampleValues( + ed_private_raw=ed_private_raw, + ecdsa_private_raw=ecdsa_private_raw, + ed_private_der=ed_private_key.to_bytes_der().hex(), + ecdsa_private_der=ecdsa_private_key.to_bytes_der().hex(), + ed_public_raw=ed_public_key.to_bytes_raw().hex(), + ecdsa_public_compressed=ecdsa_public_key.to_bytes_ecdsa().hex(), + ecdsa_public_uncompressed=ecdsa_public_key.to_bytes_ecdsa(compressed=False).hex(), + ed_public_der=ed_public_key.to_bytes_der().hex(), + ecdsa_public_der=ecdsa_public_key.to_bytes_der().hex(), + ed25519_alias_hex=ed_public_key.to_bytes_raw().hex(), + ecdsa_alias_hex=ecdsa_public_key.to_bytes_ecdsa().hex(), + evm_hex="abcdef0123456789abcdef0123456789abcdef01", + ) + + +def _build_entity_id_strategies( + primitives: _IdentifierPrimitives, +) -> dict[str, SearchStrategy[Any]]: + """Build valid dotted and checksum entity ID strategies with the current numeric bounds.""" + + entity_id_valid_dotted = st.builds( + lambda shard_value, realm_value, num_value: EntityIdCase( + text=f"{shard_value}.{realm_value}.{num_value}", + shard=shard_value, + realm=realm_value, + value=num_value, + ), + primitives.shard, + primitives.realm, + primitives.entity_num, + ) + entity_id_valid_checksum = st.builds( + lambda base, checksum_value: EntityIdCase( + text=f"{base.shard}.{base.realm}.{base.value}-{checksum_value}", + shard=base.shard, + realm=base.realm, + value=base.value, + checksum=checksum_value, + ), + entity_id_valid_dotted, + primitives.checksum, + ) + + return { + "entity_id_valid_dotted": entity_id_valid_dotted, + "entity_id_valid_checksum": entity_id_valid_checksum, + } + + +def _build_alias_identifier_strategies( + primitives: _IdentifierPrimitives, key_samples: _KeySampleValues +) -> dict[str, SearchStrategy[Any]]: + """Build valid account and contract alias strategies using canonical alias and EVM samples.""" + + ed25519_alias_hex = st.just(key_samples.ed25519_alias_hex) + ecdsa_alias_hex = st.just(key_samples.ecdsa_alias_hex) + evm_hex = st.just(key_samples.evm_hex) + + account_id_valid_alias = st.builds( + lambda shard_value, realm_value, alias_hex: AccountIdAliasCase( + text=f"{shard_value}.{realm_value}.{alias_hex}", + shard=shard_value, + realm=realm_value, + alias_hex=alias_hex, + ), + primitives.shard, + primitives.realm, + st.one_of(ed25519_alias_hex, ecdsa_alias_hex), + ) + account_id_valid_evm = st.one_of( + evm_hex.map(lambda value: AccountIdAliasCase(text=value, shard=0, realm=0, evm_hex=value)), + evm_hex.map( + lambda value: AccountIdAliasCase(text=f"0x{value}", shard=0, realm=0, evm_hex=value) + ), + st.builds( + lambda shard_value, realm_value, value: AccountIdAliasCase( + text=f"{shard_value}.{realm_value}.{value}", + shard=shard_value, + realm=realm_value, + evm_hex=value, + ), + primitives.shard, + primitives.realm, + evm_hex, + ), + ) + contract_id_valid_evm = st.builds( + lambda shard_value, realm_value, value: AccountIdAliasCase( + text=f"{shard_value}.{realm_value}.{value}", + shard=shard_value, + realm=realm_value, + evm_hex=value, + ), + primitives.shard, + primitives.realm, + evm_hex, + ) + + return { + "account_id_valid_alias": account_id_valid_alias, + "account_id_valid_evm": account_id_valid_evm, + "contract_id_valid_evm": contract_id_valid_evm, + } + + +def _build_invalid_identifier_strategies() -> dict[str, SearchStrategy[Any]]: + """Build malformed identifier strategies while preserving existing invalid-string coverage.""" + + invalid_entity_strings = st.one_of( + st.sampled_from( + [ + "", + ".", + "..", + "0", + "0.0", + "0.0.0.0", + "0_0_0", + "0/0/0", + "0.0.-1", + "-1.0.0", + "0.-1.0", + "abc.def.ghi", + "1e3", + "nan", + "inf", + ] + ), + st.text(alphabet=string.ascii_letters + "_-:/ ", min_size=1, max_size=48), + ) + invalid_account_strings = st.one_of( + invalid_entity_strings, + st.sampled_from( + [ + "0.0.0xabcdef0123456789abcdef0123456789abcdef01", + "0x1234", + "abcdef", + ] + ), + ) + invalid_contract_strings = st.one_of( + invalid_entity_strings, + st.sampled_from( + [ + "abcdef0123456789abcdef0123456789abcdef01", + "0xabcdef0123456789abcdef0123456789abcdef01", + "1.2.0xabcdef0123456789abcdef0123456789abcdef01", + ] + ), + ) + invalid_token_strings = st.one_of( + invalid_entity_strings, + st.sampled_from( + [ + "abcdef0123456789abcdef0123456789abcdef01", + "0xabcdef0123456789abcdef0123456789abcdef01", + "1.2.abcdef0123456789abcdef0123456789abcdef01", + ] + ), + ) + entity_id_invalid_type = st.sampled_from([None, 123, True, {}, []]) + + return { + "account_id_invalid_string": invalid_account_strings, + "token_id_invalid_string": invalid_token_strings, + "contract_id_invalid_string": invalid_contract_strings, + "entity_id_invalid_type": entity_id_invalid_type, + } + + +def _build_private_key_strategies(key_samples: _KeySampleValues) -> dict[str, SearchStrategy[Any]]: + """Build the private key strategies from the canonical valid sample encodings.""" + + private_key_valid_string = st.one_of( + with_optional_0x( + st.sampled_from([key_samples.ed_private_raw, key_samples.ed_private_der]) + ), + ) + private_key_valid_bytes = st.sampled_from( + [ + bytes.fromhex(key_samples.ed_private_raw), + bytes.fromhex(key_samples.ed_private_der), + bytes.fromhex(key_samples.ecdsa_private_der), + ] + ) + private_key_invalid_string = st.one_of( + st.sampled_from( + [ + "not-hex", + "xyz", + "0xzz", + "11" * 31, + "11" * 33, + "30", + ] + ), + st.text(alphabet="ghijklmnopqrstuvwxyz_-", min_size=1, max_size=66), + ) + private_key_invalid_bytes = st.one_of( + st.binary(min_size=0, max_size=31), + st.binary(min_size=33, max_size=96), + st.binary(min_size=32, max_size=64).map(lambda data: b"\x30" + data), + ) + + return { + "private_key_valid_string": private_key_valid_string, + "private_key_valid_bytes": private_key_valid_bytes, + "private_key_invalid_string": private_key_invalid_string, + "private_key_invalid_bytes": private_key_invalid_bytes, + } + + +def _build_public_key_strategies(key_samples: _KeySampleValues) -> dict[str, SearchStrategy[Any]]: + """Build the public key strategies from the canonical valid sample encodings.""" + + public_key_valid_string = st.one_of( + with_optional_0x( + st.sampled_from( + [ + key_samples.ed_public_raw, + key_samples.ecdsa_public_compressed, + key_samples.ecdsa_public_uncompressed, + key_samples.ed_public_der, + key_samples.ecdsa_public_der, + ] + ) + ), + ) + public_key_valid_bytes = st.sampled_from( + [ + bytes.fromhex(key_samples.ed_public_raw), + bytes.fromhex(key_samples.ecdsa_public_compressed), + bytes.fromhex(key_samples.ecdsa_public_uncompressed), + bytes.fromhex(key_samples.ed_public_der), + bytes.fromhex(key_samples.ecdsa_public_der), + ] + ) + public_key_invalid_string = st.one_of( + st.sampled_from( + [ + "not-hex", + "0xzz", + "05" + "00" * 32, + "11" * 31, + "11" * 34, + "30", + ] + ), + st.text(alphabet="ghijklmnopqrstuvwxyz_-", min_size=1, max_size=130), + ) + public_key_invalid_bytes = st.one_of( + st.binary(min_size=0, max_size=31), + st.just(bytes.fromhex("05" + "00" * 32)), + st.binary(min_size=34, max_size=64), + st.binary(min_size=32, max_size=64).map(lambda data: b"\x30" + data), + ) + + return { + "public_key_valid_string": public_key_valid_string, + "public_key_valid_bytes": public_key_valid_bytes, + "public_key_invalid_string": public_key_invalid_string, + "public_key_invalid_bytes": public_key_invalid_bytes, + } + + +def _build_key_strategies(key_samples: _KeySampleValues) -> dict[str, SearchStrategy[Any]]: + """Build the complete private and public key strategy registry entries.""" + + strategies: dict[str, SearchStrategy[Any]] = {} + strategies.update(_build_private_key_strategies(key_samples)) + strategies.update(_build_public_key_strategies(key_samples)) + return strategies + + +def _build_hbar_strategies() -> dict[str, SearchStrategy[Any]]: + """Build Hbar parsing and constructor strategies, including intentional invalid edge cases.""" + + hbar_valid_tinybars = st.integers(min_value=-(10**12), max_value=10**12) + hbar_valid_string = st.one_of( + hbar_valid_tinybars.map(lambda tinybars: hbar_string_case(HbarUnit.HBAR, tinybars)), + hbar_valid_tinybars.map(lambda tinybars: hbar_string_case(HbarUnit.MICROBAR, tinybars)), + hbar_valid_tinybars.map(lambda tinybars: hbar_string_case(HbarUnit.MILLIBAR, tinybars)), + hbar_valid_tinybars.map(lambda tinybars: hbar_string_case(HbarUnit.KILOBAR, tinybars)), + hbar_valid_tinybars.map(lambda tinybars: hbar_string_case(HbarUnit.MEGABAR, tinybars)), + hbar_valid_tinybars.map(lambda tinybars: hbar_string_case(HbarUnit.GIGABAR, tinybars)), + ) + hbar_invalid_string = st.one_of( + st.sampled_from( + [ + "1e3", + "1 hbar", + "1 tinybar", + "1 tℏ", + "1.5 tℏ", + " 1 ℏ", + "1 ℏ", + "1\tℏ", + "nan", + "inf", + "-inf", + "", + ] + ), + st.text(alphabet=string.ascii_letters + "_-", min_size=1, max_size=24), + ) + hbar_valid_constructor = st.one_of( + hbar_valid_tinybars.map(lambda tinybars: hbar_constructor_case(HbarUnit.TINYBAR, tinybars)), + hbar_valid_tinybars.map(lambda tinybars: hbar_constructor_case(HbarUnit.HBAR, tinybars)), + hbar_valid_tinybars.map(lambda tinybars: hbar_constructor_case(HbarUnit.MICROBAR, tinybars)), + ) + hbar_invalid_nonfinite_float = st.sampled_from([float("inf"), float("-inf"), float("nan")]) + fractional_tinybar_amount = st.decimals( + min_value=Decimal("-1000"), + max_value=Decimal("1000"), + allow_nan=False, + allow_infinity=False, + places=1, + ).filter(lambda value: value != value.to_integral_value()) + hbar_invalid_constructor_type = st.sampled_from(["1", None, True, object()]) + + return { + "hbar_valid_string": hbar_valid_string, + "hbar_invalid_string": hbar_invalid_string, + "hbar_valid_constructor": hbar_valid_constructor, + "hbar_invalid_nonfinite_float": hbar_invalid_nonfinite_float, + "fractional_tinybar_amount": fractional_tinybar_amount, + "hbar_invalid_constructor_type": hbar_invalid_constructor_type, + } + + +def _build_transaction_byte_strategies() -> dict[str, SearchStrategy[Any]]: + """Build valid and intentionally broken transaction byte payload strategies.""" + + unsigned_tx_bytes, signed_tx_bytes = build_valid_transaction_bytes() + tx_valid_bytes = st.sampled_from([unsigned_tx_bytes, signed_tx_bytes]) + tx_invalid_empty = st.just(b"") + tx_invalid_random = st.binary(min_size=1, max_size=8) + tx_invalid_truncated = st.sampled_from( + [unsigned_tx_bytes[:10], signed_tx_bytes[:10], unsigned_tx_bytes[:-1], signed_tx_bytes[:-1]] + ) + tx_invalid_corrupted = st.sampled_from( + [ + bytes([unsigned_tx_bytes[0] ^ 0x01]) + unsigned_tx_bytes[1:], + unsigned_tx_bytes[:1] + bytes([unsigned_tx_bytes[1] ^ 0x01]) + unsigned_tx_bytes[2:], + unsigned_tx_bytes[:20] + b"\x00" + unsigned_tx_bytes[20:], + b"\x00" + unsigned_tx_bytes, + unsigned_tx_bytes + b"\x00", + bytes([signed_tx_bytes[0] ^ 0x01]) + signed_tx_bytes[1:], + signed_tx_bytes[:1] + bytes([signed_tx_bytes[1] ^ 0x01]) + signed_tx_bytes[2:], + signed_tx_bytes[:20] + b"\x00" + signed_tx_bytes[20:], + b"\x00" + signed_tx_bytes, + signed_tx_bytes + b"\x00", + ] + ) + + return { + "tx_valid_bytes": tx_valid_bytes, + "tx_invalid_empty": tx_invalid_empty, + "tx_invalid_random": tx_invalid_random, + "tx_invalid_truncated": tx_invalid_truncated, + "tx_invalid_corrupted": tx_invalid_corrupted, + } + + +def _build_contract_value_strategies() -> dict[str, SearchStrategy[Any]]: + """Build valid and invalid contract parameter cases without changing ABI edge coverage.""" + + valid_contract_value = st.one_of( + st.booleans().map(lambda value: ContractValueCase("add_bool", value)), + st.integers(min_value=-(2**31), max_value=2**31 - 1).map( + lambda value: ContractValueCase("add_int32", value) + ), + st.integers(min_value=0, max_value=2**32 - 1).map( + lambda value: ContractValueCase("add_uint32", value) + ), + st.integers(min_value=MIN_I64, max_value=MAX_I64).map( + lambda value: ContractValueCase("add_int64", value) + ), + st.integers(min_value=0, max_value=2**64 - 1).map( + lambda value: ContractValueCase("add_uint64", value) + ), + st.integers(min_value=-(2**255), max_value=2**255 - 1).map( + lambda value: ContractValueCase("add_int256", value) + ), + st.integers(min_value=0, max_value=2**256 - 1).map( + lambda value: ContractValueCase("add_uint256", value) + ), + st.text(min_size=0, max_size=32).map(lambda value: ContractValueCase("add_string", value)), + st.binary(min_size=0, max_size=64).map(lambda value: ContractValueCase("add_bytes", value)), + st.binary(min_size=0, max_size=32).map( + lambda value: ContractValueCase("add_bytes32", value) + ), + st.binary(min_size=20, max_size=20).map( + lambda value: ContractValueCase("add_address", value) + ), + with_optional_0x(sized_hex(20)).map(lambda value: ContractValueCase("add_address", value)), + st.lists(st.integers(min_value=-(2**31), max_value=2**31 - 1), min_size=0, max_size=6).map( + lambda value: ContractValueCase("add_int32_array", value) + ), + st.lists(st.integers(min_value=0, max_value=2**32 - 1), min_size=0, max_size=6).map( + lambda value: ContractValueCase("add_uint32_array", value) + ), + st.lists(st.binary(min_size=0, max_size=24), min_size=0, max_size=6).map( + lambda value: ContractValueCase("add_bytes_array", value) + ), + st.lists(st.binary(min_size=0, max_size=32), min_size=0, max_size=6).map( + lambda value: ContractValueCase("add_bytes32_array", value) + ), + st.lists(st.text(min_size=0, max_size=16), min_size=0, max_size=6).map( + lambda value: ContractValueCase("add_string_array", value) + ), + st.lists(st.binary(min_size=20, max_size=20), min_size=0, max_size=6).map( + lambda value: ContractValueCase("add_address_array", value) + ), + ) + invalid_contract_value = st.one_of( + st.just(InvalidContractValueCase("add_bool", "true", EncodingTypeError)), + st.just(InvalidContractValueCase("add_int32", 2**31, ValueOutOfBounds)), + st.just(InvalidContractValueCase("add_uint32", -1, ValueOutOfBounds)), + st.just(InvalidContractValueCase("add_bytes32", b"a" * 33, ValueOutOfBounds)), + st.just(InvalidContractValueCase("add_address", b"a" * 19, EncodingTypeError)), + st.just(InvalidContractValueCase("add_address", "0x1234", EncodingTypeError)), + st.just(InvalidContractValueCase("add_bytes_array", [b"a", "b"], EncodingTypeError)), + st.just(InvalidContractValueCase("add_string_array", ["ok", b"bad"], EncodingTypeError)), + st.just(InvalidContractValueCase("add_address_array", [b"a" * 19], EncodingTypeError)), + ) + contract_function_name = st.one_of( + st.none(), + st.text(alphabet=string.ascii_letters + string.digits + "_", min_size=1, max_size=24), + ) + + return { + "contract_value_valid": valid_contract_value, + "contract_value_invalid": invalid_contract_value, + "contract_function_name": contract_function_name, + } + + +def build_strategy_registry() -> dict[str, SearchStrategy[Any]]: + """Assemble all domain-specific strategy groups into the shared fuzz registry.""" + + primitives = _build_identifier_primitives() + key_samples = _build_key_sample_values() + + registry: dict[str, SearchStrategy[Any]] = {} + registry.update(_build_entity_id_strategies(primitives)) + registry.update(_build_alias_identifier_strategies(primitives, key_samples)) + registry.update(_build_invalid_identifier_strategies()) + registry.update(_build_key_strategies(key_samples)) + registry.update(_build_hbar_strategies()) + registry.update(_build_transaction_byte_strategies()) + registry.update(_build_contract_value_strategies()) + return registry + + +FUZZ_STRATEGIES = build_strategy_registry() + + +def get_strategy(name: str) -> SearchStrategy[Any]: + """Return the named fuzz strategy from the shared registry or raise a helpful KeyError.""" + + try: + return FUZZ_STRATEGIES[name] + except KeyError as exc: + available = ", ".join(sorted(FUZZ_STRATEGIES)) + raise KeyError(f"Unknown strategy {name!r}. Available strategies: {available}") from exc + + +@pytest.fixture(name="fuzz_strategies") +def fuzz_strategies_fixture() -> dict[str, SearchStrategy[Any]]: + """Provide the shared fuzz strategy registry.""" + + return FUZZ_STRATEGIES + + +@pytest.fixture(name="get_strategy") +def get_strategy_fixture() -> Any: + """Provide the named strategy accessor.""" + + return get_strategy diff --git a/tests/fuzz/test_contract_function_parameters_fuzz.py b/tests/fuzz/test_contract_function_parameters_fuzz.py new file mode 100644 index 000000000..22121bf5b --- /dev/null +++ b/tests/fuzz/test_contract_function_parameters_fuzz.py @@ -0,0 +1,61 @@ +import pytest +from eth_abi.exceptions import EncodingTypeError, ValueOutOfBounds +from hypothesis import given + +from hiero_sdk_python import ContractFunctionParameters + +from tests.fuzz.conftest import ContractValueCase, InvalidContractValueCase, get_strategy + +pytestmark = pytest.mark.fuzz + + +def _encode(function_name: str | None, case: ContractValueCase) -> bytes: + params = ContractFunctionParameters(function_name) + getattr(params, case.method_name)(case.value) + encoded = params.to_bytes() + + assert isinstance(encoded, bytes) + assert bytes(params) == encoded + assert params.to_bytes() == encoded + return encoded + + +@given( + function_name=get_strategy("contract_function_name"), + case=get_strategy("contract_value_valid"), +) +def test_contract_function_parameters_encode_valid_values( + function_name: str | None, + case: ContractValueCase, +) -> None: + """Valid add_* inputs must encode deterministically.""" + encoded = _encode(function_name, case) + unnamed_encoded = _encode(None, case) + + if function_name is None: + assert encoded == unnamed_encoded + else: + assert len(encoded) >= 4 + assert encoded[4:] == unnamed_encoded + + +@given( + function_name=get_strategy("contract_function_name"), + case=get_strategy("contract_value_invalid"), +) +def test_contract_function_parameters_reject_invalid_values( + function_name: str | None, + case: InvalidContractValueCase, +) -> None: + """Wrong-shaped or out-of-range values must raise the expected encoder exception.""" + params = ContractFunctionParameters(function_name) + + with pytest.raises(case.expected_exception): + getattr(params, case.method_name)(case.value) + params.to_bytes() + + +def test_contract_function_parameters_invalid_case_exceptions_are_specific() -> None: + """Guardrail: invalid-case expectations should stay narrow and intentional.""" + assert ValueOutOfBounds is not ValueError + assert EncodingTypeError is not TypeError diff --git a/tests/fuzz/test_entity_id_fuzz.py b/tests/fuzz/test_entity_id_fuzz.py new file mode 100644 index 000000000..df64eeb16 --- /dev/null +++ b/tests/fuzz/test_entity_id_fuzz.py @@ -0,0 +1,184 @@ +import warnings + +import pytest +from hypothesis import given + +from hiero_sdk_python import AccountId, TokenId +from hiero_sdk_python.contract.contract_id import ContractId + +from tests.fuzz.conftest import AccountIdAliasCase, EntityIdCase, get_strategy + +pytestmark = pytest.mark.fuzz + + +def _assert_account_roundtrip(case: EntityIdCase) -> None: + parsed = AccountId.from_string(case.text) + assert parsed.shard == case.shard + assert parsed.realm == case.realm + assert parsed.num == case.value + assert parsed.checksum == case.checksum + assert str(parsed) == f"{case.shard}.{case.realm}.{case.value}" + canonical = AccountId.from_string(str(parsed)) + assert canonical.shard == parsed.shard + assert canonical.realm == parsed.realm + assert canonical.num == parsed.num + assert canonical.alias_key == parsed.alias_key + assert canonical.evm_address == parsed.evm_address + assert canonical.checksum is None + + +def _assert_token_roundtrip(case: EntityIdCase) -> None: + parsed = TokenId.from_string(case.text) + assert parsed.shard == case.shard + assert parsed.realm == case.realm + assert parsed.num == case.value + assert parsed.checksum == case.checksum + assert str(parsed) == f"{case.shard}.{case.realm}.{case.value}" + canonical = TokenId.from_string(str(parsed)) + assert canonical.shard == parsed.shard + assert canonical.realm == parsed.realm + assert canonical.num == parsed.num + assert canonical.checksum is None + + +def _assert_contract_roundtrip(case: EntityIdCase) -> None: + parsed = ContractId.from_string(case.text) + assert parsed.shard == case.shard + assert parsed.realm == case.realm + assert parsed.contract == case.value + assert parsed.checksum == case.checksum + assert str(parsed) == f"{case.shard}.{case.realm}.{case.value}" + canonical = ContractId.from_string(str(parsed)) + assert canonical.shard == parsed.shard + assert canonical.realm == parsed.realm + assert canonical.contract == parsed.contract + assert canonical.checksum is None + + +@given(case=get_strategy("entity_id_valid_dotted")) +def test_account_id_accepts_valid_dotted_ids(case: EntityIdCase) -> None: + """Valid dotted account IDs must parse and round-trip canonically.""" + _assert_account_roundtrip(case) + + +@given(case=get_strategy("entity_id_valid_checksum")) +def test_account_id_preserves_checksum_text(case: EntityIdCase) -> None: + """Checksum-bearing account IDs must retain checksum metadata after parsing.""" + _assert_account_roundtrip(case) + + +@given(case=get_strategy("account_id_valid_alias")) +def test_account_id_accepts_valid_alias_hex(case: AccountIdAliasCase) -> None: + """Alias-style account IDs must decode into alias_key and reserialize canonically.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + parsed = AccountId.from_string(case.text) + + assert parsed.shard == case.shard + assert parsed.realm == case.realm + assert parsed.num == 0 + assert parsed.alias_key is not None + assert parsed.evm_address is None + assert parsed.alias_key.to_bytes_raw().hex() == case.alias_hex + assert str(parsed) == case.text + + +@given(case=get_strategy("account_id_valid_evm")) +def test_account_id_accepts_valid_evm_forms(case: AccountIdAliasCase) -> None: + """Raw, prefixed, and scoped EVM-address inputs must parse without silent coercion.""" + parsed = AccountId.from_string(case.text) + + assert parsed.shard == case.shard + assert parsed.realm == case.realm + assert parsed.num == 0 + assert parsed.alias_key is None + assert parsed.evm_address is not None + assert parsed.evm_address.address_bytes.hex() == case.evm_hex + assert str(parsed) == f"{case.shard}.{case.realm}.{case.evm_hex}" + assert AccountId.from_string(str(parsed)) == parsed + + +@given(case=get_strategy("entity_id_valid_dotted")) +def test_token_id_accepts_valid_dotted_ids(case: EntityIdCase) -> None: + """Valid dotted token IDs must parse and round-trip canonically.""" + _assert_token_roundtrip(case) + + +@given(case=get_strategy("entity_id_valid_checksum")) +def test_token_id_preserves_checksum_text(case: EntityIdCase) -> None: + """Checksum-bearing token IDs must retain checksum metadata after parsing.""" + _assert_token_roundtrip(case) + + +@given(case=get_strategy("entity_id_valid_dotted")) +def test_contract_id_accepts_valid_dotted_ids(case: EntityIdCase) -> None: + """Valid dotted contract IDs must parse and round-trip canonically.""" + _assert_contract_roundtrip(case) + + +@given(case=get_strategy("entity_id_valid_checksum")) +def test_contract_id_preserves_checksum_text(case: EntityIdCase) -> None: + """Checksum-bearing contract IDs must retain checksum metadata after parsing.""" + _assert_contract_roundtrip(case) + + +@given(case=get_strategy("contract_id_valid_evm")) +def test_contract_id_accepts_scoped_evm_forms(case: AccountIdAliasCase) -> None: + """Scoped 20-byte hex EVM addresses are a valid public ContractId format.""" + parsed = ContractId.from_string(case.text) + + assert parsed.shard == case.shard + assert parsed.realm == case.realm + assert parsed.contract == 0 + assert parsed.evm_address is not None + assert parsed.evm_address.hex() == case.evm_hex + assert str(parsed) == case.text + assert ContractId.from_string(str(parsed)) == parsed + + +@given(value=get_strategy("entity_id_invalid_type")) +def test_account_id_rejects_non_string_inputs(value: object) -> None: + """AccountId.from_string() documents a str-only API.""" + with pytest.raises(TypeError): + AccountId.from_string(value) # type: ignore[arg-type] + + +@given(value=get_strategy("entity_id_invalid_type")) +def test_token_id_rejects_non_string_inputs(value: object) -> None: + """TokenId.from_string() must reject non-string inputs rather than coercing them.""" + if value is None: + with pytest.raises(ValueError, match="cannot be None"): + TokenId.from_string(value) # type: ignore[arg-type] + else: + with pytest.raises(ValueError): + TokenId.from_string(value) # type: ignore[arg-type] + + +@given(value=get_strategy("entity_id_invalid_type")) +def test_contract_id_rejects_non_string_inputs(value: object) -> None: + """ContractId.from_string() documents a str-only API.""" + with pytest.raises(TypeError): + ContractId.from_string(value) # type: ignore[arg-type] + + +@given(text=get_strategy("account_id_invalid_string")) +def test_account_id_rejects_invalid_strings(text: str) -> None: + """Malformed account ID text must raise ValueError instead of partially parsing.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + with pytest.raises(ValueError): + AccountId.from_string(text) + + +@given(text=get_strategy("token_id_invalid_string")) +def test_token_id_rejects_invalid_strings(text: str) -> None: + """Malformed token ID text must raise ValueError.""" + with pytest.raises(ValueError, match="Invalid token ID string"): + TokenId.from_string(text) + + +@given(text=get_strategy("contract_id_invalid_string")) +def test_contract_id_rejects_invalid_strings(text: str) -> None: + """Malformed contract ID text must raise ValueError.""" + with pytest.raises(ValueError): + ContractId.from_string(text) diff --git a/tests/fuzz/test_hbar_fuzz.py b/tests/fuzz/test_hbar_fuzz.py new file mode 100644 index 000000000..d84ae26f8 --- /dev/null +++ b/tests/fuzz/test_hbar_fuzz.py @@ -0,0 +1,60 @@ +import math + +import pytest +from hypothesis import given + +from hiero_sdk_python import Hbar, HbarUnit + +from tests.fuzz.conftest import HbarConstructorCase, HbarStringCase, get_strategy + +pytestmark = pytest.mark.fuzz + + +def _assert_hbar_invariants(value: Hbar, expected_tinybars: int) -> None: + assert value.to_tinybars() == expected_tinybars + assert math.isfinite(value.to_hbars()) + roundtripped = Hbar.from_tinybars(value.to_tinybars()) + assert roundtripped == value + assert roundtripped.to_tinybars() == expected_tinybars + + +@given(case=get_strategy("hbar_valid_string")) +def test_hbar_from_string_accepts_valid_inputs(case: HbarStringCase) -> None: + """Valid public Hbar strings must parse to the exact tinybar value.""" + parsed = Hbar.from_string(case.text) + _assert_hbar_invariants(parsed, case.tinybars) + + +@given(text=get_strategy("hbar_invalid_string")) +def test_hbar_from_string_rejects_invalid_inputs(text: str) -> None: + """Invalid Hbar strings must raise ValueError instead of partially parsing.""" + with pytest.raises(ValueError): + Hbar.from_string(text) + + +@given(case=get_strategy("hbar_valid_constructor")) +def test_hbar_constructor_preserves_exact_value(case: HbarConstructorCase) -> None: + """Valid constructor inputs must convert to the exact expected tinybar count.""" + parsed = Hbar(case.amount, unit=case.unit) + _assert_hbar_invariants(parsed, case.tinybars) + + +@given(amount=get_strategy("hbar_invalid_nonfinite_float")) +def test_hbar_constructor_rejects_nonfinite_floats(amount: float) -> None: + """The constructor must reject NaN and infinities explicitly.""" + with pytest.raises(ValueError): + Hbar(amount) + + +@given(amount=get_strategy("fractional_tinybar_amount")) +def test_hbar_rejects_fractional_tinybars(amount: object) -> None: + """Tinybar inputs are integral only; fractional values are invalid.""" + with pytest.raises(ValueError): + Hbar(amount, unit=HbarUnit.TINYBAR) + + +@given(value=get_strategy("hbar_invalid_constructor_type")) +def test_hbar_constructor_rejects_invalid_types(value: object) -> None: + """The constructor must not silently coerce unsupported input types.""" + with pytest.raises(TypeError): + Hbar(value) \ No newline at end of file diff --git a/tests/fuzz/test_keys_fuzz.py b/tests/fuzz/test_keys_fuzz.py new file mode 100644 index 000000000..51b5ddf83 --- /dev/null +++ b/tests/fuzz/test_keys_fuzz.py @@ -0,0 +1,112 @@ +import warnings + +import pytest +from hypothesis import given + +from hiero_sdk_python import PrivateKey, PublicKey + +from tests.fuzz.conftest import get_strategy + +pytestmark = pytest.mark.fuzz + + +def _load_private_key_from_string(text: str) -> PrivateKey: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + return PrivateKey.from_string(text) + + +def _load_private_key_from_bytes(data: bytes) -> PrivateKey: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + return PrivateKey.from_bytes(data) + + +def _load_public_key_from_string(text: str) -> PublicKey: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + return PublicKey.from_string(text) + + +def _load_public_key_from_bytes(data: bytes) -> PublicKey: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + return PublicKey.from_bytes(data) + + +@given(text=get_strategy("private_key_valid_string")) +def test_private_key_from_string_roundtrips_valid_inputs(text: str) -> None: + """Valid private-key strings must produce a stable key object and raw bytes.""" + parsed = _load_private_key_from_string(text) + reparsed = _load_private_key_from_string(parsed.to_string_raw()) + + assert parsed.to_bytes_raw() == reparsed.to_bytes_raw() + assert parsed.is_ed25519() == reparsed.is_ed25519() + assert parsed.to_string_raw() == parsed.to_bytes_raw().hex() + + +@given(data=get_strategy("private_key_valid_bytes")) +def test_private_key_from_bytes_roundtrips_valid_inputs(data: bytes) -> None: + """Valid private-key byte encodings must round-trip through raw or DER serialization.""" + parsed = _load_private_key_from_bytes(data) + reparsed = _load_private_key_from_bytes(parsed.to_bytes_der()) + + assert parsed.to_bytes_raw() == reparsed.to_bytes_raw() + assert parsed.is_ed25519() == reparsed.is_ed25519() + + +@given(text=get_strategy("private_key_invalid_string")) +def test_private_key_from_string_rejects_invalid_inputs(text: str) -> None: + """Malformed private-key strings must raise ValueError.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + with pytest.raises(ValueError): + PrivateKey.from_string(text) + + +@given(data=get_strategy("private_key_invalid_bytes")) +def test_private_key_from_bytes_rejects_invalid_inputs(data: bytes) -> None: + """Malformed private-key bytes must raise ValueError.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + with pytest.raises(ValueError): + PrivateKey.from_bytes(data) + + +@given(text=get_strategy("public_key_valid_string")) +def test_public_key_from_string_roundtrips_valid_inputs(text: str) -> None: + """Valid public-key strings must preserve canonical raw bytes.""" + parsed = _load_public_key_from_string(text) + reparsed = _load_public_key_from_string(parsed.to_string_raw()) + + assert parsed.to_bytes_raw() == reparsed.to_bytes_raw() + assert parsed.is_ed25519() == reparsed.is_ed25519() + assert parsed.to_string_raw() == parsed.to_bytes_raw().hex() + + +@given(data=get_strategy("public_key_valid_bytes")) +def test_public_key_from_bytes_roundtrips_valid_inputs(data: bytes) -> None: + """Valid public-key byte encodings must round-trip through DER serialization.""" + parsed = _load_public_key_from_bytes(data) + reparsed = _load_public_key_from_bytes(parsed.to_bytes_der()) + + assert parsed.to_bytes_raw() == reparsed.to_bytes_raw() + assert parsed.is_ed25519() == reparsed.is_ed25519() + + +@given(text=get_strategy("public_key_invalid_string")) +def test_public_key_from_string_rejects_invalid_inputs(text: str) -> None: + """Malformed public-key strings must raise ValueError.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + with pytest.raises(ValueError): + PublicKey.from_string(text) + + +@given(data=get_strategy("public_key_invalid_bytes")) +def test_public_key_from_bytes_rejects_invalid_inputs(data: bytes) -> None: + """Malformed public-key bytes must raise ValueError.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + with pytest.raises(ValueError): + PublicKey.from_bytes(data) diff --git a/tests/fuzz/test_transaction_from_bytes_fuzz.py b/tests/fuzz/test_transaction_from_bytes_fuzz.py new file mode 100644 index 000000000..f793dfffd --- /dev/null +++ b/tests/fuzz/test_transaction_from_bytes_fuzz.py @@ -0,0 +1,52 @@ +import pytest +from hypothesis import given, settings + +from hiero_sdk_python import Transaction + +from tests.fuzz.conftest import get_strategy + +pytestmark = pytest.mark.fuzz + + +@settings(max_examples=80) +@given(data=get_strategy("tx_valid_bytes")) +def test_transaction_from_bytes_roundtrips_valid_payloads(data: bytes) -> None: + """Serialized transactions must deserialize and reserialize byte-for-byte.""" + parsed = Transaction.from_bytes(data) + reparsed = Transaction.from_bytes(parsed.to_bytes()) + + assert parsed.to_bytes() == data + assert type(reparsed) is type(parsed) + assert reparsed.to_bytes() == parsed.to_bytes() + + +@settings(max_examples=30) +@given(data=get_strategy("tx_invalid_empty")) +def test_transaction_from_bytes_rejects_empty_payloads(data: bytes) -> None: + """Empty payloads are explicitly invalid for Transaction.from_bytes().""" + with pytest.raises(ValueError): + Transaction.from_bytes(data) + + +@settings(max_examples=80) +@given(data=get_strategy("tx_invalid_truncated")) +def test_transaction_from_bytes_rejects_truncated_payloads(data: bytes) -> None: + """Truncated transaction payloads must raise ValueError.""" + with pytest.raises(ValueError): + Transaction.from_bytes(data) + + +@settings(max_examples=80) +@given(data=get_strategy("tx_invalid_corrupted")) +def test_transaction_from_bytes_rejects_corrupted_payloads(data: bytes) -> None: + """Byte-level corruption of a valid payload must not deserialize successfully.""" + with pytest.raises(ValueError): + Transaction.from_bytes(data) + + +@settings(max_examples=80) +@given(data=get_strategy("tx_invalid_random")) +def test_transaction_from_bytes_rejects_random_payloads(data: bytes) -> None: + """Random byte payloads must not parse as valid SDK transactions.""" + with pytest.raises(ValueError): + Transaction.from_bytes(data) diff --git a/tests/unit/hbar_test.py b/tests/unit/hbar_test.py index 67553893d..99137d3f4 100644 --- a/tests/unit/hbar_test.py +++ b/tests/unit/hbar_test.py @@ -39,6 +39,13 @@ def test_constructor_non_finite_amount_value(invalid_amount): Hbar(invalid_amount) +@pytest.mark.parametrize("invalid_amount", [Decimal("Infinity"), Decimal("NaN")]) +def test_constructor_non_finite_decimal_amount_value(invalid_amount): + """Test creation raises errors for non-finite Decimal amounts.""" + with pytest.raises(ValueError, match="Hbar amount must be finite"): + Hbar(invalid_amount) + + def test_constructor_with_tinybar_unit(): """Test creation with unit set to HbarUnit.TINYBAR.""" hbar1 = Hbar(50, unit=HbarUnit.TINYBAR) @@ -83,6 +90,12 @@ def test_constructor_fractional_tinybar(): Hbar(0.1, unit=HbarUnit.TINYBAR) +def test_constructor_fractional_non_tinybar_amount(): + """Test creation rejects values that do not map to an integral tinybar amount.""" + with pytest.raises(ValueError, match="Fractional tinybar value not allowed"): + Hbar(Decimal("0.000000001"), unit=HbarUnit.HBAR) + + def test_constructor_invalid_type(): """Test creation of Hbar with invalid type.""" with pytest.raises( @@ -102,6 +115,22 @@ def test_from_string(): assert Hbar.from_string("-3").to_tinybars() == -300_000_000 +def test_to_tinybars_preserves_large_integer_values(): + """to_tinybars() should not lose precision for integers above float-safe range.""" + tinybars = 9_007_199_254_740_993 # 2^53 + 1 + hbar = Hbar.from_tinybars(tinybars) + + assert hbar.to_tinybars() == tinybars + + +def test_from_string_preserves_large_integer_hbar_values(): + """Parsing large whole hbar values should preserve the exact tinybar amount.""" + hbar_amount = "90071992.54740993" + expected_tinybars = 9_007_199_254_740_993 + + assert Hbar.from_string(hbar_amount).to_tinybars() == expected_tinybars + + @pytest.mark.parametrize( "invalid_str", [ From 72940e86141f77d2a1c17f1bf4ac37c641676368 Mon Sep 17 00:00:00 2001 From: exploreriii <133720349+exploreriii@users.noreply.github.com> Date: Fri, 27 Mar 2026 04:48:46 +0000 Subject: [PATCH 16/60] chore: release v0.2.3 (#2028) Signed-off-by: exploreriii <133720349+exploreriii@users.noreply.github.com> --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3285d7ba..ddcba1a8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ## [Unreleased] +### Src +- + +### Tests + + +### Examples + + +### Docs + + +### .github + + +## [0.2.3] - 2026-03-26 + ### Added - Add `__repr__` method to `TokenId` class for cleaner debugging output (#1653) From 80e6f46dc0ef18399b6499cddfeb411d56ccdf78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 11:28:46 +0000 Subject: [PATCH 17/60] chore(deps): bump codecov/codecov-action from 5.5.3 to 6.0.0 (#2030) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pr-check-codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-check-codecov.yml b/.github/workflows/pr-check-codecov.yml index cf2c67ff2..5be0941f5 100644 --- a/.github/workflows/pr-check-codecov.yml +++ b/.github/workflows/pr-check-codecov.yml @@ -47,7 +47,7 @@ jobs: - name: Upload coverage to Codecov if: github.repository_owner == 'hiero-ledger' - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml From bd119370b5be5ec6cef8f95459e2a5b854c7911a Mon Sep 17 00:00:00 2001 From: Manish Dait <90558243+manishdait@users.noreply.github.com> Date: Fri, 27 Mar 2026 20:58:22 +0530 Subject: [PATCH 18/60] fix: Build fails in publish.yml (#2032) Signed-off-by: Manish Dait --- .github/workflows/publish.yml | 4 +++- CHANGELOG.md | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f777ab7d6..bc1448209 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,12 +27,14 @@ jobs: - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.14' - name: Upgrade pip run: pip install --upgrade pip - name: Install build, pdm-backend, and grpcio-tools - run: pip install build pdm-backend "grpcio-tools==1.68.1" + run: pip install build pdm-backend "grpcio-tools>=1.76.0" - name: Generate Protobuf run: python generate_proto.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ddcba1a8e..236aafc1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - fix: prevent CodeRabbit from posting comments on closed issues(#1962) - chore: update spam list #1988 - chore: Update `bot-advanced-check.yml`, `bot-gfi-assign-on-comment.yml`, `bot-intermediate-assignment.yml`, `bot-linked-issue-enforcer.yml`, `unassign-on-comment.yml`, `working-on-comment.yml` workflow runner configuration +- Fix build failing in `publish.yml` From f0028f7efe49f02a8f297f008bd255890d3e3886 Mon Sep 17 00:00:00 2001 From: Akshat8510 Date: Sat, 28 Mar 2026 15:35:16 +0530 Subject: [PATCH 19/60] docs: refactor Advanced Issue Template to V2 (#2029) Signed-off-by: Akshat Kumar --- .github/ISSUE_TEMPLATE/05_advanced_issue.yml | 255 +++++-------------- CHANGELOG.md | 2 +- 2 files changed, 69 insertions(+), 188 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/05_advanced_issue.yml b/.github/ISSUE_TEMPLATE/05_advanced_issue.yml index d423d31ff..afeb135b5 100644 --- a/.github/ISSUE_TEMPLATE/05_advanced_issue.yml +++ b/.github/ISSUE_TEMPLATE/05_advanced_issue.yml @@ -1,226 +1,107 @@ name: Advanced Issue Template -description: Create a high-impact issue for experienced contributors with deep familiarity with the codebase +description: For expert contributors tackling architectural, multi-module, or core-logic changes. title: "[Advanced]: " labels: ["advanced"] assignees: [] body: - - type: markdown + - type: textarea + id: advanced_contributors attributes: + label: 🧠 Advanced Contributors — Prerequisites & Expectations + description: | + PRE-FILLED. DO NOT CLAIM THIS ISSUE UNLESS YOU MEET THESE GUIDELINES. value: | - --- - ## **Thanks for contributing!** 🚀 - - We truly appreciate your interest in tackling an **Advanced issue**. - - This template is designed for work that requires **deep familiarity with the Hiero Python SDK** - and confidence making changes that may span multiple modules or affect core behavior. + > [!CAUTION] + > **Advanced issues are the highest-risk work in this project. We will reject PRs that do not meet these standards.** - The goal is to create issues for contributors who: - - have strong familiarity with the Hiero Python SDK internals - - are comfortable reasoning about trade-offs and design decisions - - can work independently with minimal guidance - --- + ### 🏁 Concrete Prerequisites + - **Proven History:** Successfully completed **≥ 10 non-trivial intermediate issues** in this repo. + - **Consistency:** **≥ 3–4 months** of active, human-led contributions to this SDK. + - **Expertise:** Deep architectural understanding of `_Executable`, `Transaction`, and `Query` base classes. - - type: textarea - id: intro - attributes: - label: 🧠 Advanced Contributors - description: Who is this issue intended for? - value: | - This issue is intended for contributors who are already very familiar with the - [Hiero Python SDK](https://hiero.org) codebase and its architectural patterns. + > [!NOTE] + > **Workflow Exception:** For issues focused on **GitHub Actions / Workflows**, the Python-specific thresholds above may be waived if the contributor demonstrates expert-level proficiency in CI/CD security and automation. - You should feel comfortable: - - navigating multiple modules across `src/` - - understanding and modifying core SDK abstractions - - reasoning about API design and backwards compatibility - - updating or extending tests, examples, and documentation as needed - - making changes that may affect public-facing behavior + ### ⚠️ AI Usage Policy + Using AI to generate code for Advanced issues is **strictly forbidden**. AI may be used only to help explain file relationships. Submitting AI-mined code or unvalidated generated output is grounds for **immediate rejection**. - New developers should start with - **Good First Issues** or **Intermediate Issues** first. + ### ⏱️ Timeline & Workflow + - **Typical time:** ~1 month / ~50 hours. + - 🔴 Completing an advanced issue in 1–3 days is a **red flag** and will likely be rejected. + - **Mandatory:** Post your proposed architectural approach as a comment and wait for explicit maintainer approval **before writing any code.** validations: required: false - - type: markdown - attributes: - value: | - > [!WARNING] - > ### 🧭 What we consider an *Advanced Issue* - > - > This issue typically: - > - > - Requires **deep understanding of existing SDK design and behavior** - > - May touch **core abstractions**, shared utilities, or cross-cutting concerns - > - May involve **non-trivial refactors**, design changes, or behavior extensions - > - Has **medium to high risk** if implemented incorrectly - > - Requires careful consideration of **backwards compatibility** - > - May require updating **tests, examples, and documentation together** - > - > **What this issue is NOT:** - > - A simple bug fix - > - A narrowly scoped refactor - > - A task solvable by following existing patterns alone - > - > 📖 Helpful references: - > - `docs/sdk_developers/training` - > - `docs/sdk_developers/training/setup/project_structure.md` - > - `docs/sdk_developers/rebasing.md` - - type: textarea id: problem attributes: label: 🐞 Problem Description - description: | - Describe the problem in depth. - - You may assume the reader: - - understands the overall SDK architecture - - can navigate and reason about multiple modules - - is comfortable reading and modifying core logic - - Clearly explain: - - what the current behavior is - - why it is insufficient or incorrect - - which components or layers are involved - - any relevant historical or design context - value: | - Describe the problem here. - validations: - required: true - - - type: markdown - attributes: - value: | - - ## 🐞 Problem – Example - - The current transaction execution pipeline tightly couples - receipt retrieval, record retrieval, and retry logic into a single - execution flow. - - This coupling makes it difficult to: - - customize retry behavior - - extend execution semantics for scheduled or mirror-node-backed workflows - - test individual stages of transaction execution in isolation - - Several downstream SDK features would benefit from a clearer separation - of concerns in this area. - - Relevant areas: - - `src/hiero_sdk_python/transaction/` - - `src/hiero_sdk_python/execution/` - - `src/hiero_sdk_python/client/` - - - type: textarea - id: solution - attributes: - label: 💡 Proposed / Expected Solution - description: | - Describe the intended direction or design. - - This should include: - - the high-level approach - - any new abstractions or changes to existing ones - - constraints (e.g. backwards compatibility, performance, API stability) - - known alternatives and why they were rejected (if applicable) - - A full design document is not required, but reasoning and intent should be clear. - value: | - Describe the proposed solution here. + placeholder: "Describe the current limitation or architectural risk here..." validations: required: true - - type: markdown - attributes: - value: | - - ## 💡 Proposed Solution – Example - - Introduce a dedicated execution pipeline abstraction that separates: - - transaction submission - - receipt polling - - record retrieval - - retry and timeout logic - - The new design should: - - preserve existing public APIs - - allow advanced users to override or extend execution behavior - - make individual stages independently testable - - Existing transaction execution should be reimplemented - using the new pipeline internally. - - type: textarea id: implementation attributes: - label: 🧠 Implementation & Design Notes - description: | - Provide detailed technical guidance. - - This section is especially important for Advanced issues. - - Consider including: - - specific modules or classes involved - - suggested refactoring strategy - - migration or deprecation concerns - - testing strategy - - performance or security considerations + label: 🛠️ Implementation Notes value: | - Add detailed implementation notes here. + ### Technical domains involved in this issue: + - [ ] **API Client Architecture** (request → serialization → execution → response mapping) + - [ ] **Backward Compatibility** (preserving method signatures, defaults, and return types) + - [ ] **Protobuf Alignment** (reading `.proto` files, `_to_proto()` / `_from_proto()` correctness) + - [ ] **State & Immutability** (correct usage of guards like `_require_not_frozen`) + - [ ] **Execution Boundaries** (retry logic, backoff, node selection, gRPC deadlines) + + _Replace this with specific implementation notes._ validations: required: false - - type: markdown - attributes: - value: | - - ## 🧠 Implementation Notes – Example - - Suggested approach: - - - Introduce a new `ExecutionPipeline` abstraction under - `src/hiero_sdk_python/execution/` - - Refactor existing transaction execution logic to delegate - to this pipeline - - Ensure existing public APIs remain unchanged - - Add focused unit tests for each pipeline stage - - Update at least one example to demonstrate extensibility - - Care should be taken to avoid breaking timeout semantics - relied upon by existing users. - - type: textarea - id: acceptance-criteria + id: quality_standards attributes: - label: ✅ Acceptance Criteria - description: Define what "done" means for this issue + label: 🛡️ Quality & Review Standards value: | - To merge this issue, the pull request must: + The bar for advanced PRs is **"safe, maintainable, architecturally sound, and production-ready."** - - [ ] Fully address the problem and design goals described above - - [ ] Maintain backwards compatibility unless explicitly approved otherwise - - [ ] Follow existing architectural and coding conventions - - [ ] Include comprehensive tests covering new and existing behavior - - [ ] Update relevant examples and documentation - - [ ] Pass all CI checks - - [ ] Include a valid changelog entry - - [ ] be a DCO and GPG key signed as `git commit -S -s -m "chore: my change"` with a GPG key set up + ### 🚀 Defining "Production Ready" + 1. **Architectural Fit:** The solution must fit naturally into the existing SDK abstractions. Avoid "hacks" or isolated logic that bypasses the core execution model. + 2. **Security & Correctness:** Evaluate all logic for injection risks, state corruption, or thread-safety issues. Every line of code must be manually verified. + 3. **Maintainability:** Code must be clear enough for any other maintainer to debug without your assistance. Prefer standard patterns over "clever" one-liners. + 4. **Backward Compatibility:** Public API signatures must be preserved. If a breaking change is required, it must be explicitly managed through a deprecation cycle. validations: - required: true + required: false - - type: textarea - id: additional-info + - type: checkboxes + id: acceptance attributes: - label: 📚 Additional Context, Links, or Prior Art + label: ✅ PR Quality Checklist description: | - Add any references that may help: - - design docs - - prior discussions - - related issues or PRs - - external references + The contributor must manually verify each of these criteria in their Pull Request. + options: + - label: "I understand the system-wide impact of these changes on affected modules and performance." + required: true + - label: "The system design fits with current Hiero SDK architectural approaches." + required: true + - label: "I have tested my changes extensively against both local and network environments." + required: true + - label: "I have verified naming, types, and field ordering against pinned Protobufs (v0.72.0-rc.2)." + required: true + - label: "Every line of code is personally understood and explainable (no unvalidated AI code)." + required: true + + - type: textarea + id: resources + attributes: + label: 📚 Resources & Support value: | - Optional. + **Project References:** + - [SDK Project Structure](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/setup/project_structure.md) + - [Transaction Lifecycle](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/transaction_lifecycle.md) + - [Hedera Protobufs (v0.72.0-rc.2)](https://github.com/hashgraph/hedera-protobufs/tree/v0.72.0-rc.2/services) + + **🆘 Stuck?** + - [Office Hours](https://zoom-lfx.platform.linuxfoundation.org/meeting/99912667426?password=5b584a0e-1ed7-49d3-b2fc-dc5ddc888338) (Wednesdays, 2pm UTC) + - [Discord #hiero-python-sdk](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md) validations: - required: false + required: false \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 236aafc1e..3016010ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ### .github - +- Refactored the Advanced Issue Template to V2 with stricter prerequisites and a focus on architectural design (#2016). ## [0.2.3] - 2026-03-26 From 5b57ef3f7e9588d2645e7abe023ada5d4bed8428 Mon Sep 17 00:00:00 2001 From: Akshat8510 Date: Sun, 29 Mar 2026 02:24:32 +0530 Subject: [PATCH 20/60] docs: refine Advanced Issue Template UX and prerequisites (#2037) Signed-off-by: Akshat Kumar --- .github/ISSUE_TEMPLATE/05_advanced_issue.yml | 25 +++++++++----------- CHANGELOG.md | 2 +- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/05_advanced_issue.yml b/.github/ISSUE_TEMPLATE/05_advanced_issue.yml index afeb135b5..b91b3805b 100644 --- a/.github/ISSUE_TEMPLATE/05_advanced_issue.yml +++ b/.github/ISSUE_TEMPLATE/05_advanced_issue.yml @@ -10,7 +10,7 @@ body: attributes: label: 🧠 Advanced Contributors — Prerequisites & Expectations description: | - PRE-FILLED. DO NOT CLAIM THIS ISSUE UNLESS YOU MEET THESE GUIDELINES. + REQUIRED: Contributors must meet these thresholds before claiming. value: | > [!CAUTION] > **Advanced issues are the highest-risk work in this project. We will reject PRs that do not meet these standards.** @@ -19,6 +19,7 @@ body: - **Proven History:** Successfully completed **≥ 10 non-trivial intermediate issues** in this repo. - **Consistency:** **≥ 3–4 months** of active, human-led contributions to this SDK. - **Expertise:** Deep architectural understanding of `_Executable`, `Transaction`, and `Query` base classes. + - **Advanced Python:** Demonstrated proficiency with complex patterns (e.g., async concurrency, decorators, or state management) used in the core SDK. > [!NOTE] > **Workflow Exception:** For issues focused on **GitHub Actions / Workflows**, the Python-specific thresholds above may be waived if the contributor demonstrates expert-level proficiency in CI/CD security and automation. @@ -72,23 +73,19 @@ body: validations: required: false - - type: checkboxes + - type: textarea id: acceptance attributes: label: ✅ PR Quality Checklist description: | - The contributor must manually verify each of these criteria in their Pull Request. - options: - - label: "I understand the system-wide impact of these changes on affected modules and performance." - required: true - - label: "The system design fits with current Hiero SDK architectural approaches." - required: true - - label: "I have tested my changes extensively against both local and network environments." - required: true - - label: "I have verified naming, types, and field ordering against pinned Protobufs (v0.72.0-rc.2)." - required: true - - label: "Every line of code is personally understood and explainable (no unvalidated AI code)." - required: true + PRE-FILLED. These are the standards the contributor must meet before opening a PR. + value: | + Before opening your PR, the contributor must confirm: + - [ ] I understand the system-wide impact of these changes on affected modules and performance. + - [ ] The system design fits with current Hiero SDK architectural approaches. + - [ ] I have tested my changes extensively against both local and network environments. + - [ ] I have verified naming, types, and field ordering against pinned Protobufs (v0.72.0-rc.2). + - [ ] Every line of code is personally understood and explainable (no unvalidated AI code). - type: textarea id: resources diff --git a/CHANGELOG.md b/CHANGELOG.md index 3016010ab..fbf5d022a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ### .github - Refactored the Advanced Issue Template to V2 with stricter prerequisites and a focus on architectural design (#2016). - +- Refactored the Advanced Issue Template to ensure PR-level quality checklists do not block maintainers during issue creation (#2036) ## [0.2.3] - 2026-03-26 ### Added From dde2fa3770130e61a06c1640f56664a2dc83ac08 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:00:36 +0530 Subject: [PATCH 21/60] chore(deps): bump astral-sh/setup-uv from 7.5.0 to 8.0.0 (#2045) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deps-check.yml | 2 +- .github/workflows/pr-check-codecov.yml | 2 +- .github/workflows/pr-check-examples.yml | 2 +- .github/workflows/pr-check-test.yml | 4 ++-- .github/workflows/tck-test.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deps-check.yml b/.github/workflows/deps-check.yml index a741f2078..caf8825db 100644 --- a/.github/workflows/deps-check.yml +++ b/.github/workflows/deps-check.yml @@ -37,7 +37,7 @@ jobs: cache: "pip" - name: Install uv - uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - name: Create virtual environment run: uv venv diff --git a/.github/workflows/pr-check-codecov.yml b/.github/workflows/pr-check-codecov.yml index 5be0941f5..c18a721e5 100644 --- a/.github/workflows/pr-check-codecov.yml +++ b/.github/workflows/pr-check-codecov.yml @@ -29,7 +29,7 @@ jobs: python-version: "3.11" - name: Install uv - uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - name: Install dependencies run: | diff --git a/.github/workflows/pr-check-examples.yml b/.github/workflows/pr-check-examples.yml index c14fe8a23..f263303f8 100644 --- a/.github/workflows/pr-check-examples.yml +++ b/.github/workflows/pr-check-examples.yml @@ -24,7 +24,7 @@ jobs: fetch-depth: 0 - name: Install uv - uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - name: Install dependencies run: uv sync --all-extras diff --git a/.github/workflows/pr-check-test.yml b/.github/workflows/pr-check-test.yml index 54e0f2826..f6ddcfa9a 100644 --- a/.github/workflows/pr-check-test.yml +++ b/.github/workflows/pr-check-test.yml @@ -49,7 +49,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install uv - uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true @@ -112,7 +112,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install uv - uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true diff --git a/.github/workflows/tck-test.yml b/.github/workflows/tck-test.yml index fb74e33a6..4e83aa9e4 100644 --- a/.github/workflows/tck-test.yml +++ b/.github/workflows/tck-test.yml @@ -37,7 +37,7 @@ jobs: python-version: "3.14" - name: Install uv - uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true From 8fa79db1a958bc08b2c43af91cb16d20a3273918 Mon Sep 17 00:00:00 2001 From: Roshan Singh <110438835+Roshan-Singh-07@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:42:14 +0530 Subject: [PATCH 22/60] chore: update ubuntu runners to hl-sdk-py-lin-md (#2022) Signed-off-by: Roshan Kumar Signed-off-by: Roshan Singh <110438835+Roshan-Singh-07@users.noreply.github.com> --- .github/workflows/deps-check.yml | 2 +- .github/workflows/pr-check-examples.yml | 2 +- .github/workflows/pr-check-test-files.yml | 3 ++- .github/workflows/pr-check-test.yml | 6 +++--- CHANGELOG.md | 3 ++- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deps-check.yml b/.github/workflows/deps-check.yml index caf8825db..e8cdbf1e6 100644 --- a/.github/workflows/deps-check.yml +++ b/.github/workflows/deps-check.yml @@ -14,7 +14,7 @@ permissions: jobs: min-deps: - runs-on: ubuntu-latest + runs-on: ${{ (github.repository_owner != 'hiero-ledger' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork)) && 'ubuntu-latest' || 'hl-sdk-py-lin-md' }} strategy: fail-fast: false diff --git a/.github/workflows/pr-check-examples.yml b/.github/workflows/pr-check-examples.yml index f263303f8..db8b45032 100644 --- a/.github/workflows/pr-check-examples.yml +++ b/.github/workflows/pr-check-examples.yml @@ -11,7 +11,7 @@ on: jobs: run-examples: - runs-on: ubuntu-latest + runs-on: ${{ (github.repository_owner != 'hiero-ledger' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork)) && 'ubuntu-latest' || 'hl-sdk-py-lin-md' }} steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 diff --git a/.github/workflows/pr-check-test-files.yml b/.github/workflows/pr-check-test-files.yml index feb4d917a..366823c63 100644 --- a/.github/workflows/pr-check-test-files.yml +++ b/.github/workflows/pr-check-test-files.yml @@ -14,7 +14,8 @@ concurrency: jobs: check-test-files: - runs-on: ubuntu-latest + # No fork check needed: push-only workflow; fork pushes have different repository_owner + runs-on: ${{ (github.repository_owner != 'hiero-ledger') && 'ubuntu-latest' || 'hl-sdk-py-lin-md' }} steps: - name: Harden the runner (Audit all outbound calls) diff --git a/.github/workflows/pr-check-test.yml b/.github/workflows/pr-check-test.yml index f6ddcfa9a..ddb7e4d47 100644 --- a/.github/workflows/pr-check-test.yml +++ b/.github/workflows/pr-check-test.yml @@ -27,7 +27,7 @@ permissions: jobs: unit-tests: name: Unit Tests (Python ${{ matrix.python-version }}) - runs-on: ubuntu-latest + runs-on: ${{ (github.repository_owner != 'hiero-ledger' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork)) && 'ubuntu-latest' || 'hl-sdk-py-lin-md' }} timeout-minutes: 10 strategy: fail-fast: false @@ -87,7 +87,7 @@ jobs: integration-tests: name: Integration Tests (Python ${{ matrix.python-version }}) - runs-on: ubuntu-latest + runs-on: ${{ (github.repository_owner != 'hiero-ledger' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork)) && 'ubuntu-latest' || 'hl-sdk-py-lin-md' }} timeout-minutes: 20 needs: - unit-tests @@ -162,7 +162,7 @@ jobs: test-summary: name: Test Results Summary - runs-on: ubuntu-latest + runs-on: ${{ (github.repository_owner != 'hiero-ledger' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork)) && 'ubuntu-latest' || 'hl-sdk-py-lin-md' }} needs: - unit-tests - integration-tests diff --git a/CHANGELOG.md b/CHANGELOG.md index fbf5d022a..4d4bec583 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,10 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ### .github +- chore: update GitHub Actions runners from ubuntu-latest to hl-sdk-py-lin-md (#2021) - Refactored the Advanced Issue Template to V2 with stricter prerequisites and a focus on architectural design (#2016). - Refactored the Advanced Issue Template to ensure PR-level quality checklists do not block maintainers during issue creation (#2036) + ## [0.2.3] - 2026-03-26 ### Added @@ -31,7 +33,6 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Fix the TransactionGetReceiptQuery to raise ReceiptStatusError for the non-retryable and non success receipt status - Refactor `AccountInfo` to use the existing `StakingInfo` wrapper class instead of flattened staking fields. Access is now via `info.staking_info.staked_account_id`, `info.staking_info.staked_node_id`, and `info.staking_info.decline_reward`. The old flat accessors (`info.staked_account_id`, `info.staked_node_id`, `info.decline_staking_reward`) are still available as deprecated properties and will emit a `DeprecationWarning`. (#1366) - Added abstract `Key` supper class to handle various proto Keys. - ### Examples ### Tests From 553aea09de6923ed98e2b502b37b0bab97ce48c2 Mon Sep 17 00:00:00 2001 From: Manish Dait <90558243+manishdait@users.noreply.github.com> Date: Tue, 31 Mar 2026 04:27:07 +0530 Subject: [PATCH 23/60] chore: Refactor `mock_server` setup for network level TLS handling and added thread safety (#2047) Signed-off-by: Manish Dait --- CHANGELOG.md | 1 + tests/unit/mock_server.py | 52 +++++++++++++++++---------------------- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d4bec583..7fe756fde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - ### Tests +- Refactor `mock_server` setup for network level TLS handling and added thread safety ### Examples diff --git a/tests/unit/mock_server.py b/tests/unit/mock_server.py index 805a76711..71c8df412 100644 --- a/tests/unit/mock_server.py +++ b/tests/unit/mock_server.py @@ -1,4 +1,5 @@ import grpc +import threading from concurrent import futures from contextlib import contextmanager from hiero_sdk_python.client.network import Network @@ -6,8 +7,6 @@ from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.client.network import _Node -import socket -from contextlib import closing from hiero_sdk_python.hapi.services import ( crypto_service_pb2_grpc, token_service_pb2_grpc, @@ -32,14 +31,15 @@ def __init__(self, responses): responses (list): List of response objects to return in sequence """ self.responses = responses + self._lock = threading.Lock() self.server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) - self.port = _find_free_port() + + self.port = self.server.add_insecure_port('[::]:0') self.address = f"localhost:{self.port}" self._register_services() # Start the server - self.server.add_insecure_port(self.address) self.server.start() def _register_services(self): @@ -95,6 +95,7 @@ def _create_mock_servicer(self, servicer_class): A mock servicer object """ responses = self.responses + lock = self._lock; class MockServicer(servicer_class): def __getattribute__(self, name): @@ -103,12 +104,11 @@ def __getattribute__(self, name): return super().__getattribute__(name) def method_wrapper(request, context): - nonlocal responses - if not responses: - # If no more responses are available, return None - return None + with lock: + if not responses: + return None - response = responses.pop(0) + response = responses.pop(0) if isinstance(response, RealRpcError): # Abort with custom error @@ -122,21 +122,11 @@ def method_wrapper(request, context): def close(self): """Stop the server.""" - self.server.stop(0) + shutdown_event = self.server.stop(0) + success = shutdown_event.wait(timeout=2.0) - -def _find_free_port(): - """Find a free port on localhost.""" - with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: - s.bind(("", 0)) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - port = s.getsockname()[1] - - # If we get the tls port 50212 port skip it - if port in [50212]: - return port + 1 - - return port + if not success: + pass class RealRpcError(grpc.RpcError): @@ -172,16 +162,20 @@ def mock_hedera_servers(response_sequences): nodes = [] for i, server in enumerate(servers): node = _Node(AccountId(0, 0, 3 + i), server.address, None) - - # force insecure transport and mock cert even if we get the tls-port - node._set_root_certificates(b"mock-tls-cert-for-unit-tests") - node._apply_transport_security(False) - node._set_verify_certificates(False) - nodes.append(node) # Create network and client network = Network(nodes=nodes) + network.set_transport_security(False) + network.set_verify_certificates(False) + client = Client(network) + + # Force non-tls for channel + for node in client.network.nodes: + node._address._is_transport_security = lambda: False + node._set_verify_certificates(False) + node._close() + client = Client(network) client.logger.set_level(LogLevel.DISABLED) # Set the operator From 65253a82f7242d133f13f14fe75de73eb40d8214 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:29:17 +0100 Subject: [PATCH 24/60] chore(deps): bump step-security/harden-runner from 2.15.1 to 2.16.1 (#2051) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/bot-advanced-check.yml | 2 +- .github/workflows/bot-assignment-check.yml | 2 +- .github/workflows/bot-beginner-assign-on-comment.yml | 2 +- .github/workflows/bot-coderabbit-plan-trigger.yml | 2 +- .github/workflows/bot-community-calls.yml | 2 +- .github/workflows/bot-gfi-assign-on-comment.yml | 2 +- .github/workflows/bot-gfi-candidate-notification.yaml | 2 +- .github/workflows/bot-inactivity-unassign.yml | 2 +- .github/workflows/bot-intermediate-assignment.yml | 2 +- .github/workflows/bot-issue-reminder-no-pr.yml | 2 +- .github/workflows/bot-linked-issue-enforcer.yml | 2 +- .github/workflows/bot-office-hours.yml | 2 +- .github/workflows/bot-p0-issues-notify-team.yml | 2 +- .github/workflows/bot-pr-draft-explainer.yaml | 2 +- .github/workflows/bot-pr-inactivity-reminder.yml | 2 +- .github/workflows/bot-workflows.yml | 2 +- .github/workflows/cron-check-broken-links.yml | 2 +- .github/workflows/cron-update-spam-list.yml | 2 +- .github/workflows/deps-check.yml | 2 +- .github/workflows/pr-check-broken-links.yml | 2 +- .github/workflows/pr-check-changelog.yml | 2 +- .github/workflows/pr-check-codecov.yml | 2 +- .github/workflows/pr-check-examples.yml | 2 +- .github/workflows/pr-check-test-files.yml | 2 +- .github/workflows/pr-check-test.yml | 4 ++-- .github/workflows/publish.yml | 2 +- .github/workflows/tck-test.yml | 2 +- .github/workflows/unassign-on-comment.yml | 2 +- .github/workflows/working-on-comment.yml | 2 +- 29 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/bot-advanced-check.yml b/.github/workflows/bot-advanced-check.yml index 3548ef07f..cd41a943b 100644 --- a/.github/workflows/bot-advanced-check.yml +++ b/.github/workflows/bot-advanced-check.yml @@ -28,7 +28,7 @@ jobs: runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/bot-assignment-check.yml b/.github/workflows/bot-assignment-check.yml index e282589d3..13db23e17 100644 --- a/.github/workflows/bot-assignment-check.yml +++ b/.github/workflows/bot-assignment-check.yml @@ -12,7 +12,7 @@ jobs: runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/bot-beginner-assign-on-comment.yml b/.github/workflows/bot-beginner-assign-on-comment.yml index 312e9b637..2ea2ef679 100644 --- a/.github/workflows/bot-beginner-assign-on-comment.yml +++ b/.github/workflows/bot-beginner-assign-on-comment.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Harden runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/bot-coderabbit-plan-trigger.yml b/.github/workflows/bot-coderabbit-plan-trigger.yml index c932dabc3..bf88d0b39 100644 --- a/.github/workflows/bot-coderabbit-plan-trigger.yml +++ b/.github/workflows/bot-coderabbit-plan-trigger.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/bot-community-calls.yml b/.github/workflows/bot-community-calls.yml index 4bedbf191..831f4f1f3 100644 --- a/.github/workflows/bot-community-calls.yml +++ b/.github/workflows/bot-community-calls.yml @@ -27,7 +27,7 @@ jobs: runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 #2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d #2.16.1 with: egress-policy: audit diff --git a/.github/workflows/bot-gfi-assign-on-comment.yml b/.github/workflows/bot-gfi-assign-on-comment.yml index 9e3d248c2..8342676ba 100644 --- a/.github/workflows/bot-gfi-assign-on-comment.yml +++ b/.github/workflows/bot-gfi-assign-on-comment.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Harden runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/bot-gfi-candidate-notification.yaml b/.github/workflows/bot-gfi-candidate-notification.yaml index 125793e11..f393f1a0a 100644 --- a/.github/workflows/bot-gfi-candidate-notification.yaml +++ b/.github/workflows/bot-gfi-candidate-notification.yaml @@ -21,7 +21,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d with: egress-policy: audit diff --git a/.github/workflows/bot-inactivity-unassign.yml b/.github/workflows/bot-inactivity-unassign.yml index b77bb2bdf..88a122332 100644 --- a/.github/workflows/bot-inactivity-unassign.yml +++ b/.github/workflows/bot-inactivity-unassign.yml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Harden the runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d with: egress-policy: audit diff --git a/.github/workflows/bot-intermediate-assignment.yml b/.github/workflows/bot-intermediate-assignment.yml index 93bcf66b3..339c6054a 100644 --- a/.github/workflows/bot-intermediate-assignment.yml +++ b/.github/workflows/bot-intermediate-assignment.yml @@ -26,7 +26,7 @@ jobs: runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/bot-issue-reminder-no-pr.yml b/.github/workflows/bot-issue-reminder-no-pr.yml index dd35eb490..c9e32b45d 100644 --- a/.github/workflows/bot-issue-reminder-no-pr.yml +++ b/.github/workflows/bot-issue-reminder-no-pr.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d with: egress-policy: audit diff --git a/.github/workflows/bot-linked-issue-enforcer.yml b/.github/workflows/bot-linked-issue-enforcer.yml index 69fcf9aa4..fbac3a03e 100644 --- a/.github/workflows/bot-linked-issue-enforcer.yml +++ b/.github/workflows/bot-linked-issue-enforcer.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit - name: Checkout repository diff --git a/.github/workflows/bot-office-hours.yml b/.github/workflows/bot-office-hours.yml index 8334b157c..3c5f621aa 100644 --- a/.github/workflows/bot-office-hours.yml +++ b/.github/workflows/bot-office-hours.yml @@ -26,7 +26,7 @@ jobs: cancel-in-progress: false steps: - name: Harden the runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 #2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d #2.16.1 with: egress-policy: audit diff --git a/.github/workflows/bot-p0-issues-notify-team.yml b/.github/workflows/bot-p0-issues-notify-team.yml index 1ac7b14e2..4a753cc20 100644 --- a/.github/workflows/bot-p0-issues-notify-team.yml +++ b/.github/workflows/bot-p0-issues-notify-team.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/bot-pr-draft-explainer.yaml b/.github/workflows/bot-pr-draft-explainer.yaml index 2a4a62267..57bbc8a46 100644 --- a/.github/workflows/bot-pr-draft-explainer.yaml +++ b/.github/workflows/bot-pr-draft-explainer.yaml @@ -29,7 +29,7 @@ jobs: cancel-in-progress: true steps: - name: Harden the runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/bot-pr-inactivity-reminder.yml b/.github/workflows/bot-pr-inactivity-reminder.yml index ab6098d11..11f288b72 100644 --- a/.github/workflows/bot-pr-inactivity-reminder.yml +++ b/.github/workflows/bot-pr-inactivity-reminder.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit - name: Checkout repository diff --git a/.github/workflows/bot-workflows.yml b/.github/workflows/bot-workflows.yml index 79fcf164b..2de71473a 100644 --- a/.github/workflows/bot-workflows.yml +++ b/.github/workflows/bot-workflows.yml @@ -21,7 +21,7 @@ jobs: runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/cron-check-broken-links.yml b/.github/workflows/cron-check-broken-links.yml index 2ddc61712..95977a5ea 100644 --- a/.github/workflows/cron-check-broken-links.yml +++ b/.github/workflows/cron-check-broken-links.yml @@ -20,7 +20,7 @@ jobs: runs-on: hl-sdk-py-lin-md steps: - name: Harden runner (audit outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/cron-update-spam-list.yml b/.github/workflows/cron-update-spam-list.yml index e5a611665..6e2e89490 100644 --- a/.github/workflows/cron-update-spam-list.yml +++ b/.github/workflows/cron-update-spam-list.yml @@ -23,7 +23,7 @@ jobs: runs-on: hl-sdk-py-lin-md steps: - name: Harden runner (audit outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/deps-check.yml b/.github/workflows/deps-check.yml index e8cdbf1e6..aa90413b5 100644 --- a/.github/workflows/deps-check.yml +++ b/.github/workflows/deps-check.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/pr-check-broken-links.yml b/.github/workflows/pr-check-broken-links.yml index 869da8618..33b6f1241 100644 --- a/.github/workflows/pr-check-broken-links.yml +++ b/.github/workflows/pr-check-broken-links.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Harden runner (audit outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/pr-check-changelog.yml b/.github/workflows/pr-check-changelog.yml index 17294b252..10ea4d79f 100644 --- a/.github/workflows/pr-check-changelog.yml +++ b/.github/workflows/pr-check-changelog.yml @@ -16,7 +16,7 @@ jobs: fetch-depth: 0 - name: Harden the runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/pr-check-codecov.yml b/.github/workflows/pr-check-codecov.yml index c18a721e5..b46e80a36 100644 --- a/.github/workflows/pr-check-codecov.yml +++ b/.github/workflows/pr-check-codecov.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/pr-check-examples.yml b/.github/workflows/pr-check-examples.yml index db8b45032..2ffedf599 100644 --- a/.github/workflows/pr-check-examples.yml +++ b/.github/workflows/pr-check-examples.yml @@ -14,7 +14,7 @@ jobs: runs-on: ${{ (github.repository_owner != 'hiero-ledger' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork)) && 'ubuntu-latest' || 'hl-sdk-py-lin-md' }} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/pr-check-test-files.yml b/.github/workflows/pr-check-test-files.yml index 366823c63..c8de51b8d 100644 --- a/.github/workflows/pr-check-test-files.yml +++ b/.github/workflows/pr-check-test-files.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/pr-check-test.yml b/.github/workflows/pr-check-test.yml index ddb7e4d47..0f4191f04 100644 --- a/.github/workflows/pr-check-test.yml +++ b/.github/workflows/pr-check-test.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit @@ -99,7 +99,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bc1448209..fe92f5834 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,7 @@ jobs: id-token: write steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/tck-test.yml b/.github/workflows/tck-test.yml index 4e83aa9e4..d5717b155 100644 --- a/.github/workflows/tck-test.yml +++ b/.github/workflows/tck-test.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/unassign-on-comment.yml b/.github/workflows/unassign-on-comment.yml index 7084e98b7..f99761e90 100644 --- a/.github/workflows/unassign-on-comment.yml +++ b/.github/workflows/unassign-on-comment.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Harden runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d with: egress-policy: audit diff --git a/.github/workflows/working-on-comment.yml b/.github/workflows/working-on-comment.yml index 261e5e754..85c121145 100644 --- a/.github/workflows/working-on-comment.yml +++ b/.github/workflows/working-on-comment.yml @@ -26,7 +26,7 @@ jobs: cancel-in-progress: false steps: - name: Harden Runner - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit From ef05dd4045a50053cf9b05697ffd84812a96f35d Mon Sep 17 00:00:00 2001 From: Cheese Cake Date: Sat, 4 Apr 2026 14:32:52 +0530 Subject: [PATCH 25/60] feat: sync linked issue labels to pull requests (#2015) Signed-off-by: cheese-cakee Co-authored-by: Roger Barker Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/sync-issue-labels-add.yml | 128 +++++++++++ .../workflows/sync-issue-labels-compute.yml | 207 ++++++++++++++++++ CHANGELOG.md | 3 +- 3 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/sync-issue-labels-add.yml create mode 100644 .github/workflows/sync-issue-labels-compute.yml diff --git a/.github/workflows/sync-issue-labels-add.yml b/.github/workflows/sync-issue-labels-add.yml new file mode 100644 index 000000000..fc345431b --- /dev/null +++ b/.github/workflows/sync-issue-labels-add.yml @@ -0,0 +1,128 @@ +name: Add Linked Issue Labels to PR + +on: + workflow_dispatch: + inputs: + upstream_run_id: + description: "Upstream compute workflow run ID" + required: true + type: string + pr_number: + description: "Pull request number" + required: true + type: string + dry_run: + description: "Dry run flag" + required: false + type: string + default: "true" + is_fork_pr: + description: "Fork PR flag" + required: false + type: string + default: "false" +defaults: + run: + shell: bash +permissions: + actions: read + issues: write + +jobs: + add-labels: + concurrency: + group: sync-issue-labels-pr-${{ github.event.inputs.pr_number }} + cancel-in-progress: true + runs-on: ubuntu-latest + steps: + - name: Harden the runner + uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 + with: + egress-policy: audit + + - name: Download labels artifact + id: download + continue-on-error: true + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + with: + name: pr-labels-${{ github.event.inputs.pr_number }} + path: artifacts + run-id: ${{ github.event.inputs.upstream_run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Read labels payload + id: read + env: + INPUT_PR_NUMBER: ${{ github.event.inputs.pr_number }} + INPUT_IS_FORK_PR: ${{ github.event.inputs.is_fork_pr }} + INPUT_DRY_RUN: ${{ github.event.inputs.dry_run }} + run: | + labels_file="artifacts/labels.json" + if [ ! -f "$labels_file" ]; then + echo "::error::Labels artifact not found. Cross-workflow handoff is broken." + echo "labels=[]" >> "$GITHUB_OUTPUT" + echo "labels_count=0" >> "$GITHUB_OUTPUT" + echo "labels_multiline=" >> "$GITHUB_OUTPUT" + echo "pr_number=$INPUT_PR_NUMBER" >> "$GITHUB_OUTPUT" + echo "is_fork_pr=$INPUT_IS_FORK_PR" >> "$GITHUB_OUTPUT" + echo "dry_run=$INPUT_DRY_RUN" >> "$GITHUB_OUTPUT" + echo "source_event=workflow_dispatch" >> "$GITHUB_OUTPUT" + exit 1 + fi + labels=$(jq -c '.labels // []' "$labels_file") + pr_number=$(jq -r '.pr_number // 0' "$labels_file") + is_fork_pr=$(jq -r '.is_fork_pr // false' "$labels_file") + dry_run=$(jq -r '.dry_run // "true"' "$labels_file") + source_event=$(jq -r '.source_event // ""' "$labels_file") + labels_multiline=$(jq -r '.labels // [] | .[]' "$labels_file") + labels_count=$(echo "$labels" | jq 'length') + echo "labels=$labels" >> "$GITHUB_OUTPUT" + echo "labels_count=$labels_count" >> "$GITHUB_OUTPUT" + { + echo "labels_multiline<> "$GITHUB_OUTPUT" + echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" + echo "is_fork_pr=$is_fork_pr" >> "$GITHUB_OUTPUT" + echo "dry_run=$dry_run" >> "$GITHUB_OUTPUT" + echo "source_event=$source_event" >> "$GITHUB_OUTPUT" + + - name: Validate labels payload + id: validate + run: | + if [ "$PR_NUMBER" = "0" ] || [ "$(echo "$LABELS" | jq -r '. | length')" = "0" ]; then + echo "Invalid payload: pr_number=$PR_NUMBER or labels empty. Skipping label addition." + echo "valid_payload=false" >> "$GITHUB_OUTPUT" + else + echo "valid_payload=true" >> "$GITHUB_OUTPUT" + fi + env: + PR_NUMBER: ${{ steps.read.outputs.pr_number }} + LABELS: ${{ steps.read.outputs.labels }} + + - name: Determine if labels should be applied + id: should_apply + run: | + if [ "${{ steps.read.outputs.is_fork_pr }}" = "true" ]; then + echo "apply=false" >> "$GITHUB_OUTPUT" + echo "reason=fork PR" >> "$GITHUB_OUTPUT" + elif [ "${{ steps.validate.outputs.valid_payload }}" != "true" ]; then + echo "apply=false" >> "$GITHUB_OUTPUT" + echo "reason=invalid payload" >> "$GITHUB_OUTPUT" + elif [ "${{ steps.read.outputs.source_event }}" = "workflow_dispatch" ] && [ "${{ steps.read.outputs.dry_run }}" = "true" ]; then + echo "apply=false" >> "$GITHUB_OUTPUT" + echo "reason=dry run" >> "$GITHUB_OUTPUT" + else + echo "apply=true" >> "$GITHUB_OUTPUT" + echo "reason=" >> "$GITHUB_OUTPUT" + fi + + - name: Add labels to PR + if: ${{ steps.should_apply.outputs.apply == 'true' }} + uses: actions-ecosystem/action-add-labels@1a9c3715c0037e96b97bb38cb4c4b56a1f1d4871 # main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + labels: ${{ steps.read.outputs.labels_multiline }} + number: ${{ steps.read.outputs.pr_number }} + diff --git a/.github/workflows/sync-issue-labels-compute.yml b/.github/workflows/sync-issue-labels-compute.yml new file mode 100644 index 000000000..e3db19955 --- /dev/null +++ b/.github/workflows/sync-issue-labels-compute.yml @@ -0,0 +1,207 @@ +name: Compute Linked Issue Labels + +on: + pull_request: + types: [opened, edited, reopened, synchronize, ready_for_review] + workflow_dispatch: + inputs: + pr_number: + description: "PR number to sync labels for" + required: true + type: number + dry-run-enabled: + description: "Dry run (log only, do not apply labels)" + required: false + type: boolean + default: true + +permissions: + actions: write + pull-requests: read + issues: read + contents: read + +jobs: + compute-labels: + concurrency: + group: sync-issue-labels-compute-pr-${{ github.event.pull_request.number || github.event.inputs.pr_number }} + cancel-in-progress: true + runs-on: ubuntu-latest + outputs: + pr_number: ${{ steps.compute.outputs.pr_number }} + dry_run: ${{ steps.compute.outputs.dry_run }} + is_fork_pr: ${{ steps.compute.outputs.is_fork_pr }} + steps: + - name: Harden the runner + uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 + with: + egress-policy: audit + + - name: Compute linked issue labels + id: compute + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} + DRY_RUN: 'true' + REQUESTED_DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && format('{0}', github.event.inputs['dry-run-enabled']) || 'true' }} + IS_FORK_PR: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork || 'false' }} + MAX_LINKED_ISSUES: '20' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + result-encoding: json + script: | + const MAX_LINKED_ISSUES = Number(process.env.MAX_LINKED_ISSUES || "20"); + + function extractLabels(labelData) { + const result = []; + for (const item of labelData) { + const name = typeof item === "string" ? item : item && item.name; + if (name && name.trim()) result.push(name.trim()); + } + return result; + } + + function extractLinkedIssueNumbers(prBody, owner, repo) { + const numbers = new Set(); + const closingRefRegex = /(?:fix|fixes|fixed|close|closes|closed|resolve|resolves|resolved)\s+(?:([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+))?#(\d+)\b/gi; + const lines = String(prBody || "").split(/\r?\n/); + for (const line of lines) { + let m; + while ((m = closingRefRegex.exec(line)) !== null) { + const refOwner = (m[1] || "").toLowerCase(); + const refRepo = (m[2] || "").toLowerCase(); + if (refOwner && refRepo && (refOwner !== owner.toLowerCase() || refRepo !== repo.toLowerCase())) continue; + numbers.add(Number(m[3])); + } + } + const all = Array.from(numbers); + if (all.length > MAX_LINKED_ISSUES) { + console.log(`[sync] Limiting linked issue refs from ${all.length} to ${MAX_LINKED_ISSUES}.`); + } + return all.slice(0, MAX_LINKED_ISSUES); + } + + const prNumber = Number(process.env.PR_NUMBER); + if (!prNumber) { + core.setOutput('has_labels', 'false'); + core.setOutput('labels', '[]'); + core.setOutput('pr_number', ''); + core.setOutput('dry_run', 'true'); + core.setOutput('is_fork_pr', String(process.env.IS_FORK_PR || 'false')); + core.setOutput('source_event', context.eventName); + return; + } + + const { data: prData } = await github.rest.pulls.get({ + owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber + }); + + const prAuthor = (prData.user && prData.user.login) || ""; + if (/\[bot\]$/i.test(prAuthor) || /dependabot/i.test(prAuthor)) { + console.log(`[sync] Skipping bot-authored PR from ${prAuthor}.`); + core.setOutput('has_labels', 'false'); + core.setOutput('labels', '[]'); + core.setOutput('pr_number', String(prNumber)); + core.setOutput('dry_run', 'true'); + core.setOutput('is_fork_pr', String(process.env.IS_FORK_PR || 'false')); + core.setOutput('source_event', context.eventName); + return; + } + + const linkedIssues = extractLinkedIssueNumbers(prData.body || "", context.repo.owner, context.repo.repo); + if (!linkedIssues.length) { + console.log("[sync] No linked issue references found in PR body."); + core.setOutput('has_labels', 'false'); + core.setOutput('labels', '[]'); + core.setOutput('pr_number', String(prNumber)); + core.setOutput('dry_run', 'true'); + core.setOutput('is_fork_pr', String(process.env.IS_FORK_PR || 'false')); + core.setOutput('source_event', context.eventName); + return; + } + + console.log(`[sync] Linked issues: ${linkedIssues.map(n => '#' + n).join(', ')}`); + + const allLabels = []; + for (const num of linkedIssues) { + try { + const { data } = await github.rest.issues.get({ + owner: context.repo.owner, repo: context.repo.repo, issue_number: num + }); + if (data.pull_request) { console.log(`[sync] Skipping #${num}: is a PR reference.`); continue; } + const labels = extractLabels(data.labels || []); + console.log(`[sync] Issue #${num} labels: ${labels.length ? labels.join(', ') : '(none)'}`); + allLabels.push(...labels); + } catch (err) { + if (err && err.status === 404) { console.log(`[sync] Issue #${num} not found. Skipping.`); continue; } + throw err; + } + } + + const existing = extractLabels(prData.labels || []); + const existingSet = new Set(existing); + const deduped = Array.from(new Set(allLabels)); + const toAdd = deduped.filter(l => !existingSet.has(l)); + + console.log(`[sync] Existing: ${existing.length ? existing.join(', ') : '(none)'}`); + console.log(`[sync] To add: ${toAdd.length ? toAdd.join(', ') : '(none)'}`); + + const labels = toAdd; + const hasLabels = labels.length > 0; + core.setOutput('has_labels', String(hasLabels)); + core.setOutput('labels', JSON.stringify(labels)); + core.setOutput('pr_number', String(prNumber)); + core.setOutput('dry_run', String(process.env.REQUESTED_DRY_RUN || 'true')); + core.setOutput('is_fork_pr', String(process.env.IS_FORK_PR || 'false')); + core.setOutput('source_event', context.eventName); + return { has_labels: hasLabels, labels, pr_number: String(prNumber), dry_run: process.env.REQUESTED_DRY_RUN, is_fork_pr: process.env.IS_FORK_PR, source_event: context.eventName }; + + - name: Write labels artifact payload + env: + LABELS_JSON: ${{ steps.compute.outputs.labels }} + PR_NUMBER: ${{ steps.compute.outputs.pr_number }} + IS_FORK_PR: ${{ steps.compute.outputs.is_fork_pr }} + DRY_RUN: ${{ steps.compute.outputs.dry_run }} + SOURCE_EVENT: ${{ steps.compute.outputs.source_event }} + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const fs = require('fs'); + const parsed = JSON.parse(process.env.LABELS_JSON || '[]'); + const payload = { + pr_number: Number(process.env.PR_NUMBER || 0), + labels: Array.isArray(parsed) ? parsed : [], + is_fork_pr: /^true$/i.test(process.env.IS_FORK_PR || ''), + dry_run: /^true$/i.test(process.env.DRY_RUN || ''), + source_event: process.env.SOURCE_EVENT || '', + }; + fs.writeFileSync('labels.json', JSON.stringify(payload)); + console.log(`Wrote labels artifact payload for PR #${payload.pr_number}: ${payload.labels.length} labels`); + + - name: Upload labels artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: pr-labels-${{ steps.compute.outputs.pr_number }} + path: labels.json + retention-days: 1 + + dispatch-add: + needs: compute-labels + if: ${{ needs.compute-labels.outputs.is_fork_pr != 'true' }} + runs-on: ubuntu-latest + steps: + - name: Trigger add workflow + uses: step-security/workflow-dispatch@acca1a315af3bf7f33dd116d3cb405cb83f5cbdc # v1.2.8 + with: + workflow: .github/workflows/sync-issue-labels-add.yml + repo: ${{ github.repository }} + ref: main + token: ${{ secrets.GH_ACCESS_TOKEN }} + inputs: >- + { + "upstream_run_id":"${{ github.run_id }}", + "pr_number":"${{ needs.compute-labels.outputs.pr_number }}", + "dry_run":"${{ needs.compute-labels.outputs.dry_run }}", + "is_fork_pr":"${{ needs.compute-labels.outputs.is_fork_pr }}" + } + diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fe756fde..615ff30cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Changelog +# Changelog All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org). @@ -23,6 +23,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - chore: update GitHub Actions runners from ubuntu-latest to hl-sdk-py-lin-md (#2021) - Refactored the Advanced Issue Template to V2 with stricter prerequisites and a focus on architectural design (#2016). - Refactored the Advanced Issue Template to ensure PR-level quality checklists do not block maintainers during issue creation (#2036) +- Add automated label sync workflow to propagate labels from linked issues to pull requests (#1716) ## [0.2.3] - 2026-03-26 From 2443c18e8d6a663bbf284b4201487ebba752e65d Mon Sep 17 00:00:00 2001 From: Om Swastik Panda Date: Mon, 6 Apr 2026 17:07:58 +0530 Subject: [PATCH 26/60] docs: add missing docstring to setup_client (#2061) Signed-off-by: Omswastik-11 --- CHANGELOG.md | 1 + examples/tokens/token_dissociate_transaction.py | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 615ff30cf..d76a57e69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ### Examples +- Add the missing `setup_client()` docstring in `examples/tokens/token_dissociate_transaction.py` for consistency with the other example functions. (#2058) ### Docs diff --git a/examples/tokens/token_dissociate_transaction.py b/examples/tokens/token_dissociate_transaction.py index 871c8cffb..a08803d3a 100644 --- a/examples/tokens/token_dissociate_transaction.py +++ b/examples/tokens/token_dissociate_transaction.py @@ -19,6 +19,7 @@ def setup_client(): + """Initialize the Hiero client from environment variables and print connection info.""" client = Client.from_env() print(f"Network: {client.network.network}") print(f"Client set up with operator id {client.operator_account_id}") From a3ec1f05e76e41683a730db0a279ecfd162c2303 Mon Sep 17 00:00:00 2001 From: Raj koli <2024.rajk@isu.ac.in> Date: Mon, 6 Apr 2026 23:35:47 +0530 Subject: [PATCH 27/60] chore: pin pip packages to exact versions in publish.yml (#2079) Signed-off-by: Rajkoli143 <2024.rajk@isu.ac.in> --- .github/workflows/publish.yml | 4 ++-- CHANGELOG.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fe92f5834..4a83a1198 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -31,10 +31,10 @@ jobs: python-version: '3.14' - name: Upgrade pip - run: pip install --upgrade pip + run: pip install "pip==26.0.1" - name: Install build, pdm-backend, and grpcio-tools - run: pip install build pdm-backend "grpcio-tools>=1.76.0" + run: pip install build pdm-backend "grpcio-tools==1.76.0" - name: Generate Protobuf run: python generate_proto.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d76a57e69..89263f7ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ### .github +- chore: pin pip packages to exact versions in publish.yml to improve supply chain security and reproducibility (#2056) - chore: update GitHub Actions runners from ubuntu-latest to hl-sdk-py-lin-md (#2021) - Refactored the Advanced Issue Template to V2 with stricter prerequisites and a focus on architectural design (#2016). - Refactored the Advanced Issue Template to ensure PR-level quality checklists do not block maintainers during issue creation (#2036) From 12aefcf23b3a45368e61aeb5e42ebbe69c5b5bb6 Mon Sep 17 00:00:00 2001 From: Ajay Rajera Date: Tue, 7 Apr 2026 12:18:30 +0530 Subject: [PATCH 28/60] update spam list (#2078) Signed-off-by: Ajay Rajera Co-authored-by: Manish Dait <90558243+manishdait@users.noreply.github.com> --- .github/spam-list.txt | 23 ++++++++++++----------- CHANGELOG.md | 1 + 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/spam-list.txt b/.github/spam-list.txt index ed4c4b48b..ae5f18a3b 100644 --- a/.github/spam-list.txt +++ b/.github/spam-list.txt @@ -1,11 +1,12 @@ -KubanjaElijahEldred -by22Jy -CODEAbhinav-art -zhanglinqian -Halbot100 -roberthallers -SergioChan -ndpvt-web -OnlyTerp -shixian-HFUT -Yuki9814 \ No newline at end of file +KubanjaElijahEldred +by22Jy +CODEAbhinav-art +zhanglinqian +Halbot100 +roberthallers +SergioChan +ndpvt-web +OnlyTerp +shixian-HFUT +Yuki9814 +ashrafzunaira18 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 89263f7ef..82936656c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Refactored the Advanced Issue Template to V2 with stricter prerequisites and a focus on architectural design (#2016). - Refactored the Advanced Issue Template to ensure PR-level quality checklists do not block maintainers during issue creation (#2036) - Add automated label sync workflow to propagate labels from linked issues to pull requests (#1716) +- chore: update spam list (#2035) ## [0.2.3] - 2026-03-26 From 33b87309dc164b5640c3c0540bcc4de8e9ba1b9c Mon Sep 17 00:00:00 2001 From: XYZ Date: Tue, 7 Apr 2026 12:58:51 +0530 Subject: [PATCH 29/60] feat: Expose all missing protobuf fields in TransactionRecord (#2008) Signed-off-by: Siddhartha Ganguly Co-authored-by: Manish Dait <90558243+manishdait@users.noreply.github.com> --- CHANGELOG.md | 6 +- examples/transaction/transaction_record.py | 160 ++++++ src/hiero_sdk_python/__init__.py | 2 + .../tokens/token_association.py | 75 +++ .../transaction/transaction_record.py | 219 ++++++-- .../integration/prng_transaction_e2e_test.py | 11 +- .../transaction_record_query_e2e_test.py | 156 ++++-- tests/unit/token_association_test.py | 242 +++++++++ tests/unit/transaction_record_test.py | 468 +++++++++++++++--- 9 files changed, 1210 insertions(+), 129 deletions(-) create mode 100644 examples/transaction/transaction_record.py create mode 100644 src/hiero_sdk_python/tokens/token_association.py create mode 100644 tests/unit/token_association_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 82936656c..f3170ed5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Changelog +# Changelog All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org). @@ -7,7 +7,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ## [Unreleased] ### Src -- +- Exposed all missing `TransactionRecord` protobuf fields `consensusTimestamp`, `scheduleRef`, `assessed_custom_fees`, `automatic_token_associations`, `parent_consensus_timestamp`, `alias`, `ethereum_hash`, `paid_staking_rewards`, `evm_address`, `contractCreateResult` with proper `None` handling, PRNG oneof handling with unset values return `None` instead of default values 0 / b"" (#1636) ### Tests - Refactor `mock_server` setup for network level TLS handling and added thread safety @@ -38,6 +38,8 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Fix the TransactionGetReceiptQuery to raise ReceiptStatusError for the non-retryable and non success receipt status - Refactor `AccountInfo` to use the existing `StakingInfo` wrapper class instead of flattened staking fields. Access is now via `info.staking_info.staked_account_id`, `info.staking_info.staked_node_id`, and `info.staking_info.decline_reward`. The old flat accessors (`info.staked_account_id`, `info.staked_node_id`, `info.decline_staking_reward`) are still available as deprecated properties and will emit a `DeprecationWarning`. (#1366) - Added abstract `Key` supper class to handle various proto Keys. + + ### Examples ### Tests diff --git a/examples/transaction/transaction_record.py b/examples/transaction/transaction_record.py new file mode 100644 index 000000000..c98f790f5 --- /dev/null +++ b/examples/transaction/transaction_record.py @@ -0,0 +1,160 @@ +""" +Simple example: Exploring TransactionRecord fields. + +This example creates a mock TransactionRecord with sample data and prints all fields +in a readable format. + +No network or client needed — just run the file! + +Run: + python examples/transaction/transaction_record.py +""" + +from collections import defaultdict + +from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.contract.contract_function_result import ContractFunctionResult +from hiero_sdk_python.contract.contract_id import ContractId +from hiero_sdk_python.hapi.services import transaction_receipt_pb2 +from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.schedule.schedule_id import ScheduleId +from hiero_sdk_python.timestamp import Timestamp +from hiero_sdk_python.tokens.assessed_custom_fee import AssessedCustomFee +from hiero_sdk_python.tokens.token_association import TokenAssociation +from hiero_sdk_python.tokens.token_id import TokenId +from hiero_sdk_python.transaction.transaction_id import TransactionId +from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt +from hiero_sdk_python.transaction.transaction_record import TransactionRecord + + +def create_mock_record(): + """Create a mock TransactionRecord with sample values for all fields.""" + # Basic setup + tx_id = TransactionId.from_string("0.0.1234@1698765432.000000000") + + receipt_proto = transaction_receipt_pb2.TransactionReceipt() + receipt_proto.status = ResponseCode.SUCCESS.value + + receipt = TransactionReceipt( + receipt_proto=receipt_proto, + transaction_id=tx_id + ) + + ts = Timestamp(seconds=1698765432, nanos=123456789) + sched = ScheduleId(0, 0, 9999) + + record = TransactionRecord( + transaction_id=tx_id, + transaction_hash=b'\x01\x02\x03\x04' * 12, + transaction_memo="Hello from example!", + transaction_fee=50000, + receipt=receipt, + token_transfers=defaultdict(lambda: defaultdict(int)), + nft_transfers=defaultdict(list), + transfers=defaultdict(int), + new_pending_airdrops=[], + prng_number=42, + prng_bytes=None, # mutually exclusive with prng_number + duplicates=[], + children=[], + consensus_timestamp=ts, + schedule_ref=sched, + assessed_custom_fees=[ + AssessedCustomFee( + amount=1000000, + fee_collector_account_id=AccountId(shard=0, realm=0, num=98), + effective_payer_account_ids=[AccountId(shard=0, realm=0, num=100)] + ) + ], + automatic_token_associations=[ + TokenAssociation( + token_id=TokenId(shard=0, realm=0, num=5678), + account_id=AccountId(shard=0, realm=0, num=1234) + ) + ], + parent_consensus_timestamp=ts, + alias=b'\x12\x34\x56\x78\x9a\xbc', + ethereum_hash=b'\xab' * 32, + paid_staking_rewards=[ + (AccountId(shard=0, realm=0, num=456), 500000), + (AccountId(shard=0, realm=0, num=789), 250000) + ], + evm_address=b'\xef' * 20, + contract_create_result=ContractFunctionResult( + contract_id=ContractId(shard=0, realm=0, contract=1000), + contract_call_result=b"Contract created successfully!" + ), + ) + + record.transfers[AccountId(shard=0, realm=0, num=100)] = -10000 + record.transfers[AccountId(shard=0, realm=0, num=200)] = 10000 + + return record + +def _print_basic_fields(record): + print("Basic:") + print(f" Transaction ID: {record.transaction_id}") + print(f" Memo: {record.transaction_memo}") + print(f" Fee: {record.transaction_fee} tinybars") + print(f" Hash: {record.transaction_hash.hex() if record.transaction_hash else 'None'}") + if record.receipt: + try: + status = ResponseCode(record.receipt.status).name + except ValueError: + status = str(record.receipt.status) + else: + status = "None" + print(f" Receipt Status: {status}") + print(f" PRNG Number: {record.prng_number}") + print(f" PRNG Bytes (hex): {record.prng_bytes.hex() if record.prng_bytes else 'None'}") + + +def _print_transfer_fields(record): + print(f" HBAR Transfers: {dict(record.transfers) if record.transfers else 'None'}") + print(f" Token Transfers: {dict(record.token_transfers) if record.token_transfers else 'None'}") + print(f" NFT Transfers: { {k: len(v) for k, v in record.nft_transfers.items()} if record.nft_transfers else 'None'}") + print(f" Pending Airdrops: {len(record.new_pending_airdrops)}") + + +def _print_new_fields(record): + print(f" Consensus Timestamp: {record.consensus_timestamp}") + print(f" Parent Consensus Timestamp: {record.parent_consensus_timestamp}") + print(f" Schedule Ref: {record.schedule_ref}") + print(f" Assessed Custom Fees ({len(record.assessed_custom_fees)}):") + for fee in record.assessed_custom_fees: + token = fee.token_id if fee.token_id else "HBAR" + payers = ", ".join(str(p) for p in fee.effective_payer_account_ids) if fee.effective_payer_account_ids else "N/A" + print(f" - {fee.amount} {token} → Collector: {fee.fee_collector_account_id}, Payers: {payers}") + print(f" Automatic Token Associations ({len(record.automatic_token_associations)}):") + for assoc in record.automatic_token_associations: + print(f" - Token {assoc.token_id} → Account {assoc.account_id}") + print(f" Alias (hex): {record.alias.hex() if record.alias else 'None'}") + print(f" Ethereum Hash (hex): {record.ethereum_hash.hex() if record.ethereum_hash else 'None'}") + print(f" Paid Staking Rewards ({len(record.paid_staking_rewards)}):") + for account, amount in record.paid_staking_rewards: + print(f" - {account}: {amount} tinybars") + print(f" EVM Address (hex): {record.evm_address.hex() if record.evm_address else 'None'}") + if record.contract_create_result: + print(f" Contract Create Result: {record.contract_create_result.contract_id}") + print(f" Result bytes (first 32): {record.contract_create_result.contract_call_result[:32].hex() if record.contract_create_result.contract_call_result else 'None'}...") + else: + print(" Contract Create Result: None") + + +def print_all_fields(record): + """Print all fields of TransactionRecord in a simple, readable way.""" + print("=== TransactionRecord Example ===") + _print_basic_fields(record) + _print_transfer_fields(record) + _print_new_fields(record) + +def main(): + """Run the TransactionRecord example.""" + print("Creating mock TransactionRecord...\n") + record = create_mock_record() + print_all_fields(record) + + +if __name__ == "__main__": + main() + \ No newline at end of file diff --git a/src/hiero_sdk_python/__init__.py b/src/hiero_sdk_python/__init__.py index e9ffb31e2..513044aed 100644 --- a/src/hiero_sdk_python/__init__.py +++ b/src/hiero_sdk_python/__init__.py @@ -52,6 +52,7 @@ from .tokens.token_pause_transaction import TokenPauseTransaction from .tokens.token_airdrop_claim import TokenClaimAirdropTransaction from .tokens.assessed_custom_fee import AssessedCustomFee +from .tokens.token_association import TokenAssociation # Transaction from .transaction.transaction import Transaction @@ -193,6 +194,7 @@ "TokenBurnTransaction", "TokenGrantKycTransaction", "TokenRelationship", + "TokenAssociation", "TokenUpdateTransaction", "TokenAirdropTransaction", "TokenCancelAirdropTransaction", diff --git a/src/hiero_sdk_python/tokens/token_association.py b/src/hiero_sdk_python/tokens/token_association.py new file mode 100644 index 000000000..5c8848049 --- /dev/null +++ b/src/hiero_sdk_python/tokens/token_association.py @@ -0,0 +1,75 @@ +"""Dataclass for automatic token associations in Hedera transaction records.""" +from __future__ import annotations + +from dataclasses import dataclass + +from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.hapi.services.basic_types_pb2 import TokenAssociation as TokenAssociationProto +from hiero_sdk_python.tokens.token_id import TokenId + + +@dataclass(frozen=True) +class TokenAssociation: + """Represents an automatic token association between a token and an account. + + This class appears in `TransactionRecord.automatic_token_associations` (repeated field) + when the network creates associations automatically (e.g., during token transfers + or airdrops to unassociated accounts). + + These associations are informational only and cannot be used to create new associations + on the ledger — use TokenAssociateTransaction for that. + """ + + token_id: TokenId | None = None + """The ID of the token that was automatically associated.""" + + account_id: AccountId | None = None + """The ID of the account that received the automatic association.""" + + @classmethod + def _from_proto(cls, proto: TokenAssociationProto) -> TokenAssociation: + """Create a TokenAssociation instance from the protobuf message.""" + return cls( + token_id=( + TokenId._from_proto(proto.token_id) + if proto.HasField("token_id") + else None + ), + account_id=( + AccountId._from_proto(proto.account_id) + if proto.HasField("account_id") + else None + ), + ) + + def _to_proto(self) -> TokenAssociationProto: + """Convert this TokenAssociation instance back to a protobuf message.""" + proto = TokenAssociationProto() + + if self.token_id is not None: + proto.token_id.CopyFrom(self.token_id._to_proto()) + + if self.account_id is not None: + proto.account_id.CopyFrom(self.account_id._to_proto()) + + return proto + + def to_bytes(self) -> bytes: + """Serialize this TokenAssociation to raw protobuf bytes.""" + return self._to_proto().SerializeToString() + + @classmethod + def from_bytes(cls, data: bytes) -> TokenAssociation: + """Deserialize a TokenAssociation from raw protobuf bytes.""" + proto = TokenAssociationProto() + proto.ParseFromString(data) + return cls._from_proto(proto) + + def __repr__(self) -> str: + """Returns an unambiguous string representation for debugging.""" + return f"TokenAssociation(token_id={self.token_id!r}, account_id={self.account_id!r})" + + def __str__(self) -> str: + """Returns a human-readable string representation.""" + return self.__repr__() + \ No newline at end of file diff --git a/src/hiero_sdk_python/transaction/transaction_record.py b/src/hiero_sdk_python/transaction/transaction_record.py index 91f9479eb..75bb7d6d1 100644 --- a/src/hiero_sdk_python/transaction/transaction_record.py +++ b/src/hiero_sdk_python/transaction/transaction_record.py @@ -1,4 +1,6 @@ """ +Module for the TransactionRecord class. + This module implements the TransactionRecord class which represents a complete record of a transaction executed on the Hiero network. It serves as a comprehensive data structure that captures all aspects of a transaction's execution and its effects. @@ -16,14 +18,18 @@ """ +from __future__ import annotations from collections import defaultdict from dataclasses import dataclass, field -from typing import Optional from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.contract.contract_function_result import ContractFunctionResult from hiero_sdk_python.hapi.services import transaction_record_pb2 +from hiero_sdk_python.schedule.schedule_id import ScheduleId +from hiero_sdk_python.timestamp import Timestamp +from hiero_sdk_python.tokens.assessed_custom_fee import AssessedCustomFee from hiero_sdk_python.tokens.token_airdrop_pending_record import PendingAirdropRecord +from hiero_sdk_python.tokens.token_association import TokenAssociation from hiero_sdk_python.tokens.token_id import TokenId from hiero_sdk_python.tokens.token_nft_transfer import TokenNftTransfer from hiero_sdk_python.transaction.transaction_id import TransactionId @@ -34,6 +40,7 @@ class TransactionRecord: """ Represents a record of a completed transaction on the Hiero network. + This class combines detailed information about the a transaction including the transaction ID, receipt, token and NFT transfers, fees & other metadata such as pseudo-random number generation(PRNG) results and @@ -60,32 +67,58 @@ class TransactionRecord: with include_duplicates=True. Empty by default. children (list[TransactionRecord]): A list of children transaction records returned when queried - with include_children=True. Empty by default. + with include_children=True. Empty by default. + consensus_timestamp (Optional[Timestamp]): Network-assigned consensus timestamp when the transaction was finalized. + schedule_ref (Optional[ScheduleId]): Schedule ID if this transaction was executed via a schedule. + assessed_custom_fees (list[AssessedCustomFee]): Custom fees that were assessed and charged during execution. + automatic_token_associations (list[TokenAssociation]): + Automatic token-account associations created by the network. + parent_consensus_timestamp (Optional[Timestamp]): Consensus timestamp of the parent transaction (for child records). + alias (Optional[bytes]): New account alias created (e.g., during account creation with alias). + ethereum_hash (Optional[bytes]): Keccak-256 hash of the Ethereum-compatible transaction data. + paid_staking_rewards (list[tuple[AccountId, int]]): + Staking rewards paid out (account ID → amount in tinybars). + evm_address (Optional[bytes]): EVM address created or associated with the account. + contract_create_result (Optional[ContractFunctionResult]): + Result of a contract creation transaction (if applicable) """ - transaction_id: Optional[TransactionId] = None - transaction_hash: Optional[bytes] = None - transaction_memo: Optional[str] = None - transaction_fee: Optional[int] = None - receipt: Optional[TransactionReceipt] = None - call_result: Optional[ContractFunctionResult] = None + transaction_id: TransactionId | None = None + transaction_hash: bytes | None = None + transaction_memo: str | None = None + transaction_fee: int | None = None + receipt: TransactionReceipt | None = None + call_result: ContractFunctionResult | None = None token_transfers: defaultdict[TokenId, defaultdict[AccountId, int]] = field(default_factory=lambda: defaultdict(lambda: defaultdict(int))) nft_transfers: defaultdict[TokenId, list[TokenNftTransfer]] = field(default_factory=lambda: defaultdict(list[TokenNftTransfer])) transfers: defaultdict[AccountId, int] = field(default_factory=lambda: defaultdict(int)) new_pending_airdrops: list[PendingAirdropRecord] = field(default_factory=list) - prng_number: Optional[int] = None - prng_bytes: Optional[bytes] = None - duplicates: list['TransactionRecord'] = field(default_factory=list) - children: list['TransactionRecord'] = field(default_factory=list) + prng_number: int | None = None + prng_bytes: bytes | None = None + duplicates: list[TransactionRecord] = field(default_factory=list) + children: list[TransactionRecord] = field(default_factory=list) + + consensus_timestamp: Timestamp | None = None + schedule_ref: ScheduleId | None = None + assessed_custom_fees: list[AssessedCustomFee] = field(default_factory=list) + automatic_token_associations: list[TokenAssociation] = field(default_factory=list) + parent_consensus_timestamp: Timestamp | None = None + alias: bytes | None = None + ethereum_hash: bytes | None = None + paid_staking_rewards: list[tuple[AccountId, int]] = field(default_factory=list) + evm_address: bytes | None = None + contract_create_result: ContractFunctionResult | None = None def __repr__(self) -> str: """Returns a human-readable string representation of the TransactionRecord. This method constructs a detailed string containing all significant fields of the - transaction record including transaction ID, hash, memo, fees, status, transfers, - and PRNG results. For the receipt status, it attempts to resolve the numeric status + transaction record including transaction ID, hash, memo, fees, status, transfers, + PRNG results, consensus timestamp, schedule ref, custom fees, automatic associations, + parent timestamp, alias, Ethereum hash, staking rewards, EVM address, contract create result. + For the receipt status, it attempts to resolve the numeric status to a human-readable ResponseCode name. Returns: @@ -112,16 +145,26 @@ def __repr__(self) -> str: f"prng_number={self.prng_number}, " f"prng_bytes={self.prng_bytes}, " f"duplicates_count={len(self.duplicates)}, " - f"children_count={len(self.children)})") - + f"children_count={len(self.children)}, " + f"consensus_timestamp={self.consensus_timestamp}, " + f"schedule_ref={self.schedule_ref}, " + f"assessed_custom_fees={self.assessed_custom_fees}, " + f"automatic_token_associations={self.automatic_token_associations}, " + f"parent_consensus_timestamp={self.parent_consensus_timestamp}, " + f"alias={self.alias}, " + f"ethereum_hash={self.ethereum_hash}, " + f"paid_staking_rewards={self.paid_staking_rewards}, " + f"evm_address={self.evm_address}, " + f"contract_create_result={self.contract_create_result})") + @classmethod def _from_proto( cls, proto: transaction_record_pb2.TransactionRecord, - transaction_id: Optional[TransactionId] = None, - duplicates: Optional[list['TransactionRecord']] = None, - children: Optional[list['TransactionRecord']] = None, - ) -> 'TransactionRecord': + transaction_id: TransactionId | None = None, + duplicates: list[TransactionRecord] | None = None, + children: list[TransactionRecord] | None = None, + ) -> TransactionRecord: """Creates a TransactionRecord instance from a protobuf transaction record. This method performs complex data aggregation from the protobuf message, @@ -132,6 +175,11 @@ def _from_proto( - Contract execution results - Airdrop records - PRNG (pseudo-random number generation) results + - Other protobuf fields: consensus timestamp, schedule reference, + assessed custom fees, automatic token associations, parent consensus + timestamp, alias, ethereum hash, paid staking rewards, EVM address, + and contract create result + The method maps all nested transfer data from the raw protobuf message into the structured format used by the TransactionRecord class & organizing them @@ -155,7 +203,54 @@ def _from_proto( token_transfers, nft_transfers = cls._parse_token_transfers(proto) transfers = cls._parse_hbar_transfers(proto) new_pending_airdrops = cls._parse_pending_airdrops(proto) - call_result = cls._parse_contract_call_result(proto) + call_result = None + contract_create_result = None + + body_case = proto.WhichOneof("body") + if body_case == "contractCallResult": + call_result = ContractFunctionResult._from_proto(proto.contractCallResult) + elif body_case == "contractCreateResult": + contract_create_result = ContractFunctionResult._from_proto(proto.contractCreateResult) + + consensus_timestamp = ( + Timestamp._from_protobuf(proto.consensusTimestamp) + if proto.HasField("consensusTimestamp") else None + ) + parent_consensus_timestamp = ( + Timestamp._from_protobuf(proto.parent_consensus_timestamp) + if proto.HasField("parent_consensus_timestamp") else None + ) + schedule_ref = ( + ScheduleId._from_proto(proto.scheduleRef) + if proto.HasField("scheduleRef") else None + ) + assessed_custom_fees = [ + AssessedCustomFee._from_proto(fee) for fee in proto.assessed_custom_fees + ] + automatic_token_associations = [ + TokenAssociation._from_proto(assoc) for assoc in proto.automatic_token_associations + ] + paid_staking_rewards = [ + (AccountId._from_proto(r.accountID), r.amount) + for r in proto.paid_staking_rewards + ] + contract_create_result = ( + ContractFunctionResult._from_proto(proto.contractCreateResult) + if proto.HasField("contractCreateResult") else None + ) + alias = proto.alias if proto.alias else None + ethereum_hash = proto.ethereum_hash if proto.ethereum_hash else None + evm_address = proto.evm_address if proto.evm_address else None + + entropy_case = proto.WhichOneof("entropy") + + prng_number = None + prng_bytes = None + + if entropy_case == "prng_number": + prng_number = proto.prng_number + elif entropy_case == "prng_bytes": + prng_bytes = proto.prng_bytes return cls( transaction_id=tx_id, @@ -168,16 +263,26 @@ def _from_proto( transfers=transfers, new_pending_airdrops=new_pending_airdrops, call_result=call_result, - prng_number=proto.prng_number, - prng_bytes=proto.prng_bytes, + prng_number=prng_number, + prng_bytes=prng_bytes, duplicates=duplicates, children=children, + consensus_timestamp=consensus_timestamp, + schedule_ref=schedule_ref, + assessed_custom_fees=assessed_custom_fees, + automatic_token_associations=automatic_token_associations, + parent_consensus_timestamp=parent_consensus_timestamp, + alias=alias, + ethereum_hash=ethereum_hash, + paid_staking_rewards=paid_staking_rewards, + evm_address=evm_address, + contract_create_result=contract_create_result, ) @staticmethod def _resolve_transaction_id( proto: transaction_record_pb2.TransactionRecord, - transaction_id: Optional[TransactionId], + transaction_id: TransactionId | None, ) -> TransactionId: """Resolves the transaction ID from proto or raises error if not available.""" if proto.HasField("transactionID"): @@ -235,7 +340,7 @@ def _parse_pending_airdrops( @staticmethod def _parse_contract_call_result( proto: transaction_record_pb2.TransactionRecord, - ) -> Optional[ContractFunctionResult]: + ) -> ContractFunctionResult | None: """Parses contract call result from proto if present.""" if proto.HasField("contractCallResult"): return ContractFunctionResult._from_proto(proto.contractCallResult) @@ -256,6 +361,10 @@ def _to_proto(self) -> transaction_record_pb2.TransactionRecord: - Contract execution results - Airdrop records - PRNG (pseudo-random number generation) results + - Other protobuf fields: consensus timestamp, schedule reference, + assessed custom fees, automatic token associations, parent consensus + timestamp, alias, ethereum hash, paid staking rewards, EVM address, + and contract create result The method performs a deep conversion of all nested objects (token transfers, NFT transfers, account transfers, and pending airdrops) to their respective @@ -266,17 +375,33 @@ def _to_proto(self) -> transaction_record_pb2.TransactionRecord: all the transaction record data in a format suitable for network transmission or storage. """ + if self.call_result is not None and self.contract_create_result is not None: + raise ValueError( + "contractCallResult and contractCreateResult are mutually exclusive " + "proto oneof fields and cannot both be set simultaneously." + ) + + if self.prng_number is not None and self.prng_bytes is not None: + raise ValueError( + "prng_number and prng_bytes are mutually exclusive " + "proto oneof fields (entropy) and cannot both be set simultaneously." + ) + record_proto = transaction_record_pb2.TransactionRecord( transactionHash=self.transaction_hash, memo=self.transaction_memo, transactionFee=self.transaction_fee, receipt=self.receipt._to_proto() if self.receipt else None, - contractCallResult=( - self.call_result._to_proto() if self.call_result else None - ), - prng_number=self.prng_number, - prng_bytes=self.prng_bytes, ) + + if self.call_result is not None: + record_proto.contractCallResult.CopyFrom(self.call_result._to_proto()) + elif self.contract_create_result is not None: + record_proto.contractCreateResult.CopyFrom(self.contract_create_result._to_proto()) + if self.prng_number is not None: + record_proto.prng_number = self.prng_number + if self.prng_bytes is not None: + record_proto.prng_bytes = self.prng_bytes if self.transaction_id is not None: record_proto.transactionID.CopyFrom(self.transaction_id._to_proto()) @@ -302,6 +427,38 @@ def _to_proto(self) -> transaction_record_pb2.TransactionRecord: for pending_airdrop in self.new_pending_airdrops: record_proto.new_pending_airdrops.add().CopyFrom(pending_airdrop._to_proto()) + if self.consensus_timestamp is not None: + record_proto.consensusTimestamp.CopyFrom(self.consensus_timestamp._to_protobuf()) + + if self.schedule_ref is not None: + record_proto.scheduleRef.CopyFrom(self.schedule_ref._to_proto()) + + record_proto.assessed_custom_fees.extend( + fee._to_proto() for fee in self.assessed_custom_fees + ) + + record_proto.automatic_token_associations.extend( + assoc._to_proto() for assoc in self.automatic_token_associations + ) + + if self.parent_consensus_timestamp is not None: + record_proto.parent_consensus_timestamp.CopyFrom(self.parent_consensus_timestamp._to_protobuf()) + + if self.alias is not None: + record_proto.alias = self.alias + + if self.ethereum_hash is not None: + record_proto.ethereum_hash = self.ethereum_hash + + for account_id, amount in self.paid_staking_rewards: + aa = record_proto.paid_staking_rewards.add() + aa.accountID.CopyFrom(account_id._to_proto()) + aa.amount = amount + + if self.evm_address is not None: + record_proto.evm_address = self.evm_address + + if self.contract_create_result is not None: + record_proto.contractCreateResult.CopyFrom(self.contract_create_result._to_proto()) return record_proto - diff --git a/tests/integration/prng_transaction_e2e_test.py b/tests/integration/prng_transaction_e2e_test.py index e467bd68f..ae9df7d83 100644 --- a/tests/integration/prng_transaction_e2e_test.py +++ b/tests/integration/prng_transaction_e2e_test.py @@ -24,7 +24,7 @@ def test_integration_prng_transaction_can_execute(env): assert ( record.prng_number >= 0 and record.prng_number <= 100 ), "PRNG number should be between 0 and 100" - assert record.prng_bytes == b"", "PRNG bytes should be empty bytes" + assert record.prng_bytes is None, "PRNG bytes should be None" @pytest.mark.integration @@ -37,10 +37,9 @@ def test_integration_prng_transaction_can_execute_without_range(env): record = TransactionRecordQuery(receipt.transaction_id).execute(env.client) - assert record.prng_number == 0, "PRNG number should be 0" - assert len(record.prng_bytes) == 48, "PRNG bytes should be 48 bytes" + assert record.prng_number is None, "PRNG number should be None" assert record.prng_bytes is not None, "PRNG bytes should not be None" - + assert len(record.prng_bytes) == 48, "PRNG bytes should be 48 bytes" @pytest.mark.integration def test_integration_prng_transaction_can_execute_with_zero_range(env): @@ -52,6 +51,6 @@ def test_integration_prng_transaction_can_execute_with_zero_range(env): record = TransactionRecordQuery(receipt.transaction_id).execute(env.client) - assert record.prng_number == 0, "PRNG number should be 0" - assert len(record.prng_bytes) == 48, "PRNG bytes should be 48 bytes" + assert record.prng_number is None, "PRNG number should be None" assert record.prng_bytes is not None, "PRNG bytes should not be None" + assert len(record.prng_bytes) == 48, "PRNG bytes should be 48 bytes" \ No newline at end of file diff --git a/tests/integration/transaction_record_query_e2e_test.py b/tests/integration/transaction_record_query_e2e_test.py index 523938864..a0b24f291 100644 --- a/tests/integration/transaction_record_query_e2e_test.py +++ b/tests/integration/transaction_record_query_e2e_test.py @@ -1,11 +1,16 @@ -import pytest +"""Integration tests for TransactionRecordQuery end-to-end functionality.""" + import os -from hiero_sdk_python import AccountId, Client, TransactionRecord + +import pytest + +from hiero_sdk_python import AccountId, Timestamp, TransactionRecord +from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.hbar import Hbar -from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.query.transaction_record_query import TransactionRecordQuery from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.schedule.schedule_id import ScheduleId from hiero_sdk_python.tokens.nft_id import NftId from hiero_sdk_python.tokens.token_associate_transaction import ( TokenAssociateTransaction, @@ -16,7 +21,6 @@ IntegrationTestEnv, create_fungible_token, create_nft_token, - env, ) @@ -41,6 +45,7 @@ def _submit_alias_auto_create_transfer(env: IntegrationTestEnv): @pytest.mark.integration def test_transaction_record_query_can_execute(): + """Test that a basic transaction record query can execute successfully.""" env = IntegrationTestEnv() try: @@ -75,41 +80,46 @@ def test_transaction_record_query_can_execute(): @pytest.mark.integration -def test_transaction_record_query_include_children_returns_child_records(env): +def test_transaction_record_query_include_children_returns_child_records(): """Querying an alias auto-create parent record should return parsed child records.""" - parent_transaction_id = _submit_alias_auto_create_transfer(env) - parent_account_id = parent_transaction_id.account_id - - parent_record = ( - TransactionRecordQuery() - .set_transaction_id(parent_transaction_id) - .set_include_children(True) - .execute(env.client) - ) - - assert parent_record.transaction_id == parent_transaction_id - assert parent_record.receipt.status == ResponseCode.SUCCESS - assert len(parent_record.children) > 0 - assert parent_record.transfers[parent_account_id] < 0 + env = IntegrationTestEnv() + try: + parent_transaction_id = _submit_alias_auto_create_transfer(env) + parent_account_id = parent_transaction_id.account_id - child_record = parent_record.children[0] - created_account_id = child_record.receipt.account_id + parent_record = ( + TransactionRecordQuery() + .set_transaction_id(parent_transaction_id) + .set_include_children(True) + .execute(env.client) + ) - assert isinstance(child_record, TransactionRecord) - assert child_record.receipt.status == ResponseCode.SUCCESS - assert child_record.transaction_id == parent_transaction_id - assert created_account_id is not None - assert created_account_id.shard == 0 - assert created_account_id.realm == 0 - assert created_account_id.num > 0 - assert child_record.transaction_hash != parent_record.transaction_hash - assert child_record.transaction_memo == "" - assert child_record.children == [] - assert child_record.duplicates == [] + assert parent_record.transaction_id == parent_transaction_id + assert parent_record.receipt.status == ResponseCode.SUCCESS + assert len(parent_record.children) > 0 + assert parent_record.transfers[parent_account_id] < 0 + + child_record = parent_record.children[0] + created_account_id = child_record.receipt.account_id + + assert isinstance(child_record, TransactionRecord) + assert child_record.receipt.status == ResponseCode.SUCCESS + assert child_record.transaction_id == parent_transaction_id + assert created_account_id is not None + assert created_account_id.shard == 0 + assert created_account_id.realm == 0 + assert created_account_id.num > 0 + assert child_record.transaction_hash != parent_record.transaction_hash + assert child_record.transaction_memo == "" + assert child_record.children == [] + assert child_record.duplicates == [] + finally: + env.close() @pytest.mark.integration def test_transaction_record_query_can_execute_nft_transfer(): + """Test that NFT transfers are correctly captured in the transaction record.""" env = IntegrationTestEnv() try: @@ -196,6 +206,7 @@ def test_transaction_record_query_can_execute_nft_transfer(): @pytest.mark.integration def test_transaction_record_query_can_execute_fungible_transfer(): + """Test that fungible token transfers are correctly captured in the record.""" env = IntegrationTestEnv() try: @@ -265,6 +276,7 @@ def test_transaction_record_query_can_execute_fungible_transfer(): ), ) def test_query_with_include_duplicates(): + """Verify that duplicate records are returned when the flag is enabled.""" env = IntegrationTestEnv() try: # Use a fresh keypair for isolation @@ -329,3 +341,83 @@ def test_query_with_include_duplicates(): finally: env.close() +@pytest.mark.integration +def test_transaction_record_new_fields(): + """Simple test to verify some fields in TransactionRecord. + + consensus_timestamp, automatic_token_associations, paid_staking_rewards, + evm_address, alias, ethereum_hash, parent_consensus_timestamp, + assessed_custom_fees, schedule_ref, and PRNG oneof handling. + """ + env = IntegrationTestEnv() + try: + new_account_private_key = PrivateKey.generate_ed25519() + new_account_public_key = new_account_private_key.public_key() + + receipt = ( + AccountCreateTransaction() + .set_key_without_alias(new_account_public_key) + .set_initial_balance(Hbar(1)) + .execute(env.client) + ) + assert receipt.status == ResponseCode.SUCCESS + + record: TransactionRecord = TransactionRecordQuery( + receipt.transaction_id + ).execute(env.client) + + _assert_basic_record_fields(record, receipt) + _assert_fields(record) + finally: + env.close() + +def _assert_basic_record_fields(record: TransactionRecord, receipt) -> None: + """Assert basic fields that existed before this PR.""" + assert record.transaction_id == receipt.transaction_id + assert record.transaction_fee > 0 + assert record.consensus_timestamp is not None + assert record.transaction_hash is not None and len(record.transaction_hash) > 0 + +def _assert_fields(record: TransactionRecord) -> None: + """Assert all newly exposed fields from this PR.""" + _assert_list_fields(record) + _assert_optional_bytes_fields(record) + _assert_timestamp_and_schedule_fields(record) + _assert_prng_fields(record) + _assert_token_associations(record) + +def _assert_list_fields(record: TransactionRecord) -> None: + """Assert list fields are properly initialized.""" + assert isinstance(record.automatic_token_associations, list) + assert isinstance(record.paid_staking_rewards, list) + assert isinstance(record.assessed_custom_fees, list) + +def _assert_optional_bytes_fields(record: TransactionRecord) -> None: + """Assert optional bytes fields.""" + assert record.evm_address is None or isinstance(record.evm_address, bytes) + assert record.alias is None or isinstance(record.alias, bytes) + assert record.ethereum_hash is None or isinstance(record.ethereum_hash, bytes) + +def _assert_timestamp_and_schedule_fields(record: TransactionRecord) -> None: + """Assert timestamp and schedule related fields.""" + assert record.parent_consensus_timestamp is None or isinstance( + record.parent_consensus_timestamp, Timestamp + ) + assert record.schedule_ref is None or isinstance(record.schedule_ref, ScheduleId) + +def _assert_prng_fields(record: TransactionRecord) -> None: + """Assert PRNG oneof handling.""" + assert not (record.prng_number is not None and record.prng_bytes is not None), \ + "prng_number and prng_bytes are mutually exclusive" + + if record.prng_number is not None: + assert isinstance(record.prng_number, int) + if record.prng_bytes is not None: + assert isinstance(record.prng_bytes, bytes) + +def _assert_token_associations(record: TransactionRecord) -> None: + """Assert TokenAssociation model fields.""" + for assoc in record.automatic_token_associations: + assert hasattr(assoc, "token_id") + assert hasattr(assoc, "account_id") + \ No newline at end of file diff --git a/tests/unit/token_association_test.py b/tests/unit/token_association_test.py new file mode 100644 index 000000000..e80b79c8f --- /dev/null +++ b/tests/unit/token_association_test.py @@ -0,0 +1,242 @@ +"""Unit tests for the TokenAssociation class.""" +import pytest + +from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.hapi.services.basic_types_pb2 import TokenAssociation as TokenAssociationProto +from hiero_sdk_python.tokens.token_association import TokenAssociation +from hiero_sdk_python.tokens.token_id import TokenId + +pytestmark = pytest.mark.unit + + +@pytest.fixture +def sample_token_id() -> TokenId: + """Return a sample TokenId for testing.""" + return TokenId(shard=0, realm=0, num=5678) + + +@pytest.fixture +def sample_account_id() -> AccountId: + """Return a sample AccountId for testing.""" + return AccountId(shard=0, realm=0, num=1234) + + +def test_default_initialization(): + """Test default initialization of TokenAssociation (both fields None).""" + assoc = TokenAssociation() + + assert assoc.token_id is None + assert assoc.account_id is None + + +def test_initialization_both_fields(sample_token_id: TokenId, sample_account_id: AccountId): + """Test initialization with both token_id and account_id provided.""" + assoc = TokenAssociation( + token_id=sample_token_id, + account_id=sample_account_id + ) + + assert assoc.token_id == sample_token_id + assert assoc.account_id == sample_account_id + + +def test_initialization_token_only(sample_token_id: TokenId): + """Test initialization with only token_id (account_id None).""" + assoc = TokenAssociation(token_id=sample_token_id) + + assert assoc.token_id == sample_token_id + assert assoc.account_id is None + + +def test_initialization_account_only(sample_account_id: AccountId): + """Test initialization with only account_id (token_id None).""" + assoc = TokenAssociation(account_id=sample_account_id) + + assert assoc.token_id is None + assert assoc.account_id == sample_account_id + + +def test_from_proto_both_fields(sample_token_id: TokenId, sample_account_id: AccountId): + """Test _from_proto with both token_id and account_id present in proto.""" + proto = TokenAssociationProto() + proto.token_id.shardNum = sample_token_id.shard + proto.token_id.realmNum = sample_token_id.realm + proto.token_id.tokenNum = sample_token_id.num + proto.account_id.shardNum = sample_account_id.shard + proto.account_id.realmNum = sample_account_id.realm + proto.account_id.accountNum = sample_account_id.num + + assoc = TokenAssociation._from_proto(proto) + + assert assoc.token_id == sample_token_id + assert assoc.account_id == sample_account_id + + +def test_from_proto_token_only(sample_token_id: TokenId): + """Test _from_proto with only token_id present (account_id absent).""" + proto = TokenAssociationProto() + proto.token_id.shardNum = sample_token_id.shard + proto.token_id.realmNum = sample_token_id.realm + proto.token_id.tokenNum = sample_token_id.num + + assoc = TokenAssociation._from_proto(proto) + + assert assoc.token_id == sample_token_id + assert assoc.account_id is None + + +def test_from_proto_account_only(sample_account_id: AccountId): + """Test _from_proto with only account_id present (token_id absent).""" + proto = TokenAssociationProto() + proto.account_id.shardNum = sample_account_id.shard + proto.account_id.realmNum = sample_account_id.realm + proto.account_id.accountNum = sample_account_id.num + + assoc = TokenAssociation._from_proto(proto) + + assert assoc.token_id is None + assert assoc.account_id == sample_account_id + + +def test_from_proto_empty(): + """Test _from_proto with completely empty protobuf message.""" + proto = TokenAssociationProto() + assoc = TokenAssociation._from_proto(proto) + + assert assoc.token_id is None + assert assoc.account_id is None + + +def test_to_proto_both_fields(sample_token_id: TokenId, sample_account_id: AccountId): + """Test _to_proto serializes both fields correctly.""" + assoc = TokenAssociation( + token_id=sample_token_id, + account_id=sample_account_id + ) + + proto = assoc._to_proto() + + assert proto.HasField("token_id") + assert proto.token_id.shardNum == sample_token_id.shard + assert proto.token_id.realmNum == sample_token_id.realm + assert proto.token_id.tokenNum == sample_token_id.num + + assert proto.HasField("account_id") + assert proto.account_id.shardNum == sample_account_id.shard + assert proto.account_id.realmNum == sample_account_id.realm + assert proto.account_id.accountNum == sample_account_id.num + + +def test_to_proto_token_only(sample_token_id: TokenId): + """Test _to_proto with only token_id (account_id None).""" + assoc = TokenAssociation(token_id=sample_token_id) + proto = assoc._to_proto() + + assert proto.HasField("token_id") + assert not proto.HasField("account_id") + + +def test_to_proto_account_only(sample_account_id: AccountId): + """Test _to_proto with only account_id (token_id None).""" + assoc = TokenAssociation(account_id=sample_account_id) + proto = assoc._to_proto() + + assert not proto.HasField("token_id") + assert proto.HasField("account_id") + + +def test_to_proto_empty(): + """Test _to_proto with empty/default instance.""" + assoc = TokenAssociation() + proto = assoc._to_proto() + + assert not proto.HasField("token_id") + assert not proto.HasField("account_id") + + +def test_round_trip_both_fields(sample_token_id: TokenId, sample_account_id: AccountId): + """Test full round-trip conversion preserves both fields.""" + original = TokenAssociation( + token_id=sample_token_id, + account_id=sample_account_id + ) + + proto = original._to_proto() + reconstructed = TokenAssociation._from_proto(proto) + + assert reconstructed.token_id == original.token_id + assert reconstructed.account_id == original.account_id + + +def test_round_trip_token_only(sample_token_id: TokenId): + """Test round-trip with only token_id.""" + original = TokenAssociation(token_id=sample_token_id) + proto = original._to_proto() + reconstructed = TokenAssociation._from_proto(proto) + + assert reconstructed.token_id == original.token_id + assert reconstructed.account_id is None + + +def test_round_trip_account_only(sample_account_id: AccountId): + """Test round-trip with only account_id.""" + original = TokenAssociation(account_id=sample_account_id) + proto = original._to_proto() + reconstructed = TokenAssociation._from_proto(proto) + + assert reconstructed.token_id is None + assert reconstructed.account_id == original.account_id + +def test_round_trip_empty(): + """Test round-trip with empty/default instance.""" + original = TokenAssociation() + proto = original._to_proto() + reconstructed = TokenAssociation._from_proto(proto) + + assert reconstructed.token_id is None + assert reconstructed.account_id is None + +def test_bytes_round_trip_both_fields( + sample_token_id: TokenId, + sample_account_id: AccountId, + ): + """Test round-trip via public to_bytes() / from_bytes() preserves both fields.""" + original = TokenAssociation( + token_id=sample_token_id, + account_id=sample_account_id, + ) + + reconstructed = TokenAssociation.from_bytes(original.to_bytes()) + + assert reconstructed == original + +def test_bytes_round_trip_empty(): + """Test round-trip via to_bytes()/from_bytes() with empty/default instance.""" + original = TokenAssociation() + reconstructed = TokenAssociation.from_bytes(original.to_bytes()) + assert reconstructed == original + assert reconstructed.token_id is None + assert reconstructed.account_id is None + +def test_repr_representation(sample_token_id: TokenId, sample_account_id: AccountId): + """Test __repr__ output for TokenAssociation.""" + assoc = TokenAssociation( + token_id=sample_token_id, + account_id=sample_account_id + ) + repr_str = repr(assoc) + + assert "TokenAssociation" in repr_str + assert "token_id=" in repr_str + assert "account_id=" in repr_str + + +def test_repr_with_none_fields(): + """Test __repr__ when some fields are None.""" + assoc = TokenAssociation(token_id=TokenId(shard=0, realm=0, num=5678)) + repr_str = repr(assoc) + assert "token_id=TokenId" in repr_str + assert "account_id=None" in repr_str + + empty = TokenAssociation() + assert "TokenAssociation(token_id=None, account_id=None)" in repr(empty) diff --git a/tests/unit/transaction_record_test.py b/tests/unit/transaction_record_test.py index 426379123..4e9078075 100644 --- a/tests/unit/transaction_record_test.py +++ b/tests/unit/transaction_record_test.py @@ -1,19 +1,28 @@ -from hiero_sdk_python.tokens.token_airdrop_pending_id import PendingAirdropId -from hiero_sdk_python.tokens.token_airdrop_pending_record import PendingAirdropRecord -import pytest +"""Unit tests for the TransactionRecord class functionality.""" + from collections import defaultdict + +import pytest + from hiero_sdk_python.account.account_id import AccountId -from hiero_sdk_python.tokens.token_id import TokenId -from hiero_sdk_python.tokens.token_nft_transfer import TokenNftTransfer -from hiero_sdk_python.transaction.transaction_record import TransactionRecord -from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt -from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.contract.contract_function_result import ContractFunctionResult +from hiero_sdk_python.contract.contract_id import ContractId from hiero_sdk_python.hapi.services import ( - transaction_record_pb2, transaction_receipt_pb2, + transaction_record_pb2, ) -from hiero_sdk_python.contract.contract_function_result import ContractFunctionResult -from hiero_sdk_python.contract.contract_id import ContractId +from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.schedule.schedule_id import ScheduleId +from hiero_sdk_python.timestamp import Timestamp +from hiero_sdk_python.tokens.assessed_custom_fee import AssessedCustomFee +from hiero_sdk_python.tokens.token_airdrop_pending_id import PendingAirdropId +from hiero_sdk_python.tokens.token_airdrop_pending_record import PendingAirdropRecord +from hiero_sdk_python.tokens.token_association import TokenAssociation +from hiero_sdk_python.tokens.token_id import TokenId +from hiero_sdk_python.tokens.token_nft_transfer import TokenNftTransfer +from hiero_sdk_python.transaction.transaction_id import TransactionId # noqa: F401 +from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt +from hiero_sdk_python.transaction.transaction_record import TransactionRecord pytestmark = pytest.mark.unit @@ -27,7 +36,10 @@ def transaction_record(transaction_id): ), transaction_id=transaction_id ) - + + ts = Timestamp(seconds=1234567890, nanos=500000000) + sched = ScheduleId(0, 0, 9999) + return TransactionRecord( transaction_id=transaction_id, transaction_hash=b'\x01\x02\x03\x04' * 12, @@ -41,12 +53,37 @@ def transaction_record(transaction_id): prng_number=100, prng_bytes=None, children=[], + consensus_timestamp=ts, + schedule_ref=sched, + assessed_custom_fees=[ + AssessedCustomFee( + amount=500000000, + fee_collector_account_id=AccountId(shard=0, realm=0, num=98) + ) + ], + automatic_token_associations=[ + TokenAssociation( + token_id=TokenId(shard=0, realm=0, num=5678), + account_id=AccountId(shard=0, realm=0, num=1234) + ) + ], + parent_consensus_timestamp=ts, + alias=b'\x12\x34\x56\x78', + ethereum_hash=b'\xab' * 32, + paid_staking_rewards=[ + (AccountId(shard=0, realm=0, num=456), 1000000) + ], + evm_address=b'\x12' * 20, + contract_create_result=ContractFunctionResult( + contract_id=ContractId(shard=0, realm=0, contract=789), + contract_call_result=b"Mock result" + ), ) @pytest.fixture def proto_transaction_record(transaction_id): """Create a mock transaction record protobuf.""" - proto = transaction_record_pb2.TransactionRecord( + return transaction_record_pb2.TransactionRecord( transactionHash=b'\x01\x02\x03\x04' * 12, memo="Test transaction memo", transactionFee=100000, @@ -54,10 +91,9 @@ def proto_transaction_record(transaction_id): transactionID=transaction_id._to_proto(), prng_number=100, ) - return proto def test_transaction_record_initialization(transaction_record, transaction_id): - """Test the initialization of the TransactionRecord class""" + """Test the initialization of the TransactionRecord class.""" assert transaction_record.transaction_id == transaction_id assert transaction_record.transaction_hash == b'\x01\x02\x03\x04' * 12 assert transaction_record.transaction_memo == "Test transaction memo" @@ -70,7 +106,7 @@ def test_transaction_record_initialization(transaction_record, transaction_id): def test_transaction_record_default_initialization(): - """Test the default initialization of the TransactionRecord class""" + """Test the default initialization of the TransactionRecord class.""" record = TransactionRecord() assert record.transaction_id is None assert record.transaction_hash is None @@ -89,8 +125,19 @@ def test_transaction_record_default_initialization(): assert record.duplicates == [] assert record.children == [] + assert record.consensus_timestamp is None + assert record.schedule_ref is None + assert record.assessed_custom_fees == [] + assert record.automatic_token_associations == [] + assert record.parent_consensus_timestamp is None + assert record.alias is None + assert record.ethereum_hash is None + assert record.paid_staking_rewards == [] + assert record.evm_address is None + assert record.contract_create_result is None + def test_from_proto(proto_transaction_record, transaction_id): - """Test the from_proto method of the TransactionRecord class""" + """Test the from_proto method of the TransactionRecord class.""" record = TransactionRecord._from_proto(proto_transaction_record, transaction_id) assert record.transaction_id == transaction_id @@ -100,10 +147,21 @@ def test_from_proto(proto_transaction_record, transaction_id): assert isinstance(record.receipt, TransactionReceipt) assert record.receipt.status == ResponseCode.SUCCESS assert record.prng_number == 100 - assert record.prng_bytes == b"" + assert record.prng_bytes is None + + assert record.consensus_timestamp is None + assert record.schedule_ref is None + assert record.assessed_custom_fees == [] + assert record.automatic_token_associations == [] + assert record.parent_consensus_timestamp is None + assert record.alias is None + assert record.ethereum_hash is None + assert record.paid_staking_rewards == [] + assert record.evm_address is None + assert record.contract_create_result is None def test_from_proto_with_transfers(transaction_id): - """Test from_proto with HBAR transfers""" + """Test from_proto with HBAR transfers.""" proto = transaction_record_pb2.TransactionRecord() transfer = proto.transferList.accountAmounts.add() transfer.accountID.CopyFrom(AccountId(0, 0, 200)._to_proto()) @@ -113,7 +171,7 @@ def test_from_proto_with_transfers(transaction_id): assert record.transfers[AccountId(0, 0, 200)] == 1000 def test_from_proto_with_token_transfers(transaction_id): - """Test from_proto with token transfers""" + """Test from_proto with token transfers.""" proto = transaction_record_pb2.TransactionRecord() token_list = proto.tokenTransferLists.add() token_list.token.CopyFrom(TokenId(0, 0, 300)._to_proto()) @@ -126,7 +184,7 @@ def test_from_proto_with_token_transfers(transaction_id): assert record.token_transfers[TokenId(0, 0, 300)][AccountId(0, 0, 200)] == 500 def test_from_proto_with_nft_transfers(transaction_id): - """Test from_proto with NFT transfers""" + """Test from_proto with NFT transfers.""" proto = transaction_record_pb2.TransactionRecord() token_list = proto.tokenTransferLists.add() token_list.token.CopyFrom(TokenId(0, 0, 300)._to_proto()) @@ -143,10 +201,10 @@ def test_from_proto_with_nft_transfers(transaction_id): assert transfer.sender_id == AccountId(0, 0, 100) assert transfer.receiver_id == AccountId(0, 0, 200) assert transfer.serial_number == 1 - assert transfer.is_approved == False + assert transfer.is_approved is False def test_from_proto_with_new_pending_airdrops(transaction_id): - """Test from_proto with Pending Airdrops""" + """Test from_proto with Pending Airdrops.""" sender = AccountId(0,0,100) receiver = AccountId(0,0,200) token_id = TokenId(0,0,1) @@ -166,27 +224,155 @@ def test_from_proto_with_new_pending_airdrops(transaction_id): def test_from_proto_with_prng_number(transaction_id): - """Test from_proto with prng_number set""" + """Test from_proto with prng_number set.""" proto = transaction_record_pb2.TransactionRecord() proto.prng_number = 42 record = TransactionRecord._from_proto(proto, transaction_id) assert record.prng_number == 42 - assert record.prng_bytes == b"" + assert record.prng_bytes is None def test_from_proto_with_prng_bytes(transaction_id): - """Test from_proto with prng_bytes set""" + """Test from_proto with prng_bytes set.""" proto = transaction_record_pb2.TransactionRecord() proto.prng_bytes = b"123" record = TransactionRecord._from_proto(proto, transaction_id) assert record.prng_bytes == b"123" - assert record.prng_number == 0 + assert record.prng_number is None + +def test_from_proto_with_consensus_timestamp(transaction_id): + """Test parsing consensus_timestamp from proto.""" + proto = transaction_record_pb2.TransactionRecord() + proto.consensusTimestamp.seconds = 1234567890 + proto.consensusTimestamp.nanos = 500000000 + + record = TransactionRecord._from_proto(proto, transaction_id) + assert record.consensus_timestamp is not None + assert record.consensus_timestamp.seconds == 1234567890 + assert record.consensus_timestamp.nanos == 500000000 + + +def test_from_proto_with_schedule_ref(transaction_id): + """Test parsing schedule_ref from proto.""" + proto = transaction_record_pb2.TransactionRecord() + proto.scheduleRef.shardNum = 0 + proto.scheduleRef.realmNum = 0 + proto.scheduleRef.scheduleNum = 9999 + + record = TransactionRecord._from_proto(proto, transaction_id) + assert record.schedule_ref is not None + assert record.schedule_ref.shard == 0 + assert record.schedule_ref.realm == 0 + assert record.schedule_ref.schedule == 9999 + + +def test_from_proto_with_assessed_custom_fees(transaction_id): + """Test parsing assessed_custom_fees from proto.""" + proto = transaction_record_pb2.TransactionRecord() + fee = proto.assessed_custom_fees.add() + fee.amount = 500000000 + fee.fee_collector_account_id.shardNum = 0 + fee.fee_collector_account_id.realmNum = 0 + fee.fee_collector_account_id.accountNum = 98 + + record = TransactionRecord._from_proto(proto, transaction_id) + assert len(record.assessed_custom_fees) == 1 + f = record.assessed_custom_fees[0] + assert isinstance(f, AssessedCustomFee) + assert f.amount == 500000000 + assert f.fee_collector_account_id.num == 98 + + +def test_from_proto_with_automatic_token_associations(transaction_id): + """Test parsing automatic_token_associations from proto.""" + proto = transaction_record_pb2.TransactionRecord() + assoc = proto.automatic_token_associations.add() + assoc.token_id.shardNum = 0 + assoc.token_id.realmNum = 0 + assoc.token_id.tokenNum = 5678 + assoc.account_id.shardNum = 0 + assoc.account_id.realmNum = 0 + assoc.account_id.accountNum = 1234 + + record = TransactionRecord._from_proto(proto, transaction_id) + assert len(record.automatic_token_associations) == 1 + a = record.automatic_token_associations[0] + assert isinstance(a, TokenAssociation) + assert a.token_id.num == 5678 + assert a.account_id.num == 1234 + + +def test_from_proto_with_parent_consensus_timestamp(transaction_id): + """Test parsing parent_consensus_timestamp from proto.""" + proto = transaction_record_pb2.TransactionRecord() + proto.parent_consensus_timestamp.seconds = 9876543210 + proto.parent_consensus_timestamp.nanos = 0 + + record = TransactionRecord._from_proto(proto, transaction_id) + assert record.parent_consensus_timestamp is not None + assert record.parent_consensus_timestamp.seconds == 9876543210 + + +def test_from_proto_with_alias(transaction_id): + """Test parsing alias bytes from proto.""" + proto = transaction_record_pb2.TransactionRecord() + proto.alias = b'\x12\x34\x56\x78' + + record = TransactionRecord._from_proto(proto, transaction_id) + assert record.alias == b'\x12\x34\x56\x78' + + +def test_from_proto_with_ethereum_hash(transaction_id): + """Test parsing ethereum_hash from proto.""" + proto = transaction_record_pb2.TransactionRecord() + proto.ethereum_hash = b'\xab' * 32 + record = TransactionRecord._from_proto(proto, transaction_id) + assert record.ethereum_hash == b'\xab' * 32 + + +def test_from_proto_with_paid_staking_rewards(transaction_id): + """Test parsing paid_staking_rewards from proto.""" + proto = transaction_record_pb2.TransactionRecord() + reward = proto.paid_staking_rewards.add() + reward.accountID.shardNum = 0 + reward.accountID.realmNum = 0 + reward.accountID.accountNum = 456 + reward.amount = 1000000 + + record = TransactionRecord._from_proto(proto, transaction_id) + assert len(record.paid_staking_rewards) == 1 + account, amount = record.paid_staking_rewards[0] + assert account.num == 456 + assert amount == 1000000 + + +def test_from_proto_with_evm_address(transaction_id): + """Test parsing evm_address from proto.""" + proto = transaction_record_pb2.TransactionRecord() + proto.evm_address = b'\x12' * 20 + + record = TransactionRecord._from_proto(proto, transaction_id) + assert record.evm_address == b'\x12' * 20 + + +def test_from_proto_with_contract_create_result(transaction_id): + """Test parsing contract_create_result from proto.""" + proto = transaction_record_pb2.TransactionRecord() + proto.contractCreateResult.contractID.shardNum = 0 + proto.contractCreateResult.contractID.realmNum = 0 + proto.contractCreateResult.contractID.contractNum = 789 + proto.contractCreateResult.contractCallResult = b"Created!" + + record = TransactionRecord._from_proto(proto, transaction_id) + assert record.contract_create_result is not None + assert record.contract_create_result.contract_id.contract == 789 + assert record.contract_create_result.contract_call_result == b"Created!" def test_to_proto(transaction_record, transaction_id): - """Test the to_proto method of the TransactionRecord class""" + """Test the to_proto method of the TransactionRecord class.""" proto = transaction_record._to_proto() assert proto.transactionHash == b'\x01\x02\x03\x04' * 12 @@ -198,7 +384,7 @@ def test_to_proto(transaction_record, transaction_id): assert proto.prng_bytes == b"" def test_proto_conversion(transaction_record): - """Test converting TransactionRecord to proto and back preserves data""" + """Test converting TransactionRecord to proto and back preserves data.""" proto = transaction_record._to_proto() converted = TransactionRecord._from_proto(proto, transaction_record.transaction_id) @@ -208,10 +394,10 @@ def test_proto_conversion(transaction_record): assert converted.transaction_fee == transaction_record.transaction_fee assert converted.receipt.status == transaction_record.receipt.status assert converted.prng_number == transaction_record.prng_number - assert converted.prng_bytes == b"" + assert converted.prng_bytes is None def test_proto_conversion_with_transfers(transaction_id): - """Test proto conversion preserves transfer data""" + """Test proto conversion preserves transfer data.""" record = TransactionRecord() record.transfers = defaultdict(int) record.transfers[AccountId(0, 0, 100)] = -1000 @@ -224,7 +410,7 @@ def test_proto_conversion_with_transfers(transaction_id): assert converted.transfers[AccountId(0, 0, 200)] == 1000 def test_proto_conversion_with_token_transfers(transaction_id): - """Test proto conversion preserves token transfer data""" + """Test proto conversion preserves token transfer data.""" record = TransactionRecord() token_id = TokenId(0, 0, 300) record.token_transfers = defaultdict(lambda: defaultdict(int)) @@ -238,7 +424,7 @@ def test_proto_conversion_with_token_transfers(transaction_id): assert converted.token_transfers[token_id][AccountId(0, 0, 200)] == 500 def test_proto_conversion_with_nft_transfers(transaction_id): - """Test proto conversion preserves NFT transfer data""" + """Test proto conversion preserves NFT transfer data.""" record = TransactionRecord() token_id = TokenId(0, 0, 300) nft_transfer = TokenNftTransfer( @@ -259,10 +445,10 @@ def test_proto_conversion_with_nft_transfers(transaction_id): assert transfer.sender_id == AccountId(0, 0, 100) assert transfer.receiver_id == AccountId(0, 0, 200) assert transfer.serial_number == 1 - assert transfer.is_approved == False + assert not transfer.is_approved def test_proto_conversion_with_new_pending_airdrops(transaction_id): - """Test proto conversion preserves PendingAirdropsRecord""" + """Test proto conversion preserves PendingAirdropsRecord.""" sender = AccountId(0,0,100) receiver = AccountId(0,0,200) token_id = TokenId(0,0,1) @@ -304,7 +490,17 @@ def test_repr_method(transaction_id): "prng_number=None, " "prng_bytes=None, " "duplicates_count=0, " - "children_count=0)" + "children_count=0, " + "consensus_timestamp=None, " + "schedule_ref=None, " + "assessed_custom_fees=[], " + "automatic_token_associations=[], " + "parent_consensus_timestamp=None, " + "alias=None, " + "ethereum_hash=None, " + "paid_staking_rewards=[], " + "evm_address=None, " + "contract_create_result=None)" ) assert repr(record_default) == expected_repr_default @@ -336,40 +532,63 @@ def test_repr_method(transaction_id): f"prng_number=None, " f"prng_bytes=None, " f"duplicates_count=0, " - f"children_count=0)" + f"children_count=0, " + f"consensus_timestamp=None, " + f"schedule_ref=None, " + f"assessed_custom_fees=[], " + f"automatic_token_associations=[], " + f"parent_consensus_timestamp=None, " + f"alias=None, " + f"ethereum_hash=None, " + f"paid_staking_rewards=[], " + f"evm_address=None, " + f"contract_create_result=None)" ) assert repr(record_with_receipt) == expected_repr_with_receipt # Test with all parameters set + ts = Timestamp(1234567890, 0) + sched = ScheduleId(0, 0, 9999) record_full = TransactionRecord( transaction_id=transaction_id, transaction_hash=b'\x01\x02\x03\x04', transaction_memo="Test memo", transaction_fee=100000, receipt=receipt, + consensus_timestamp=ts, + schedule_ref=sched, + assessed_custom_fees=[AssessedCustomFee( + amount=500000000, + fee_collector_account_id=AccountId(0, 0, 98) + )], + automatic_token_associations=[TokenAssociation(token_id=TokenId(0, 0, 5678), account_id=AccountId(0, 0, 1234))], + parent_consensus_timestamp=ts, + alias=b'\x12\x34', + ethereum_hash=b'\xab' * 32, + paid_staking_rewards=[(AccountId(0, 0, 456), 1000000)], + evm_address=b'\x12' * 20, + contract_create_result=ContractFunctionResult(contract_id=ContractId(0, 0, 789)), ) + repr_full = repr(record_full) - assert "duplicates_count=0" in repr_full assert f"transaction_id='{transaction_id}'" in repr_full assert "transaction_hash=b'\\x01\\x02\\x03\\x04'" in repr_full assert "transaction_memo='Test memo'" in repr_full assert "transaction_fee=100000" in repr_full assert "receipt_status='SUCCESS'" in repr_full - expected_repr_full = (f"TransactionRecord(transaction_id='{transaction_id}', " - f"transaction_hash=b'\\x01\\x02\\x03\\x04', " - f"transaction_memo='Test memo', " - f"transaction_fee=100000, " - f"receipt_status='SUCCESS', " - f"token_transfers={{}}, " - f"nft_transfers={{}}, " - f"transfers={{}}, " - f"new_pending_airdrops={[]}, " - f"call_result=None, " - f"prng_number=None, " - f"prng_bytes=None, " - f"duplicates_count=0, " - f"children_count=0)") - assert repr(record_full) == expected_repr_full + assert "consensus_timestamp=" in repr_full + assert f"schedule_ref={sched}" in repr_full + assert "assessed_custom_fees=" in repr_full + assert "automatic_token_associations=" in repr_full + assert "parent_consensus_timestamp=" in repr_full + assert "alias=b'\\x124'" in repr_full + assert "ethereum_hash=" in repr_full + assert "paid_staking_rewards=" in repr_full + assert "evm_address=" in repr_full + assert "contract_create_result=" in repr_full + assert "duplicates_count=0" in repr_full + assert "children_count=0" in repr_full + # Test with transfers record_with_transfers = TransactionRecord( transaction_id=transaction_id, receipt=receipt @@ -393,7 +612,17 @@ def test_repr_method(transaction_id): f"prng_number=None, " f"prng_bytes=None, " f"duplicates_count=0, " - f"children_count=0)") + f"children_count=0, " + f"consensus_timestamp=None, " + f"schedule_ref=None, " + f"assessed_custom_fees=[], " + f"automatic_token_associations=[], " + f"parent_consensus_timestamp=None, " + f"alias=None, " + f"ethereum_hash=None, " + f"paid_staking_rewards=[], " + f"evm_address=None, " + f"contract_create_result=None)") assert repr(record_with_transfers) == expected_repr_with_transfers def test_proto_conversion_with_call_result(transaction_id): @@ -441,7 +670,6 @@ def test_from_proto_without_duplicates_param_backward_compat(transaction_id): proto = transaction_record_pb2.TransactionRecord() proto.memo = "Test" - # Call without duplicates parameter - should not raise record = TransactionRecord._from_proto(proto, transaction_id) assert record.duplicates == [], "Duplicates should default to empty list when omitted" @@ -503,12 +731,11 @@ def test_repr_includes_duplicates_count(transaction_id): record.duplicates = [dup, dup] assert "duplicates_count=2" in repr(record), "duplicates_count should reflect list length" - + def test_from_proto_raises_when_no_transaction_id_available(): """Verify error is raised when neither transaction_id param nor proto.transactionID is present.""" proto = transaction_record_pb2.TransactionRecord() - # Force-clear the field (works in protobuf 3 & 4) proto.ClearField("transactionID") assert not proto.HasField("transactionID"), "Field should be absent after ClearField" @@ -518,7 +745,7 @@ def test_from_proto_raises_when_no_transaction_id_available(): def test_transaction_record_children_not_shared_between_instances(): - """children not shared between two different instances""" + """Children not shared between two different instances.""" r1 = TransactionRecord() r2 = TransactionRecord() @@ -526,3 +753,128 @@ def test_transaction_record_children_not_shared_between_instances(): assert len(r1.children) == 1 assert len(r2.children) == 0 + +def test_round_trip_consensus_timestamp(transaction_id): + """Test consensus timestamp survives protobuf round-trip.""" + ts = Timestamp(1234567890, 500000000) + original = TransactionRecord(consensus_timestamp=ts) + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert round_tripped.consensus_timestamp == ts + + +def test_round_trip_schedule_ref(transaction_id): + """Test schedule reference survives protobuf round-trip.""" + sched = ScheduleId(0, 0, 9999) + original = TransactionRecord(schedule_ref=sched) + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert round_tripped.schedule_ref == sched + + +def test_round_trip_assessed_custom_fees(transaction_id): + """Test custom fees survive protobuf round-trip.""" + fee = AssessedCustomFee( + amount=500000000, + fee_collector_account_id=AccountId(0, 0, 98) + ) + original = TransactionRecord(assessed_custom_fees=[fee]) + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert len(round_tripped.assessed_custom_fees) == 1 + assert round_tripped.assessed_custom_fees[0].amount == 500000000 + + +def test_round_trip_automatic_token_associations(transaction_id): + """Test token associations survive protobuf round-trip.""" + assoc = TokenAssociation( + token_id=TokenId(0, 0, 5678), + account_id=AccountId(0, 0, 1234) + ) + original = TransactionRecord(automatic_token_associations=[assoc]) + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert len(round_tripped.automatic_token_associations) == 1 + assert round_tripped.automatic_token_associations[0].token_id.num == 5678 + + +def test_round_trip_parent_consensus_timestamp(transaction_id): + """Test parent timestamp survives protobuf round-trip.""" + ts = Timestamp(9876543210, 0) + original = TransactionRecord(parent_consensus_timestamp=ts) + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert round_tripped.parent_consensus_timestamp == ts + + +def test_round_trip_alias(transaction_id): + """Test alias bytes survive protobuf round-trip.""" + original = TransactionRecord(alias=b'\x12\x34') + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert round_tripped.alias == b'\x12\x34' + + +def test_round_trip_ethereum_hash(transaction_id): + """Test ethereum hash survives protobuf round-trip.""" + original = TransactionRecord(ethereum_hash=b'\xab' * 32) + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert round_tripped.ethereum_hash == b'\xab' * 32 + + +def test_round_trip_paid_staking_rewards(transaction_id): + """Test staking rewards survive protobuf round-trip.""" + rewards = [(AccountId(0, 0, 456), 1000000)] + original = TransactionRecord(paid_staking_rewards=rewards) + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert round_tripped.paid_staking_rewards == rewards + + +def test_round_trip_evm_address(transaction_id): + """Test EVM address survives protobuf round-trip.""" + original = TransactionRecord(evm_address=b'\x12' * 20) + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert round_tripped.evm_address == b'\x12' * 20 + + +def test_round_trip_contract_create_result(transaction_id): + """Test contract creation result survives round-trip.""" + result = ContractFunctionResult( + contract_id=ContractId(0, 0, 789), + contract_call_result=b"Created!" + ) + original = TransactionRecord(contract_create_result=result) + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert round_tripped.contract_create_result.contract_id == result.contract_id + assert round_tripped.contract_create_result.contract_call_result == b"Created!" + +def test_to_proto_raises_when_both_call_and_create_result_set(transaction_id): + """Setting both call_result and contract_create_result must raise (protobuf oneof).""" + record = TransactionRecord( + transaction_id=transaction_id, + call_result=ContractFunctionResult( + contract_id=ContractId(0, 0, 100), + ), + contract_create_result=ContractFunctionResult( + contract_id=ContractId(0, 0, 200), + ), + ) + + with pytest.raises(ValueError, match="mutually exclusive"): + record._to_proto() + +def test_to_proto_raises_when_both_prng_fields_set(transaction_id): + """Setting both prng_number and prng_bytes must raise (entropy oneof).""" + record = TransactionRecord( + transaction_id=transaction_id, + prng_number=1, + prng_bytes=b"\x01", + ) + + with pytest.raises(ValueError, match="mutually exclusive"): + record._to_proto() + \ No newline at end of file From c9acb5c72ceb48538edd7127b5b8624cbc4c336e Mon Sep 17 00:00:00 2001 From: MontyPokemon <59332150+MonaaEid@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:47:07 +0200 Subject: [PATCH 30/60] feat: add CodeRabbit release gate workflow and prompt for PR audits (#2049) Signed-off-by: MonaaEid Signed-off-by: MontyPokemon <59332150+MonaaEid@users.noreply.github.com> --- .github/coderabbit/release-pr-prompt.md | 34 +++++ .github/scripts/release-pr-coderabbit-gate.js | 143 ++++++++++++++++++ .../workflows/release-pr-coderabbit-gate.yml | 42 +++++ CHANGELOG.md | 1 + 4 files changed, 220 insertions(+) create mode 100644 .github/coderabbit/release-pr-prompt.md create mode 100644 .github/scripts/release-pr-coderabbit-gate.js create mode 100644 .github/workflows/release-pr-coderabbit-gate.yml diff --git a/.github/coderabbit/release-pr-prompt.md b/.github/coderabbit/release-pr-prompt.md new file mode 100644 index 000000000..65b478e91 --- /dev/null +++ b/.github/coderabbit/release-pr-prompt.md @@ -0,0 +1,34 @@ +## 🕵️ Release-Gate Audit: Breaking Changes & Architecture + +You are performing a **Senior Release Engineer** audit on this diff. Your goal is to identify risks that could impact downstream users or system stability. + +### 🚨 Critical Instructions +- **Ignore** linting, variable naming, or stylistic preferences. +- **Focus** on public API surface, data schemas, and logic flow. +- **Be Concise**: Use bullet points. If no risks are found, state "No breaking changes identified." + +--- + +### 1. 🛠️ Public API & Contract Changes +Identify any modifications to the public-facing interface. +- **Removals/Renames**: Are any classes, methods, or variables renamed or removed? +- **Signature Changes**: Have parameter types or return types changed in a way that breaks existing calls? +- **Defaults**: Have default values for arguments changed? + +### 2. 🏗️ Architectural Integrity +- **Dependency Changes**: Highlight any new external libraries or significant version bumps. +- **Side Effects**: Are there new global states, singletons, or changes to how the application initializes? +- **Resource Usage**: Does this diff introduce patterns that might impact memory or CPU (e.g., new loops, heavy recursive calls)? + +### 3. 📉 Backward Compatibility & Data +- **Persistence**: If applicable, do database schema changes or file format changes require a migration? +- **Protocol**: If this communicates via API/WebSockets, is the payload structure still compatible with the previous version? + +### 4. 🧪 Testing & Validation +- **Coverage Gap**: Are there major logic additions that lack corresponding test files in the diff? +- **Edge Cases**: Identify at least one "what-if" scenario that might cause a failure (e.g., "What if the network is down during this new initialization step?"). + +--- + +### 🏁 Summary Verdict +Provide a 1-sentence summary of the "Safety" of this release candidate. \ No newline at end of file diff --git a/.github/scripts/release-pr-coderabbit-gate.js b/.github/scripts/release-pr-coderabbit-gate.js new file mode 100644 index 000000000..72fe00ca6 --- /dev/null +++ b/.github/scripts/release-pr-coderabbit-gate.js @@ -0,0 +1,143 @@ +/** + * Posts a single "@coderabbit review" comment on release PRs, embedding the + * release review prompt. Designed to run with: + * - permissions: contents: read, pull-requests: write + * + * Safety: + * - Only runs for maintainer-authored PRs (MEMBER/OWNER) + * - Dedupe via hidden marker comment + */ + +const fs = require("fs"); +const path = require("path"); + +const MARKER = ""; + + +function loadPrompt() { + const promptPath = path.join( + process.env.GITHUB_WORKSPACE || ".", + ".github/coderabbit/release-pr-prompt.md" + ); + try { + const content = fs.readFileSync(promptPath, "utf8").trim(); + if (!content) { + throw new Error("Release prompt file is empty"); + } + return content; + } catch (error) { + throw new Error(`Failed to load release prompt from ${promptPath}: ${error.message}`); + } +} + + +async function commentAlreadyExists({ github, owner, repo, issue_number }) { + try { + const data = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number, + per_page: 100, + }); + data.forEach(c => { + if (c.body && c.body.includes(MARKER)) { + console.log(`FOUND MATCH in comment by ${c.user.login}: ${c.html_url}`); + } + }); + return data.some((c) => typeof c.body === "string" && c.body.includes(MARKER)); + } + catch (error) { + console.error(`Error checking for existing comments: ${error.message}`); + return true; // Fail closed to avoid duplicates if we can't verify + } +} + + +function buildBody({ prompt, headRef }) { + return [ + "@coderabbit review", + "", + MARKER, + "", + "## 🚀 Release Gate: Cumulative Audit", + "> **Notice to Reviewer**: This is a checkpoint PR for a new release. While the diff here primarily updates versioning and the Changelog, your audit must cover the **entire cumulative scope** of changes since the last version tag.", + "", + "### 🎯 Audit Objectives", + "- Analyze all features merged into `main` since the previous release.", + "- Identify breaking changes in the public API or protocol.", + "- Verify architectural integrity across the Hiero SDK Python modules.", + "", + "
", + "View Senior Audit Constraints", + "", + prompt, + "", + "
", + "", + "---", + `*Auditing Branch: \`${headRef}\` against the project history.*`, + ].join("\n"); +} + +function getSkipReason(pr) { + if (!pr) { + return "No pull_request payload; exiting."; + } + + if (!["MEMBER", "OWNER"].includes(pr.author_association)) { + return `author_association=${pr.author_association}; skipping.`; + } + + const title = (pr.title || "").toLowerCase(); + const isReleaseTitle = + title.startsWith("chore: release v") || title.startsWith("release v"); + + if (!isReleaseTitle) { + return "Not a release PR title; skipping."; + } + + return null; +} + +module.exports = async ({ github, context }) => { + let issue_number = "unknown"; + let headRef = "?"; + let baseRef = "?"; + + try { + const owner = context.repo.owner; + const repo = context.repo.repo; + const pr = context.payload.pull_request; + + const skipReason = getSkipReason(pr); + if (skipReason) { + console.log(skipReason); + return; + } + + issue_number = pr.number; + if (await commentAlreadyExists({ github, owner, repo, issue_number })) { + console.log("Release gate comment already exists. Skipping."); + return; + } + + headRef = pr.head?.ref || "unknown"; + baseRef = pr.base?.ref || "unknown"; + + const prompt = loadPrompt(); + const body = buildBody({ prompt, headRef }); + + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body, + }); + + console.log("Posted CodeRabbit release-gate comment."); + console.log(`PR #${issue_number} (${headRef} → ${baseRef})`); + } catch (error) { + console.error(`Error in release PR coderabbit gate: ${error.message}`); + console.log(`PR #${issue_number || 'unknown'} (${headRef || '?'} → ${baseRef || '?'})`); + } +}; \ No newline at end of file diff --git a/.github/workflows/release-pr-coderabbit-gate.yml b/.github/workflows/release-pr-coderabbit-gate.yml new file mode 100644 index 000000000..eb096a0d2 --- /dev/null +++ b/.github/workflows/release-pr-coderabbit-gate.yml @@ -0,0 +1,42 @@ +name: CodeRabbit Release Gate Comment + +on: + pull_request: + types: [opened, reopened, synchronize, edited] + +permissions: + contents: read + pull-requests: write + +concurrency: + group: coderabbit-release-gate-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + coderabbit-release-gate: + runs-on: ubuntu-latest + # Only run for release PRs /title check as initial filter + if: | + github.event.pull_request && + (github.event.pull_request.author_association == 'MEMBER' || + github.event.pull_request.author_association == 'OWNER') && + (startsWith(github.event.pull_request.title, 'chore: release v') || + startsWith(github.event.pull_request.title, 'release v')) + + steps: + - name: Harden the runner + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1 + + - name: Post CodeRabbit release-gate prompt comment + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 + with: + script: | + const script = require('./.github/scripts/release-pr-coderabbit-gate.js'); + await script({ github, context}); \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f3170ed5a..df1cc92ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Refactored the Advanced Issue Template to ensure PR-level quality checklists do not block maintainers during issue creation (#2036) - Add automated label sync workflow to propagate labels from linked issues to pull requests (#1716) - chore: update spam list (#2035) +- Add CodeRabbit release gate workflow and prompt for PR audits `release-pr-prompt.md`, `release-pr-coderabbit-gate.yml` and `release-pr-coderabbit-gate.js` ## [0.2.3] - 2026-03-26 From bb49a03d4a17c763754dcab6179e28bcbfc4379e Mon Sep 17 00:00:00 2001 From: exploreriii <133720349+exploreriii@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:25:21 +0100 Subject: [PATCH 31/60] chore: add concurrency guard (#2072) Signed-off-by: exploreriii <133720349+exploreriii@users.noreply.github.com> Co-authored-by: AntonioCeppellini <128388022+AntonioCeppellini@users.noreply.github.com> --- ...=> pr-check-secondary-unit-integration-test.yml} | 13 +++++++++---- CHANGELOG.md | 1 + 2 files changed, 10 insertions(+), 4 deletions(-) rename .github/workflows/{pr-check-test.yml => pr-check-secondary-unit-integration-test.yml} (94%) diff --git a/.github/workflows/pr-check-test.yml b/.github/workflows/pr-check-secondary-unit-integration-test.yml similarity index 94% rename from .github/workflows/pr-check-test.yml rename to .github/workflows/pr-check-secondary-unit-integration-test.yml index 0f4191f04..8a2dc1dc5 100644 --- a/.github/workflows/pr-check-test.yml +++ b/.github/workflows/pr-check-secondary-unit-integration-test.yml @@ -1,5 +1,4 @@ -name: Hiero Solo Integration & Unit Tests - +name: Secondary PR Check - Hiero Solo Integration & Unit Tests on: push: branches: @@ -9,21 +8,27 @@ on: - "docs/**" - "examples/**" - "tck/**" + - "tests/fuzz/**" - ".github/**" - - "!.github/workflows/pr-check-test.yml" + - "!.github/workflows/pr-check-secondary-unit-integration-test.yml" pull_request: paths-ignore: - "**/*.md" - "docs/**" - "examples/**" - "tck/**" + - "tests/fuzz/**" - ".github/**" - - "!.github/workflows/pr-check-test.yml" + - "!.github/workflows/pr-check-secondary-unit-integration-test.yml" workflow_dispatch: {} permissions: contents: read +concurrency: + group: pr-check-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: unit-tests: name: Unit Tests (Python ${{ matrix.python-version }}) diff --git a/CHANGELOG.md b/CHANGELOG.md index df1cc92ea..5de531849 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - chore: update GitHub Actions runners from ubuntu-latest to hl-sdk-py-lin-md (#2021) - Refactored the Advanced Issue Template to V2 with stricter prerequisites and a focus on architectural design (#2016). - Refactored the Advanced Issue Template to ensure PR-level quality checklists do not block maintainers during issue creation (#2036) +- chore: add concurrency to unit and integration tests (#2071) - Add automated label sync workflow to propagate labels from linked issues to pull requests (#1716) - chore: update spam list (#2035) - Add CodeRabbit release gate workflow and prompt for PR audits `release-pr-prompt.md`, `release-pr-coderabbit-gate.yml` and `release-pr-coderabbit-gate.js` From 8c346acc21c80d72a87d99f9dcb6e99451280803 Mon Sep 17 00:00:00 2001 From: exploreriii <133720349+exploreriii@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:44:02 +0100 Subject: [PATCH 32/60] ci: add codeql workflow (#2085) Signed-off-by: exploreriii <133720349+exploreriii@users.noreply.github.com> --- .github/workflows/codeql.yml | 68 ++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 1 + 2 files changed, 69 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..ac152026d --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,68 @@ +name: "CodeQL Advanced" + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + paths-ignore: + - "**/*.md" + schedule: + - cron: "28 23 * * *" # Runs every day at 23:28 UTC. + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: hl-sdk-py-lin-md + permissions: + contents: read + # required for all workflows + security-events: write + # required to fetch internal or private CodeQL packs + packages: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: python + build-mode: none + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + with: + egress-policy: audit + + - name: Initialize CodeQL + uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.3.5 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + queries: security-extended + dependency-caching: true + + - name: Set up Python + if: matrix.language == 'python' + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.13" + + - name: Install uv + if: matrix.language == 'python' + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + with: + enable-cache: true + + - name: Install dependencies + if: matrix.language == 'python' + run: uv sync --all-extras --dev + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.3.5 + with: + category: "/language:${{matrix.language}}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 5de531849..3ff775521 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - chore: update GitHub Actions runners from ubuntu-latest to hl-sdk-py-lin-md (#2021) - Refactored the Advanced Issue Template to V2 with stricter prerequisites and a focus on architectural design (#2016). - Refactored the Advanced Issue Template to ensure PR-level quality checklists do not block maintainers during issue creation (#2036) +- Added CodeQL workflow (#2084) - chore: add concurrency to unit and integration tests (#2071) - Add automated label sync workflow to propagate labels from linked issues to pull requests (#1716) - chore: update spam list (#2035) From 7571b15de149f8d0de0ce7362577530b3bfb1f75 Mon Sep 17 00:00:00 2001 From: Mohit Sharma Date: Thu, 9 Apr 2026 09:53:46 +0530 Subject: [PATCH 33/60] Add discussion label check and test to bot inactivity script (#2023) Signed-off-by: oGranny Signed-off-by: Mohit Sharma --- .github/scripts/bot-inactivity-unassign.sh | 7 + .../scripts/test-bot-inactivity-unassign.sh | 641 ++++++++++++++++++ CHANGELOG.md | 1 + 3 files changed, 649 insertions(+) create mode 100644 .github/scripts/test-bot-inactivity-unassign.sh diff --git a/.github/scripts/bot-inactivity-unassign.sh b/.github/scripts/bot-inactivity-unassign.sh index 2726c8a25..d10d72b48 100755 --- a/.github/scripts/bot-inactivity-unassign.sh +++ b/.github/scripts/bot-inactivity-unassign.sh @@ -212,6 +212,13 @@ EOF continue fi + PR_LABELS=$(gh pr view "$PR_NUM" --repo "$REPO" --json labels --jq '.labels[].name' 2>/dev/null || true) + + if echo "$PR_LABELS" | grep -qi '^discussion$'; then + echo " [SKIP] PR #$PR_NUM has 'discussion' label — skipping close & unassign" + continue + fi + COMMITS_JSON=$(gh api "repos/$REPO/pulls/$PR_NUM/commits" --paginate 2>/dev/null || echo "[]") diff --git a/.github/scripts/test-bot-inactivity-unassign.sh b/.github/scripts/test-bot-inactivity-unassign.sh new file mode 100644 index 000000000..e29ffe5fb --- /dev/null +++ b/.github/scripts/test-bot-inactivity-unassign.sh @@ -0,0 +1,641 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Test suite for bot-inactivity-unassign.sh +# Tests the discussion label functionality and other key behaviors + +TEST_REPO="test-org/test-repo" +TEMP_DIR="" +BOT_SCRIPT=".github/scripts/bot-inactivity-unassign.sh" + +# Color codes for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# Test counters +TESTS_RUN=0 +TESTS_PASSED=0 +TESTS_FAILED=0 + +setup() { + TEMP_DIR=$(mktemp -d) + export PATH="$TEMP_DIR/mocks:$PATH" + mkdir -p "$TEMP_DIR/mocks" + + # Create mock gh command directory + export GH_MOCK_DIR="$TEMP_DIR/gh_mock_data" + mkdir -p "$GH_MOCK_DIR" + + echo "Test environment created at: $TEMP_DIR" +} + +cleanup() { + if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then + rm -rf "$TEMP_DIR" + fi +} + +print_result() { + local test_name="$1" + local result="$2" + local message="${3:-}" + + TESTS_RUN=$((TESTS_RUN + 1)) + + if [[ "$result" == "PASS" ]]; then + TESTS_PASSED=$((TESTS_PASSED + 1)) + echo -e "${GREEN}✓ PASS${NC}: $test_name" + else + TESTS_FAILED=$((TESTS_FAILED + 1)) + echo -e "${RED}✗ FAIL${NC}: $test_name" + if [[ -n "$message" ]]; then + echo -e " ${YELLOW}→${NC} $message" + fi + fi +} + +# Create mock gh command +create_gh_mock() { + cat > "$TEMP_DIR/mocks/gh" << 'MOCK_END' +#!/usr/bin/env bash +set -euo pipefail +# Mock gh CLI for testing + +GH_MOCK_DIR="${GH_MOCK_DIR:-/tmp/gh_mock_data}" + +# Parse command +if [[ "$1" == "api" ]]; then + # Handle API calls + # Accept production flags like --paginate and -H, but treat fixtures as single-page responses. + endpoint="" + for arg in "$@"; do + if [[ "$arg" =~ ^repos/ ]] || [[ "$arg" =~ ^issues/ ]]; then + endpoint="$arg" + break + fi + done + + jq_filter="" + if [[ "$*" == *"--jq"* ]]; then + args=("$@") + for i in "${!args[@]}"; do + if [[ "${args[i]}" == "--jq" ]]; then + next=$((i + 1)) + if (( next < ${#args[@]} )); then + jq_filter="${args[next]}" + fi + break + elif [[ "${args[i]}" == --jq=* ]]; then + jq_filter="${args[i]#--jq=}" + break + fi + done + fi + + # Return mock data based on endpoint + if [[ -f "$GH_MOCK_DIR/${endpoint//\//_}.json" ]]; then + if [[ -n "$jq_filter" ]]; then + cat "$GH_MOCK_DIR/${endpoint//\//_}.json" | jq -r "$jq_filter" 2>/dev/null || echo "" + else + cat "$GH_MOCK_DIR/${endpoint//\//_}.json" + fi + else + if [[ -n "$jq_filter" ]]; then + echo "[]" | jq -r "$jq_filter" 2>/dev/null || echo "" + else + echo "[]" + fi + fi + +elif [[ "$1" == "pr" && "$2" == "view" ]]; then + # Handle pr view command + pr_num="$3" + + # Check if asking for state + if [[ "$*" == *"--json state"* ]]; then + if [[ "$*" == *"--jq"* ]]; then + jq_filter="" + args=("$@") + for i in "${!args[@]}"; do + if [[ "${args[i]}" == "--jq" ]]; then + next=$((i + 1)) + if (( next < ${#args[@]} )); then + jq_filter="${args[next]}" + fi + break + elif [[ "${args[i]}" == --jq=* ]]; then + jq_filter="${args[i]#--jq=}" + break + fi + done + + if [[ -f "$GH_MOCK_DIR/pr_${pr_num}_state.json" ]]; then + if [[ -n "$jq_filter" ]]; then + cat "$GH_MOCK_DIR/pr_${pr_num}_state.json" | jq -r "$jq_filter" 2>/dev/null || echo "" + else + cat "$GH_MOCK_DIR/pr_${pr_num}_state.json" + fi + else + echo '{"state":"OPEN"}' + fi + else + if [[ -f "$GH_MOCK_DIR/pr_${pr_num}_state.json" ]]; then + cat "$GH_MOCK_DIR/pr_${pr_num}_state.json" + else + echo '{"state":"OPEN"}' + fi + fi + # Check if asking for labels + elif [[ "$*" == *"--json labels"* ]]; then + # Check if jq filter is also requested + if [[ "$*" == *"--jq"* ]]; then + # Extract the caller-provided --jq filter and apply it + jq_filter="" + args=("$@") + for i in "${!args[@]}"; do + if [[ "${args[i]}" == "--jq" ]]; then + next=$((i + 1)) + if (( next < ${#args[@]} )); then + jq_filter="${args[next]}" + fi + break + elif [[ "${args[i]}" == --jq=* ]]; then + jq_filter="${args[i]#--jq=}" + break + fi + done + + if [[ -f "$GH_MOCK_DIR/pr_${pr_num}_labels.json" ]]; then + if [[ -n "$jq_filter" ]]; then + cat "$GH_MOCK_DIR/pr_${pr_num}_labels.json" | jq -r "$jq_filter" 2>/dev/null || echo "" + else + cat "$GH_MOCK_DIR/pr_${pr_num}_labels.json" + fi + else + echo "" + fi + else + # Just return the JSON + if [[ -f "$GH_MOCK_DIR/pr_${pr_num}_labels.json" ]]; then + cat "$GH_MOCK_DIR/pr_${pr_num}_labels.json" + else + echo '{"labels":[]}' + fi + fi + fi + +elif [[ "$1" == "pr" && "$2" == "comment" ]]; then + # Mock PR comment - just succeed + echo "Comment added to PR" + +elif [[ "$1" == "pr" && "$2" == "close" ]]; then + # Mock PR close - record that it was called + pr_num="$3" + echo "CLOSED_PR_$pr_num" >> "$GH_MOCK_DIR/actions.log" + echo "PR closed" + +elif [[ "$1" == "issue" && "$2" == "comment" ]]; then + # Mock issue comment + echo "Comment added to issue" + +elif [[ "$1" == "issue" && "$2" == "edit" ]]; then + # Mock issue edit - record unassignment + if [[ "$*" == *"--remove-assignee"* ]]; then + # Copy arguments into an array for indexed access + args=("$@") + # Determine the issue number (first purely numeric argument) + issue_num="" + for arg in "${args[@]}"; do + if [[ "$arg" =~ ^[0-9]+$ ]]; then + issue_num="$arg" + break + fi + done + # Find the --remove-assignee flag and its following username + user="" + for i in "${!args[@]}"; do + if [[ "${args[i]}" == "--remove-assignee" ]]; then + next=$((i + 1)) + if (( next < ${#args[@]} )); then + user="${args[next]}" + echo "UNASSIGNED_${user}_FROM_${issue_num}" >> "$GH_MOCK_DIR/actions.log" + fi + break + fi + done + fi + echo "Issue edited" + +elif [[ "$1" == "auth" && "$2" == "status" ]]; then + # Mock auth check - always succeed + exit 0 +fi +MOCK_END + + chmod +x "$TEMP_DIR/mocks/gh" + + if ! command -v jq >/dev/null 2>&1; then + echo "WARNING: jq not found, some tests may fail" + fi +} + +# Setup full issue + timeline + PR data for executing the actual bot script +setup_issue_with_linked_pr() { + local issue_num="$1" + local pr_num="$2" + local assignee="$3" + + cat > "$GH_MOCK_DIR/repos_${TEST_REPO//\//_}_issues.json" << EOF +[ + { + "number": $issue_num, + "state": "open", + "assignees": [{"login": "$assignee"}] + } +] +EOF + + cat > "$GH_MOCK_DIR/repos_${TEST_REPO//\//_}_issues_${issue_num}.json" << EOF +{ + "number": $issue_num, + "state": "open", + "created_at": "2026-01-01T00:00:00Z", + "assignees": [{"login": "$assignee"}] +} +EOF + + cat > "$GH_MOCK_DIR/repos_${TEST_REPO//\//_}_issues_${issue_num}_timeline.json" << EOF +[ + { + "event": "assigned", + "created_at": "2026-01-02T00:00:00Z", + "assignee": {"login": "$assignee"} + }, + { + "event": "cross-referenced", + "source": { + "issue": { + "number": $pr_num, + "pull_request": {"url": "https://api.github.com/repos/$TEST_REPO/pulls/$pr_num"}, + "repository": {"full_name": "$TEST_REPO"} + } + } + } +] +EOF +} + +setup_labeled_pr_with_linked_issue() { + local issue_num="$1" + local pr_num="$2" + local assignee="$3" + local label_name="$4" + + setup_issue_with_linked_pr "$issue_num" "$pr_num" "$assignee" + + echo '{"state":"OPEN"}' > "$GH_MOCK_DIR/pr_${pr_num}_state.json" + cat > "$GH_MOCK_DIR/pr_${pr_num}_labels.json" << EOF +{"labels":[{"name":"$label_name"}]} +EOF + + local old_date + old_date=$(date -u -v-25d +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -d "25 days ago" +"%Y-%m-%dT%H:%M:%SZ") + cat > "$GH_MOCK_DIR/repos_${TEST_REPO//\//_}_pulls_${pr_num}_commits.json" << EOF +[{"commit":{"committer":{"date":"$old_date"}}}] +EOF +} + +# Setup mock data for a PR with discussion label +setup_pr_with_discussion_label() { + local pr_num="$1" + + echo '{"state":"OPEN"}' > "$GH_MOCK_DIR/pr_${pr_num}_state.json" + + cat > "$GH_MOCK_DIR/pr_${pr_num}_labels.json" << 'EOF' +{"labels":[{"name":"discussion"},{"name":"enhancement"}]} +EOF + + # Mock stale commits (21+ days old) + local old_date + old_date=$(date -u -v-25d +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -d "25 days ago" +"%Y-%m-%dT%H:%M:%SZ") + cat > "$GH_MOCK_DIR/repos_${TEST_REPO//\//_}_pulls_${pr_num}_commits.json" << EOF +[{"commit":{"committer":{"date":"$old_date"}}}] +EOF +} + +# Setup mock data for a stale PR without discussion label +setup_stale_pr_without_discussion() { + local pr_num="$1" + + echo '{"state":"OPEN"}' > "$GH_MOCK_DIR/pr_${pr_num}_state.json" + + # PR has no discussion label + echo '{"labels":[{"name":"bug"}]}' > "$GH_MOCK_DIR/pr_${pr_num}_labels.json" + + # Mock stale commits + local old_date + old_date=$(date -u -v-25d +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -d "25 days ago" +"%Y-%m-%dT%H:%M:%SZ") + cat > "$GH_MOCK_DIR/repos_${TEST_REPO//\//_}_pulls_${pr_num}_commits.json" << EOF +[{"commit":{"committer":{"date":"$old_date"}}}] +EOF +} + +# Setup mock data for an active PR +setup_active_pr() { + local pr_num="$1" + + echo '{"state":"OPEN"}' > "$GH_MOCK_DIR/pr_${pr_num}_state.json" + + # PR has no discussion label + echo '{"labels":[{"name":"feature"}]}' > "$GH_MOCK_DIR/pr_${pr_num}_labels.json" + + # Mock recent commits + local recent_date + recent_date=$(date -u -v-5d +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -d "5 days ago" +"%Y-%m-%dT%H:%M:%SZ") + cat > "$GH_MOCK_DIR/repos_${TEST_REPO//\//_}_pulls_${pr_num}_commits.json" << EOF +[{"commit":{"committer":{"date":"$recent_date"}}}] +EOF +} + +# Setup mock data for a closed PR +setup_closed_pr() { + local pr_num="$1" + + # PR is closed + echo '{"state":"CLOSED"}' > "$GH_MOCK_DIR/pr_${pr_num}_state.json" + + # Labels don't matter for closed PR + echo '{"labels":[]}' > "$GH_MOCK_DIR/pr_${pr_num}_labels.json" +} + +# Test 1: PR with discussion label should NOT be closed +test_pr_with_discussion_label_not_closed() { + echo "" + echo "Test 1: Skip stale PR with 'discussion' label" + echo "==================================================================" + + local issue_num="1000" + local pr_num="100" + local assignee="alice" + + setup_issue_with_linked_pr "$issue_num" "$pr_num" "$assignee" + setup_pr_with_discussion_label "$pr_num" + + local output + if output=$(REPO="$TEST_REPO" DAYS=21 DRY_RUN=0 bash "$BOT_SCRIPT" 2>&1); then + print_result "script executed with mocked data" "PASS" + else + print_result "script executed with mocked data" "FAIL" "Bot exited non-zero: $output" + fi + if grep -Eq "\\[SKIP\\].*discussion" <<<"$output"; then + print_result "discussion-label skip log emitted" "PASS" + else + print_result "discussion-label skip log emitted" "FAIL" "Expected discussion-label skip log" + fi + if [[ ! -f "$GH_MOCK_DIR/actions.log" ]] || ! grep -q "CLOSED_PR_$pr_num" "$GH_MOCK_DIR/actions.log" 2>/dev/null; then + print_result "script did NOT close PR with discussion label" "PASS" + else + print_result "script did NOT close PR with discussion label" "FAIL" "PR was incorrectly closed" + fi + if [[ ! -f "$GH_MOCK_DIR/actions.log" ]] || ! grep -q "UNASSIGNED_${assignee}_FROM_${issue_num}" "$GH_MOCK_DIR/actions.log" 2>/dev/null; then + print_result "script did NOT unassign issue for discussion PR" "PASS" + else + print_result "script did NOT unassign issue for discussion PR" "FAIL" "Assignee was incorrectly removed" + fi +} + +# Test 2: Stale PR without discussion label should be closed +test_stale_pr_without_discussion_closed() { + echo "" + echo "Test 2: Stale PR without 'discussion' label should be closed" + echo "==============================================================" + + local issue_num="2000" + local pr_num="200" + local assignee="alice" + + setup_issue_with_linked_pr "$issue_num" "$pr_num" "$assignee" + setup_stale_pr_without_discussion "$pr_num" + + local HAS_DISCUSSION_LABEL + HAS_DISCUSSION_LABEL=$(gh pr view "$pr_num" --repo "$TEST_REPO" --json labels --jq '.labels[].name' 2>/dev/null | grep -i '^discussion$' || echo "") + + if [[ -z "$HAS_DISCUSSION_LABEL" ]]; then + print_result "PR without discussion label detected" "PASS" + else + print_result "PR without discussion label detected" "FAIL" "Discussion label incorrectly detected" + fi + + local output + if output=$(REPO="$TEST_REPO" DAYS=21 DRY_RUN=0 bash "$BOT_SCRIPT" 2>&1); then + print_result "script executed for stale PR without discussion label" "PASS" + else + print_result "script executed for stale PR without discussion label" "FAIL" "Bot exited non-zero: $output" + fi + + if [[ -f "$GH_MOCK_DIR/actions.log" ]] && grep -q "CLOSED_PR_$pr_num" "$GH_MOCK_DIR/actions.log" 2>/dev/null; then + print_result "script closed stale PR without discussion label" "PASS" + else + print_result "script closed stale PR without discussion label" "FAIL" "Expected CLOSED_PR_$pr_num in actions.log" + fi + + if [[ -f "$GH_MOCK_DIR/actions.log" ]] && grep -q "UNASSIGNED_${assignee}_FROM_${issue_num}" "$GH_MOCK_DIR/actions.log" 2>/dev/null; then + print_result "script unassigned user for stale PR without discussion label" "PASS" + else + print_result "script unassigned user for stale PR without discussion label" "FAIL" "Expected UNASSIGNED_${assignee}_FROM_${issue_num} in actions.log" + fi +} + +# Test 3: Verify label check uses jq filter correctly +test_jq_filter_correctness() { + echo "" + echo "Test 3: Mock correctly executes jq filters" + echo "==================================================================" + + setup_pr_with_discussion_label "300" + local result + result=$(gh pr view "300" --repo "$TEST_REPO" --json labels --jq '.labels[].name' 2>/dev/null || echo "") + + if echo "$result" | grep -q "discussion"; then + print_result "Mock executes .labels[].name filter" "PASS" + else + print_result "Mock executes .labels[].name filter" "FAIL" "Expected 'discussion' in output, got '$result'" + fi + + # Test with no discussion label using same filter + setup_stale_pr_without_discussion "301" + result=$(gh pr view "301" --repo "$TEST_REPO" --json labels --jq '.labels[].name' 2>/dev/null || echo "") + + if ! echo "$result" | grep -qi "^discussion$"; then + print_result "Mock handles .labels[].name without discussion" "PASS" + else + print_result "Mock handles .labels[].name without discussion" "FAIL" "Unexpected discussion label in '$result'" + fi +} + +# Test 4: Closed PRs should be skipped +test_closed_pr_skipped() { + echo "" + echo "Test 4: Closed PRs should be skipped" + echo "======================================" + + setup_closed_pr "400" + + local pr_num="400" + local pr_state + pr_state=$(gh pr view "$pr_num" --repo "$TEST_REPO" --json state --jq '.state' 2>/dev/null || echo "") + + if [[ "$pr_state" != "OPEN" ]]; then + print_result "Closed PR correctly identified" "PASS" + else + print_result "Closed PR correctly identified" "FAIL" "PR state is '$pr_state'" + fi +} + +# Test 5: Active PR should not be closed +test_active_pr_not_closed() { + echo "" + echo "Test 5: Active PR (recent commits) should not be closed" + echo "=========================================================" + + setup_active_pr "500" + + local pr_num="500" + local COMMITS_JSON + COMMITS_JSON=$(gh api "repos/$TEST_REPO/pulls/$pr_num/commits" --paginate 2>/dev/null || echo "[]") + + if echo "$COMMITS_JSON" | jq -e 'length > 0' >/dev/null 2>&1; then + print_result "Active PR has commit data" "PASS" + + local last_commit_date + last_commit_date=$(echo "$COMMITS_JSON" | jq -r 'last | .commit.committer.date // empty') + if [[ -n "$last_commit_date" ]]; then + print_result "Active PR commit timestamp retrieved" "PASS" + else + print_result "Active PR commit timestamp retrieved" "FAIL" "No timestamp found" + fi + else + print_result "Active PR has commit data" "FAIL" "No commits found" + fi +} + +# Test 6: Verify mock handles varying jq expressions +test_log_output() { + echo "" + echo "Test 6: Mock handles varying jq filter expressions" + echo "===================================================" + + setup_pr_with_discussion_label "600" + + local pr_num="600" + local output + output=$(gh pr view "$pr_num" --repo "$TEST_REPO" --json labels --jq '[.labels[] | select(.name == "discussion") | .name]' 2>/dev/null || echo "[]") + + if echo "$output" | grep -q "discussion"; then + print_result "Mock executes complex jq with select and map" "PASS" + else + print_result "Mock executes complex jq with select and map" "FAIL" "Expected 'discussion' in output" + fi +} + +# Test 7: Multiple labels including discussion +test_multiple_labels_with_discussion() { + echo "" + echo "Test 7: PR with multiple labels including 'discussion'" + echo "========================================================" + + # Create PR with multiple labels including discussion + echo '{"state":"OPEN"}' > "$GH_MOCK_DIR/pr_700_state.json" + cat > "$GH_MOCK_DIR/pr_700_labels.json" << 'EOF' +{"labels":[{"name":"bug"},{"name":"discussion"},{"name":"CICD"}]} +EOF + + local pr_num="700" + local label_count + label_count=$(gh pr view "$pr_num" --repo "$TEST_REPO" --json labels --jq '.labels | length' 2>/dev/null || echo "0") + + if [[ "$label_count" == "3" ]]; then + print_result "Mock executes .labels | length filter" "PASS" + else + print_result "Mock executes .labels | length filter" "FAIL" "Expected 3 labels, got '$label_count'" + fi +} + +# Test 8: Case-insensitive discussion label handling +test_case_insensitivity() { + echo "" + echo "Test 8: Label matching is case-insensitive" + echo "===========================================" + + local issue_num="8000" + local pr_num="800" + local assignee="bob" + + setup_labeled_pr_with_linked_issue "$issue_num" "$pr_num" "$assignee" "Discussion" + + local output + if output=$(REPO="$TEST_REPO" DAYS=21 DRY_RUN=0 bash "$BOT_SCRIPT" 2>&1); then + print_result "script executed for case-insensitive label" "PASS" + else + print_result "script executed for case-insensitive label" "FAIL" "Bot exited non-zero: $output" + fi + + if grep -Eq "\\[SKIP\\].*discussion" <<<"$output"; then + print_result "case-insensitive discussion skip log emitted" "PASS" + else + print_result "case-insensitive discussion skip log emitted" "FAIL" "Expected discussion-label skip log" + fi + + if [[ ! -f "$GH_MOCK_DIR/actions.log" ]] || ! grep -q "CLOSED_PR_$pr_num" "$GH_MOCK_DIR/actions.log" 2>/dev/null; then + print_result "bot did NOT close PR with 'Discussion' label" "PASS" + else + print_result "bot did NOT close PR with 'Discussion' label" "FAIL" "PR was incorrectly closed" + fi + + if [[ ! -f "$GH_MOCK_DIR/actions.log" ]] || ! grep -q "UNASSIGNED_${assignee}_FROM_${issue_num}" "$GH_MOCK_DIR/actions.log" 2>/dev/null; then + print_result "bot did NOT unassign issue for 'Discussion' label" "PASS" + else + print_result "bot did NOT unassign issue for 'Discussion' label" "FAIL" "Assignee was incorrectly removed" + fi +} + +main() { + echo "==============================================" + echo " Bot Inactivity Unassign - Test Suite" + echo "==============================================" + echo "" + + setup + trap cleanup EXIT + + create_gh_mock + + rm -f "$GH_MOCK_DIR/actions.log" + + test_pr_with_discussion_label_not_closed + test_stale_pr_without_discussion_closed + test_jq_filter_correctness + test_closed_pr_skipped + test_active_pr_not_closed + test_log_output + test_multiple_labels_with_discussion + test_case_insensitivity + + echo "" + echo "==============================================" + echo " Test Summary" + echo "==============================================" + echo "Total tests run: $TESTS_RUN" + echo -e "${GREEN}Tests passed: $TESTS_PASSED${NC}" + if [[ $TESTS_FAILED -gt 0 ]]; then + echo -e "${RED}Tests failed: $TESTS_FAILED${NC}" + exit 1 + else + echo -e "${GREEN}All tests passed!${NC}" + exit 0 + fi +} + +main "$@" \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ff775521..82ff18cbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ### .github +- chore: Added check for the discussion label in the inactivity bot (#1583) - chore: pin pip packages to exact versions in publish.yml to improve supply chain security and reproducibility (#2056) - chore: update GitHub Actions runners from ubuntu-latest to hl-sdk-py-lin-md (#2021) - Refactored the Advanced Issue Template to V2 with stricter prerequisites and a focus on architectural design (#2016). From fb00c14a8345f7b09f4cd83b5161929e0028ede8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 02:06:54 +0530 Subject: [PATCH 34/60] chore(deps): bump actions/download-artifact from 8.0.0 to 8.0.1 (#2091) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/sync-issue-labels-add.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-issue-labels-add.yml b/.github/workflows/sync-issue-labels-add.yml index fc345431b..cc7bd4b1c 100644 --- a/.github/workflows/sync-issue-labels-add.yml +++ b/.github/workflows/sync-issue-labels-add.yml @@ -43,7 +43,7 @@ jobs: - name: Download labels artifact id: download continue-on-error: true - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: pr-labels-${{ github.event.inputs.pr_number }} path: artifacts From d1bae92c29f46b7e87b0e36def647afda2651d57 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 11:25:38 +0100 Subject: [PATCH 35/60] chore(deps): bump pypa/gh-action-pypi-publish from 1.13.0 to 1.14.0 (#2089) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4a83a1198..1d317b4f3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -43,4 +43,4 @@ jobs: run: python -m build - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 From dbca4a24c771837df5c5c59e4c3c2da824887727 Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Sat, 11 Apr 2026 16:59:55 +0530 Subject: [PATCH 36/60] test: refactor transaction response tests to eliminate duplication (#2096) Signed-off-by: tech0priyanshu Co-authored-by: exploreriii <133720349+exploreriii@users.noreply.github.com> --- CHANGELOG.md | 2 + tests/unit/test_transaction_response.py | 89 ------------- tests/unit/transaction_response_test.py | 162 ++++++++++++++++++++++-- 3 files changed, 154 insertions(+), 99 deletions(-) delete mode 100644 tests/unit/test_transaction_response.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 82ff18cbb..b99be60bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ### Tests - Refactor `mock_server` setup for network level TLS handling and added thread safety +- Refactor `transaction_response_test.py` by organizing the tests into `test_transaction_response.py` and `transaction_response_test.py`. (#2066) + ### Examples diff --git a/tests/unit/test_transaction_response.py b/tests/unit/test_transaction_response.py deleted file mode 100644 index af7afbc81..000000000 --- a/tests/unit/test_transaction_response.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Tests for TransactionResponse behavior. -""" - -import pytest - -from hiero_sdk_python.account.account_id import AccountId -from hiero_sdk_python.response_code import ResponseCode -from hiero_sdk_python.transaction.transaction_response import TransactionResponse -from hiero_sdk_python.hapi.services import ( - response_header_pb2, - response_pb2, - transaction_get_receipt_pb2, - transaction_receipt_pb2, -) - -from tests.unit.mock_server import mock_hedera_servers - -pytestmark = pytest.mark.unit - - -def test_transaction_response_fields(transaction_id): - """asserting response is correctly populated""" - resp = TransactionResponse() - - # Assert public attributes exist (PRIORITY 1: protect against breaking changes) - assert hasattr(resp, 'transaction_id'), "Missing public attribute: transaction_id" - assert hasattr(resp, 'node_id'), "Missing public attribute: node_id" - assert hasattr(resp, 'hash'), "Missing public attribute: hash" - assert hasattr(resp, 'validate_status'), "Missing public attribute: validate_status" - assert hasattr(resp, 'transaction'), "Missing public attribute: transaction" - - # Assert default values - assert resp.hash == bytes(), "Default hash should be empty bytes" - assert resp.validate_status is False, "Default validate_status should be False" - assert resp.transaction is None, "Default transaction should be None" - - resp.transaction_id = transaction_id - resp.node_id = AccountId(0, 0, 3) - - assert resp.transaction_id == transaction_id - assert resp.node_id == AccountId(0, 0, 3) - - -def test_transaction_response_get_receipt_is_pinned_to_submitting_node(transaction_id): - """ - mock_hedera_servers assigns: - - server[0] -> node 0.0.3 - - server[1] -> node 0.0.4 - - We make node 0.0.3 return a NON-retryable precheck error, and node 0.0.4 return SUCCESS. - If TransactionResponse.get_receipt() does not pin, it will likely hit 0.0.3 and fail. - If it pins to self.node_id (0.0.4), it will succeed. - """ - bad_node_response = response_pb2.Response( - transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( - header=response_header_pb2.ResponseHeader( - nodeTransactionPrecheckCode=ResponseCode.INVALID_TRANSACTION - ), - receipt=transaction_receipt_pb2.TransactionReceipt( - status=ResponseCode.UNKNOWN - ), - ) - ) - - good_node_response = response_pb2.Response( - transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( - header=response_header_pb2.ResponseHeader( - nodeTransactionPrecheckCode=ResponseCode.OK - ), - receipt=transaction_receipt_pb2.TransactionReceipt( - status=ResponseCode.SUCCESS - ), - ) - ) - - response_sequences = [ - [bad_node_response], # node 0.0.3 - [good_node_response], # node 0.0.4 - ] - - with mock_hedera_servers(response_sequences) as client: - resp = TransactionResponse() - resp.transaction_id = transaction_id - resp.node_id = AccountId(0, 0, 4) # submitting node (server[1]) - - receipt = resp.get_receipt(client) - - assert receipt.status == ResponseCode.SUCCESS diff --git a/tests/unit/transaction_response_test.py b/tests/unit/transaction_response_test.py index 5a594b1d1..5c0135639 100644 --- a/tests/unit/transaction_response_test.py +++ b/tests/unit/transaction_response_test.py @@ -1,7 +1,27 @@ +""" +Tests for TransactionResponse behavior. + +Includes receipt handling, record queries, and validation scenarios. +""" + + +# pylint: disable=no-member,no-name-in-module +# no-member is disabled because Protobuf uses runtime descriptors +# no-name-in-module is disabled above because of the dynamic nature of the generated protobuf + + import pytest + from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.exceptions import ReceiptStatusError +from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.transaction.transaction_id import TransactionId +from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt +from hiero_sdk_python.transaction.transaction_record import TransactionRecord +from hiero_sdk_python.transaction.transaction_response import TransactionResponse + + from hiero_sdk_python.hapi.services import ( basic_types_pb2, response_header_pb2, @@ -11,41 +31,47 @@ transaction_receipt_pb2, transaction_record_pb2, ) + + from hiero_sdk_python.hapi.services.query_header_pb2 import ResponseType from hiero_sdk_python.query.transaction_get_receipt_query import ( TransactionGetReceiptQuery, ) from hiero_sdk_python.query.transaction_record_query import TransactionRecordQuery -from hiero_sdk_python.response_code import ResponseCode -from hiero_sdk_python.transaction.transaction_id import TransactionId -from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt -from hiero_sdk_python.transaction.transaction_record import TransactionRecord -from hiero_sdk_python.transaction.transaction_response import TransactionResponse + + from tests.unit.mock_server import mock_hedera_servers + pytestmark = pytest.mark.unit + @pytest.fixture def transaction_response(): """Create a populated TransactionResponse for testing.""" response = TransactionResponse() - response.transaction_id = TransactionId.from_string("0.0.1001@1234567890.000000001") + response.transaction_id = TransactionId.from_string( + "0.0.1001@1234567890.000000001" + ) response.node_id = AccountId.from_string("0.0.3") response.validate_status = True return response + def test_get_receipt_query_builds_query(transaction_response): """Test get_receipt_query builds and returns the transaction receipt query.""" query = transaction_response.get_receipt_query() + assert isinstance(query, TransactionGetReceiptQuery) assert query.transaction_id == transaction_response.transaction_id assert len(query.node_account_ids) == 1 assert query.node_account_ids[0] == transaction_response.node_id + def test_get_receipt_executes_and_returns_receipt(transaction_response): """Test get_receipt execute receipt query and return transaction receipt.""" receipt_response = response_pb2.Response( @@ -64,27 +90,38 @@ def test_get_receipt_executes_and_returns_receipt(transaction_response): ) ) + with mock_hedera_servers([[receipt_response]]) as client: receipt = transaction_response.get_receipt(client) + assert isinstance(receipt, TransactionReceipt) assert receipt.status == ResponseCode.SUCCESS assert receipt.account_id.num == 1234 + def test_get_receipt_query_set_validate_status(transaction_response): """Test receipt query correctly initializes with the validate_status flag.""" query = transaction_response.get_receipt_query(validate_status=True) + assert isinstance(query, TransactionGetReceiptQuery) - assert query.validate_status == True + assert query.validate_status is True assert query.transaction_id == transaction_response.transaction_id assert len(query.node_account_ids) == 1 assert query.node_account_ids[0] == transaction_response.node_id -def test_get_receipt_returns_failure_status_without_validate_status(transaction_response): - """Test failing status returns a receipt instead of raising an error when validation is disabled.""" + +def test_get_receipt_returns_failure_status_without_validate_status( + transaction_response, +): + """Test failing status behavior. + + Ensures a receipt is returned instead of raising an error + when validation is disabled. + """ receipt_response = response_pb2.Response( transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( header=response_header_pb2.ResponseHeader( @@ -96,12 +133,16 @@ def test_get_receipt_returns_failure_status_without_validate_status(transaction_ ) ) + with mock_hedera_servers([[receipt_response]]) as client: receipt = transaction_response.get_receipt(client) + assert isinstance(receipt, TransactionReceipt) assert receipt.status == ResponseCode.INVALID_SIGNATURE + + def test_get_receipt_raises_exception_with_validate_status(transaction_response): """Test get_receipt error is raised for non-success statuses when validation is enabled.""" receipt_response = response_pb2.Response( @@ -115,30 +156,40 @@ def test_get_receipt_raises_exception_with_validate_status(transaction_response) ) ) + with mock_hedera_servers([[receipt_response]]) as client: with pytest.raises(ReceiptStatusError) as e: transaction_response.get_receipt(client, validate_status=True) + assert e.value.status == ResponseCode.INVALID_SIGNATURE + def test_get_record_query_builds_query(transaction_response): """Test get_record_query builds and returns the transaction record query.""" query = transaction_response.get_record_query() + assert isinstance(query, TransactionRecordQuery) assert query.transaction_id == transaction_response.transaction_id assert len(query.node_account_ids) == 1 assert query.node_account_ids[0] == transaction_response.node_id + def test_get_record_executes_and_returns_record(transaction_response): """Test get_record execute record query and return transaction record.""" receipt = transaction_receipt_pb2.TransactionReceipt(status=ResponseCode.SUCCESS) + + record = transaction_record_pb2.TransactionRecord( - receipt=receipt, memo="record", transactionFee=100 + receipt=receipt, + memo="record", + transactionFee=100, ) + record_response = [ [ response_pb2.Response( @@ -163,10 +214,101 @@ def test_get_record_executes_and_returns_record(transaction_response): ] ] + with mock_hedera_servers(record_response) as client: result = transaction_response.get_record(client) + assert isinstance(result, TransactionRecord) assert result.receipt.status == ResponseCode.SUCCESS assert result.transaction_fee == record.transactionFee assert result.transaction_memo == record.memo + + +# Tests for TransactionResponse behavior. + + +def test_transaction_response_fields(transaction_id): + """Asserting response is correctly populated.""" + resp = TransactionResponse() + + + # Assert public attributes exist (PRIORITY 1: protect against breaking changes) + assert hasattr(resp, "transaction_id") + assert hasattr(resp, "node_id") + assert hasattr(resp, "hash") + assert hasattr(resp, "validate_status") + assert hasattr(resp, "transaction") + + + # Assert default values + assert resp.hash == b"" + assert resp.validate_status is False + assert resp.transaction is None + + + resp.transaction_id = transaction_id + resp.node_id = AccountId(0, 0, 3) + + + assert resp.transaction_id == transaction_id + assert resp.node_id == AccountId(0, 0, 3) + + + +def test_transaction_response_get_receipt_is_pinned_to_submitting_node( + transaction_id, +): + """ + Test receipt retrieval behavior with node pinning. + + mock_hedera_servers assigns: + - server[0] -> node 0.0.3 + - server[1] -> node 0.0.4 + + + We make node 0.0.3 return a NON-retryable precheck error, and node 0.0.4 return SUCCESS. + If TransactionResponse.get_receipt() does not pin, it will likely hit 0.0.3 and fail. + If it pins to self.node_id (0.0.4), it will succeed. + """ + bad_node_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.INVALID_TRANSACTION + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.UNKNOWN + ), + ) + ) + + + good_node_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.SUCCESS + ), + ) + ) + + + response_sequences = [ + [bad_node_response], # node 0.0.3 + [good_node_response], # node 0.0.4 + ] + + + with mock_hedera_servers(response_sequences) as client: + resp = TransactionResponse() + resp.transaction_id = transaction_id + resp.node_id = AccountId(0, 0, 4) + + + # # submitting node (server[1]) + receipt = resp.get_receipt(client) + + + assert receipt.status == ResponseCode.SUCCESS From 3871426996df56c1f37269dfb822036075a4286b Mon Sep 17 00:00:00 2001 From: Krishna Mewara <161239575+DeathGun44@users.noreply.github.com> Date: Sat, 11 Apr 2026 17:40:03 +0530 Subject: [PATCH 37/60] chore: replace issue templates with upstream sdk-collaboration-hub templates (#2097) Signed-off-by: DeathGun44 Co-authored-by: Manish Dait <90558243+manishdait@users.noreply.github.com> --- .../00-good-first-issue-candidate.yml | 256 ++++++++++ .../ISSUE_TEMPLATE/01-good-first-issue.yml | 243 ++++++++++ .../01_good_first_issue_candidate.yml | 438 ------------------ .github/ISSUE_TEMPLATE/02-beginner-issue.yml | 239 ++++++++++ .../ISSUE_TEMPLATE/02_good_first_issue.yml | 422 ----------------- .../ISSUE_TEMPLATE/03-intermediate-issue.yml | 270 +++++++++++ .github/ISSUE_TEMPLATE/03_beginner_issue.yml | 305 ------------ .github/ISSUE_TEMPLATE/04-advanced-issue.yml | 152 ++++++ .../ISSUE_TEMPLATE/04_intermediate_issue.yml | 369 --------------- .github/ISSUE_TEMPLATE/05_advanced_issue.yml | 104 ----- .github/ISSUE_TEMPLATE/bug.yml | 1 - .github/ISSUE_TEMPLATE/feature.yml | 1 - CHANGELOG.md | 3 +- 13 files changed, 1162 insertions(+), 1641 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/00-good-first-issue-candidate.yml create mode 100644 .github/ISSUE_TEMPLATE/01-good-first-issue.yml delete mode 100644 .github/ISSUE_TEMPLATE/01_good_first_issue_candidate.yml create mode 100644 .github/ISSUE_TEMPLATE/02-beginner-issue.yml delete mode 100644 .github/ISSUE_TEMPLATE/02_good_first_issue.yml create mode 100644 .github/ISSUE_TEMPLATE/03-intermediate-issue.yml delete mode 100644 .github/ISSUE_TEMPLATE/03_beginner_issue.yml create mode 100644 .github/ISSUE_TEMPLATE/04-advanced-issue.yml delete mode 100644 .github/ISSUE_TEMPLATE/04_intermediate_issue.yml delete mode 100644 .github/ISSUE_TEMPLATE/05_advanced_issue.yml diff --git a/.github/ISSUE_TEMPLATE/00-good-first-issue-candidate.yml b/.github/ISSUE_TEMPLATE/00-good-first-issue-candidate.yml new file mode 100644 index 000000000..3435d70aa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/00-good-first-issue-candidate.yml @@ -0,0 +1,256 @@ +name: Good First Issue Candidate Template +description: Propose a potential Good First Issue (candidate) +labels: ["Good First Issue Candidate"] +assignees: [] +body: + - type: markdown + attributes: + value: | + --- + ## **Thanks for creating a Good First Issue Candidate!** 😊 + + Good First Issue Candidates are proposed tasks that are being evaluated for suitability as Good First Issues. They may require clarification or refinement before they are ready to be picked up by new contributors. + + Read more: + > [Applying Good First Issue Candidate Label](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-maintainers/labelling-issues-examples.md) + > [Issue Progression System](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-maintainers/README.md) + --- + + - type: textarea + id: intro-gfi-candidate + attributes: + label: ⚠️ Good First Issue — Candidate + value: | + > This issue is not yet a confirmed Good First Issue. + > It is being evaluated for suitability and may require + > clarification or refinement before it is ready to be picked up. + > + > **Please wait for maintainer confirmation before asking to be assigned.** + validations: + required: false + + - type: textarea + id: intro + attributes: + label: 🆕🐥 Newcomer Friendly + description: | + Adapt as needed. Welcome message. + value: | + Welcome! This is a **[Good First Issue Candidate](https://github.com/issues?q=is%3Aopen%20is%3Aissue%20org%3Ahiero-ledger%20archived%3Afalse%20no%3Aassignee%20(label%3A%22good%20first%20issue%22%20OR%20label%3A%22skill%3A%20good%20first%20issue%22)%20(repo%3Ahiero-ledger%2Fhiero-sdk-cpp%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-swift%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-python%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-js%20OR%20repo%3Ahiero-ledger%2Fhiero-website))** designed to be approachable to brand-new contributors. + + 🏁 **When this issue is complete, you will have:** + + ✅ Learned how to fork and navigate the codebase + ✅ Learned our contribution workflow + ✅ Implemented a real change to the SDK + + - type: textarea + id: issue + attributes: + label: 🐞 Problem Description + description: | + Describe the issue in a way that's easy for new contributors to understand. + Briefly explain why this change is useful or needed, even if the impact seems small. + Please avoid assuming prior knowledge of the language, codebase, or Hiero, as Good First Issues are often surfaced to new developers. + value: | + + + + validations: + required: true + + - type: textarea + id: solution + attributes: + label: 💡 Solution + description: | + Describe the solution. + Keep this high-level and easy to understand. Implementation details can go in the subsequent Implementation Steps section. + value: | + + validations: + required: true + + - type: textarea + id: implementation + attributes: + label: 🛠️ Implementation Steps + description: | + To make this issue easy to pick up and complete, please include: + - Which files need to be changed or added + - Any functions, classes, or modules involved + - The complete steps to implement the solution + - What the final result or output should look like + - Links to relevant documentation or code (if helpful) + For good first issues, please keep this as guided and clear as possible. + value: | + + + + validations: + required: true + + - type: textarea + id: new_contributors + attributes: + label: 🧠 Good First Issue Developers — Prerequisites & Expectations + description: | + Adapt as needed. Concrete requirements before claiming this good first issue. Adapt as needed. + value: | + > [!IMPORTANT] + > **This issue does not require prior domain knowledge.** + > + > - No Hiero or Hedera experience needed + > - No distributed ledger background required + > - **Beginner Programming is sufficient** + + > [!NOTE] + > ⏱️ **Typical time to complete:** ~1 Day / ~4 hours (including set-up) + > 🧩 **Difficulty:** Small, well-contained change + > 🎓 **Best for:** New contributors + + 🤖 AI Usage Policy + + - Good First Issues are open for humans, AI bot-authored PRs will be rejected. + - Refer to our set-up and workflow guides, AI may make mistakes. + + > [!WARNING] + > We expect pull requests to use our workflow and meet the quality standards outlined in this issue. + + - type: textarea + id: setup_steps + attributes: + label: 📋 Step-by-Step Setup Procedure + description: Provide a step-by-step setup guide for new contributors + value: | + - [ ] Visual Studio (VS) Code: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-setup.md#visual-studio-code) + + - [ ] GitHub Desktop: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-setup.md#github-desktop) + + - [ ] Hedera Testnet Account with root .env file: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-setup.md#2-setting-up-a-portal-account) + + - [ ] Create a GPG key linked to GitHub: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-setup.md#3-set-up-a-gpg-key-for-signing) + + - [ ] **Fork** Create an online and local copy of the repository: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-setup.md#4-fork-the-repository) + + - [ ] **Connect** origin with upstream: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-setup.md#5-connect-your-origin-with-upstream) + + - [ ] **Install Packages** and protobufs: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-setup.md#6-install-packages-and-protobufs) + + - [ ] **Windows users:** see the [Windows Setup Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/setup_windows.md) for platform-specific installation steps. + + - [ ] **Sync Main** pull any recent upstream changes: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-setup.md#7-sync-main) + + You are set up! 🎉 + + - type: textarea + id: workflow_steps + attributes: + label: 📋 Step-by-step workflow guide + description: Provide a contribution workflow suitable for new contributors. + value: | + #### ✅ Get ready + + - [ ] **Claim the issue:** comment `/assign`: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-workflow.md#1-get-assigned-to-an-issue). Pull requests created without being assigned will be automatically closed. + + - [ ] **Double check the Issue and AI plan:** carefully re-read the issue description and any AI plan below + + - [ ] **Ask questions early:** ask on [Discord](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/discord.md), and the `@good_first_issue_support_team` (setup and workflow help) + + - [ ] **Sync Main** pull any recent upstream changes: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-setup.md#7-sync-main) + + - [ ] 💡 Tip: Before coding, you are free to leave a short comment describing what you plan to change. We'll confirm you're on the right track. + + #### 🛠️ Solve the Issue + + - [ ] **Create a branch from `main`:** [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-workflow.md#2-create-a-branch) + + - [ ] **Implement the solution**: follow the implementation steps in the issue description. + + - [ ] **Commit with Conventional format and DCO, GPG signing:** commit changes using: `git commit -S -s -m "chore: your message"`, [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-workflow.md#3-commit-your-changes) + + - [ ] **Add a `CHANGELOG.md` entry:** under the appropriate **[UNRELEASED]** section and commit as `git commit -S -s -m "chore: changelog entry"` [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-workflow.md#4-create-a-changelog-entry) + + #### 🚀 Create the pull request + + - [ ] **Push your commits:** push your branch to your fork `git push origin your-branch-name` [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-workflow.md#5-submitting-a-pull-request) + + - [ ] **Open a pull request:** [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-workflow.md#5-submitting-a-pull-request) + + - [ ] **Complete the PR description:** briefly describe your changes, [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-workflow.md#5-submitting-a-pull-request) + + - [ ] **Link the Issue:** link the issue the PR solves in the PR description, [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-workflow.md#5-submitting-a-pull-request). Pull requests created without a linked issue will be automatically closed. + + - [ ] **Submit the pull request:** click `**Create pull request**` and `Ready to Review` 🎉 + + - type: textarea + id: acceptance + attributes: + label: ✅ PR Quality Checklist + description: | + Adapt as needed. These are the standards the contributor must meet before opening a PR. + value: | + To be able to close this issue, the following criteria must be met: + + Workflow: + + - [ ] I have worked from a branch that is up to date with main + - [ ] I have not made changes outside the scope of this issue + - [ ] My commits are signed: `git commit -S -s -m "chore: description"` — [Signing guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/signing.md) + - [ ] I added a CHANGELOG.md entry (if appropriate) under `[Unreleased]` — [Changelog guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/changelog-guide.md) + - [ ] I linked the issue in my PR description using `Fixes #issue_number` to automatically close it when merged + - [ ] I have applied appropriate linting, code quality, and formatting tools used in this repo (if applicable). + + Issue Requirement: + - [ ] The issue is solved: I've carefully read and implemented the issue requirements + --- + + - type: textarea + id: pr_expectations + attributes: + label: 🤔 What to expect after submitting a PR + description: Explain what happens after a pull request is opened. + value: | + Once you open a pull request, here's what happens next. + + **🤖 1. Automated checks** + Automated checks will run and all must pass before merging. + Open any failed check at the bottom of the PR to see details. Ask for help if you don't understand a failure or how to fix it. + + **😎 2. Initial Team review** + Once tests pass, a team member will check if your PR follows the workflow. + + **✅ 3. Team review** + Once your tests pass and the workflow is followed, a team member will review the implementation. + You may be asked to make changes or your PR may be approved. + Approved PRs are usually merged within **one day**. + + **🔄 Merge conflicts (sometimes)** + Conflicts can happen and are normal as the SDK updates. Resolve using this [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/merge_conflicts.md) + + > [!TIP] + > **Follow the workflow and double check your work** + > This is the best way to ensure a fast review and merge process. + --- + + - type: textarea + id: getting_help + attributes: + label: 🧭 Getting help if you're stuck + description: How to get support while working on this issue. + value: | + **🆘 Stuck?** + + > [!TIP] + > + > - Comment on this issue and ask maintainers or tag `@good_first_issue_support_team` + > + + #### Other Resources: + + - [New Starter Docs (Signing, Rebasing, Changelog, Merge Conflicts, Workflow, Setup)](https://github.com/hiero-ledger/sdk-collaboration-hub/tree/main/guides/issue-progression/for-developers) + - [All Things Github Workflows](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/github-action-workflows.md) + - [Community Calls](https://zoom-lfx.platform.linuxfoundation.org/meetings/hiero?view=week) + - [Discord](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/discord.md) + + --- diff --git a/.github/ISSUE_TEMPLATE/01-good-first-issue.yml b/.github/ISSUE_TEMPLATE/01-good-first-issue.yml new file mode 100644 index 000000000..a8c0e2961 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01-good-first-issue.yml @@ -0,0 +1,243 @@ +name: Good First Issue Template +description: Create a Good First Issue +labels: ["Good First Issue"] +assignees: [] +body: + - type: markdown + attributes: + value: | + --- + ## **Thanks for creating a Good First Issue!** 😊 + + Good First Issues are guided, well-scoped tasks designed to help you get started and learn the workflow. + + Read more: + > [Applying Good First Issue Label](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-maintainers/labelling-issues-examples.md) + > [Issue Progression System](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-maintainers/README.md) + --- + + - type: textarea + id: intro + attributes: + label: 🆕🐥 Newcomer Friendly + description: | + Adapt as needed. Welcome message. + value: | + Welcome! This is a **[Good First Issue](https://github.com/issues?q=is%3Aopen%20is%3Aissue%20org%3Ahiero-ledger%20archived%3Afalse%20no%3Aassignee%20(label%3A%22good%20first%20issue%22%20OR%20label%3A%22skill%3A%20good%20first%20issue%22)%20(repo%3Ahiero-ledger%2Fhiero-sdk-cpp%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-swift%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-python%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-js%20OR%20repo%3Ahiero-ledger%2Fhiero-website))** designed to be approachable to brand-new contributors. + + 🏁 **When this issue is complete, you will have:** + + ✅ Learned how to fork and navigate the codebase + ✅ Learned our contribution workflow + ✅ Implemented a real change to the SDK + + - type: textarea + id: issue + attributes: + label: 🐞 Problem Description + description: | + Describe the issue in a way that's easy for new contributors to understand. + Briefly explain why this change is useful or needed, even if the impact seems small. + Please avoid assuming prior knowledge of the language, codebase, or Hiero, as Good First Issues are often surfaced to new developers. + value: | + + + + validations: + required: true + + - type: textarea + id: solution + attributes: + label: 💡 Solution + description: | + Describe the solution. + Keep this high-level and easy to understand. Implementation details can go in the subsequent Implementation Steps section. + value: | + + validations: + required: true + + - type: textarea + id: implementation + attributes: + label: 🛠️ Implementation Steps + description: | + To make this issue easy to pick up and complete, please include: + - Which files need to be changed or added + - Any functions, classes, or modules involved + - The complete steps to implement the solution + - What the final result or output should look like + - Links to relevant documentation or code (if helpful) + For good first issues, please keep this as guided and clear as possible. + value: | + + + + validations: + required: true + + - type: textarea + id: new_contributors + attributes: + label: 🧠 Good First Issue Developers — Prerequisites & Expectations + description: | + Adapt as needed. Concrete requirements before claiming this good first issue. Adapt as needed. + value: | + > [!IMPORTANT] + > **This issue does not require prior domain knowledge.** + > + > - No Hiero or Hedera experience needed + > - No distributed ledger background required + > - **Beginner Programming is sufficient** + + > [!NOTE] + > ⏱️ **Typical time to complete:** ~1 Day / ~4 hours (including set-up) + > 🧩 **Difficulty:** Small, well-contained change + > 🎓 **Best for:** New contributors + + 🤖 AI Usage Policy + + - Good First Issues are open for humans, AI bot-authored PRs will be rejected. + - Refer to our set-up and workflow guides, AI may make mistakes. + + > [!WARNING] + > We expect pull requests to use our workflow and meet the quality standards outlined in this issue. + + - type: textarea + id: setup_steps + attributes: + label: 📋 Step-by-Step Setup Procedure + description: Provide a step-by-step setup guide for new contributors + value: | + - [ ] Visual Studio (VS) Code: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-setup.md#visual-studio-code) + + - [ ] GitHub Desktop: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-setup.md#github-desktop) + + - [ ] Hedera Testnet Account with root .env file: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-setup.md#2-setting-up-a-portal-account) + + - [ ] Create a GPG key linked to GitHub: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-setup.md#3-set-up-a-gpg-key-for-signing) + + - [ ] **Fork** Create an online and local copy of the repository: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-setup.md#4-fork-the-repository) + + - [ ] **Connect** origin with upstream: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-setup.md#5-connect-your-origin-with-upstream) + + - [ ] **Install Packages** and protobufs: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-setup.md#6-install-packages-and-protobufs) + + - [ ] **Windows users:** see the [Windows Setup Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/setup_windows.md) for platform-specific installation steps. + + - [ ] **Sync Main** pull any recent upstream changes: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-setup.md#7-sync-main) + + You are set up! 🎉 + + - type: textarea + id: workflow_steps + attributes: + label: 📋 Step-by-step workflow guide + description: Provide a contribution workflow suitable for new contributors. + value: | + #### ✅ Get ready + + - [ ] **Claim the issue:** comment `/assign`: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-workflow.md#1-get-assigned-to-an-issue). Pull requests created without being assigned will be automatically closed. + + - [ ] **Double check the Issue and AI plan:** carefully re-read the issue description and any AI plan below + + - [ ] **Ask questions early:** ask on [Discord](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/discord.md), and the `@good_first_issue_support_team` (setup and workflow help) + + - [ ] **Sync Main** pull any recent upstream changes: [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-setup.md#7-sync-main) + + - [ ] 💡 Tip: Before coding, you are free to leave a short comment describing what you plan to change. We'll confirm you're on the right track. + + #### 🛠️ Solve the Issue + + - [ ] **Create a branch from `main`:** [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-workflow.md#2-create-a-branch) + + - [ ] **Implement the solution**: follow the implementation steps in the issue description. + + - [ ] **Commit with Conventional format and DCO, GPG signing:** commit changes using: `git commit -S -s -m "chore: your message"`, [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-workflow.md#3-commit-your-changes) + + - [ ] **Add a `CHANGELOG.md` entry:** under the appropriate **[UNRELEASED]** section and commit as `git commit -S -s -m "chore: changelog entry"` [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-workflow.md#4-create-a-changelog-entry) + + #### 🚀 Create the pull request + + - [ ] **Push your commits:** push your branch to your fork `git push origin your-branch-name` [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-workflow.md#5-submitting-a-pull-request) + + - [ ] **Open a pull request:** [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-workflow.md#5-submitting-a-pull-request) + + - [ ] **Complete the PR description:** briefly describe your changes, [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-workflow.md#5-submitting-a-pull-request) + + - [ ] **Link the Issue:** link the issue the PR solves in the PR description, [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-workflow.md#5-submitting-a-pull-request). Pull requests created without a linked issue will be automatically closed. + + - [ ] **Submit the pull request:** click `**Create pull request**` and `Ready to Review` 🎉 + + - type: textarea + id: acceptance + attributes: + label: ✅ PR Quality Checklist + description: | + Adapt as needed. These are the standards the contributor must meet before opening a PR. + value: | + To be able to close this issue, the following criteria must be met: + + Workflow: + + - [ ] I have worked from a branch that is up to date with main + - [ ] I have not made changes outside the scope of this issue + - [ ] My commits are signed: `git commit -S -s -m "chore: description"` — [Signing guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/signing.md) + - [ ] I added a CHANGELOG.md entry (if appropriate) under `[Unreleased]` — [Changelog guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/changelog-guide.md) + - [ ] I linked the issue in my PR description using `Fixes #issue_number` to automatically close it when merged + - [ ] I have applied appropriate linting, code quality, and formatting tools used in this repo (if applicable). + + Issue Requirement: + - [ ] The issue is solved: I've carefully read and implemented the issue requirements + --- + + - type: textarea + id: pr_expectations + attributes: + label: 🤔 What to expect after submitting a PR + description: Explain what happens after a pull request is opened. + value: | + Once you open a pull request, here's what happens next. + + **🤖 1. Automated checks** + Automated checks will run and all must pass before merging. + Open any failed check at the bottom of the PR to see details. Ask for help if you don't understand a failure or how to fix it. + + **😎 2. Initial Team review** + Once tests pass, a team member will check if your PR follows the workflow. + + **✅ 3. Team review** + Once your tests pass and the workflow is followed, a team member will review the implementation. + You may be asked to make changes or your PR may be approved. + Approved PRs are usually merged within **one day**. + + **🔄 Merge conflicts (sometimes)** + Conflicts can happen and are normal as the SDK updates. Resolve using this [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/merge_conflicts.md) + + > [!TIP] + > **Follow the workflow and double check your work** + > This is the best way to ensure a fast review and merge process. + --- + + - type: textarea + id: getting_help + attributes: + label: 🧭 Getting help if you're stuck + description: How to get support while working on this issue. + value: | + **🆘 Stuck?** + + > [!TIP] + > + > - Comment on this issue and ask maintainers or tag `@good_first_issue_support_team` + > + + #### Other Resources: + + - [New Starter Docs (Signing, Rebasing, Changelog, Merge Conflicts, Workflow, Setup)](https://github.com/hiero-ledger/sdk-collaboration-hub/tree/main/guides/issue-progression/for-developers) + - [All Things Github Workflows](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/github-action-workflows.md) + - [Community Calls](https://zoom-lfx.platform.linuxfoundation.org/meetings/hiero?view=week) + - [Discord](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/discord.md) + + --- diff --git a/.github/ISSUE_TEMPLATE/01_good_first_issue_candidate.yml b/.github/ISSUE_TEMPLATE/01_good_first_issue_candidate.yml deleted file mode 100644 index fad68075c..000000000 --- a/.github/ISSUE_TEMPLATE/01_good_first_issue_candidate.yml +++ /dev/null @@ -1,438 +0,0 @@ -name: Good First Issue Candidate Template -description: Propose a potential Good First Issue (candidate) -title: "[Good First Issue]:" -labels: ["Good First Issue Candidate"] -assignees: [] -body: - - type: markdown - attributes: - value: | - --- - ## **Thanks for creating a good first issue candidate!** 😊 - - We truly appreciate your time and effort, welcome! - This template is designed to help you create a Good First Issue Candidate (GFI) : a small, well-scoped task that may be missing some documentation or there is uncertainty if it is a good fit for a GFI. - --- - - - type: textarea - id: intro-gfi-candidate - attributes: - label: ⚠️ Good First Issue — Candidate - value: | - > This issue is not yet a confirmed Good First Issue. - > It is being evaluated for suitability and may require - > clarification or refinement before it is ready to be picked up. - > - > **Please wait for maintainer confirmation before asking to be assigned.** - > - > Maintainers and reviewers can read more about [Good First Issues](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/maintainers/good_first_issues_guidelines.md) and [Good First Issue Candidates](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/maintainers/good_first_issue_candidate_guidelines.md) - - validations: - required: false - - - type: textarea - id: intro - attributes: - label: 🆕🐥 Newcomer Friendly - description: Who is this issue for? - value: | - This **[Good First Issue](https://github.com/hiero-ledger/hiero-sdk-python/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22Good%20First%20Issue%22%20no%3Aassignee)** is a guided, well-scoped task intended for new human contributors to the Hiero Python SDK. - - #### What you’ll do - - ✅ understand how the repository is structured - - ✅ practice the standard contribution workflow - - ✅ submit and merge a pull request - - #### Support - A maintainer or mentor actively monitors this issue and will help **guide it to completion**. - - > [!IMPORTANT] - > **This issue does not require prior domain knowledge.** - > - > - No Hiero or Hedera experience needed - > - No distributed ledger background required - > - **Basic Python and Git are sufficient** - - > [!NOTE] - > ⏱️ **Typical time to complete:** 30–90 minutes (once setup is done) - > 🧩 **Difficulty:** Small, well-contained change - > 🎓 **Best for:** New contributors - - **🏁 Completion** - When this issue is complete, you will have: - - - ✅ Solved a real issue - - ✅ A merged pull request in the Hiero Python SDK - - ✅ Your name in the project history - - ✅ Confidence to take on larger issues next - - - type: markdown - attributes: - value: | - > [!IMPORTANT] - > #### 📋 Good First Issue (GFI) Guidelines - > A **Good First Issue [Guidelines](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/maintainers/good_first_issue_guidelines.md)** is a small, well-scoped task that helps new contributors get familiar with the codebase and contribution workflow. - > - > **Often a good fit for Good First Issues:** - > - Spelling or grammar fixes - > - Small documentation formatting improvements - > - Purely structural refactors of examples - > - Minor, clearly described edits to docstrings or comments - > - Light file moves or renames for clarity - > - Applying formatting tools (e.g. `black`, `ruff`) in limited areas - > - > More involved tasks may be a better fit for **Beginner**, **Intermediate**, or **Advanced** issues. - > - > If you’re unsure, the **Good First Issue Candidate Template** can be used. - - - type: textarea - id: issue - attributes: - label: 👾 Issue description - description: | - Describe the issue in a way that’s easy for new contributors to understand. - Briefly explain why this change is useful or needed, even if the impact seems small. - Please avoid assuming prior knowledge of the language, codebase, or Hiero, as Good First Issues are often surfaced to new developers. - Links to relevant documentation or code are very welcome. - value: | - Edit here. Example provided below. - validations: - required: true - - - type: markdown - attributes: - value: | - - ## 👾 Description of the issue - Example - - The example for Token Associate Transaction located at examples/tokens/token_associate_transaction.py can be improved. It correctly illustrates how to associate a token, however, it does so all from one function main() - - As everything is grouped together in main(), it is difficult for a user to understand all the individual steps required to associate a token. - - For example: - ```python - - def run_demo(): - """Monolithic token association demo.""" - print(f"🚀 Connecting to Hedera {network_name} network!") - client = Client(Network(network_name)) - operator_id = AccountId.from_string(os.getenv("OPERATOR_ID", "")) - operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY", "")) - client.set_operator(operator_id, operator_key) - print(f"✅ Client ready (operator {operator_id})") - - test_key = PrivateKey.generate_ed25519() - receipt = ( - AccountCreateTransaction() - .set_key(test_key.public_key()) - .set_initial_balance(Hbar(1)) - .set_account_memo("Test account for token association demo") - .freeze_with(client) - .sign(operator_key) - .execute(client) - ) - if receipt.status != ResponseCode.SUCCESS: - raise Exception(receipt.status) - account_id = receipt.account_id - print(f"✅ Created test account {account_id}") - - # Create tokens - tokens = [] - for i in range(3): - try: - receipt = ( - TokenCreateTransaction() - .set_token_name(f"DemoToken{i}") - .set_token_symbol(f"DTK{i}") - .set_decimals(2) - .set_initial_supply(100_000) - .set_treasury_account_id(operator_id) - .freeze_with(client) - .sign(operator_key) - .execute(client) - ) - if receipt.status != ResponseCode.SUCCESS: - raise Exception(receipt.status) - token_id = receipt.token_id - tokens.append(token_id) - print(f"✅ Created token {token_id}") - except Exception as e: - print(f"❌ Token creation failed: {e}") - sys.exit(1) - - # Associate first token - try: - TokenAssociateTransaction().set_account_id(account_id).add_token_id(tokens[0]).freeze_with(client).sign(test_key).execute(client) - print(f"✅ Token {tokens[0]} associated with account {account_id}") - except Exception as e: - print(f"❌ Token association failed: {e}") - sys.exit(1) - ``` - - - type: textarea - id: solution - attributes: - label: 💡 Proposed Solution - description: | - Describe what a good solution would look like. - Keep this high-level and easy to understand. Implementation details can go in the subsequent Implementation Steps section. - value: | - Edit here. Example provided below. - validations: - required: true - - - type: markdown - attributes: - value: | - - ## 💡 Solution - Example - - For the TokenAssociateTransaction example, the solution is to split the monolithic main() function for illustrating TokenAssociateTransaction into separate smaller functions which are called from main(). - Such as: - - Setting up the client - - Creating an account - - Creating a token - - Associating the account to the token - - - type: textarea - id: implementation - attributes: - label: 🛠️ Implementation Steps - description: | - To make this issue easy to pick up and complete, please include: - - Which files need to be changed or added - - Any functions, classes, or modules involved - - The complete steps to implement the solution - - What the final result or output should look like - - Links to relevant documentation or code (if helpful) - For good first issues, please keep this as guided and clear as possible. - value: | - Edit here. Example provided below. - - --- - validations: - required: true - - - type: markdown - attributes: - value: | - - #### 👩‍💻 Implementation - Example - - To break down the monolithic main function, you need to: - - [ ] Extract the Key Steps (set up a client, create a test account, create a token, associate the token) - - [ ] Copy and paste the functionality for each key step into its own function - - [ ] Pass to each function the variables you need to run it - - [ ] Call each function in main() - - [ ] Ensure you return the values you'll need to pass on to the next step in main - - [ ] Ensure the example still runs and has the same output! - - For example: - ```python - - def setup_client(): - """Initialize and set up the client with operator account.""" - - def create_test_account(client, operator_key): - """Create a new test account for demonstration.""" - - def create_fungible_token(client, operator_id, operator_key): - """Create a fungible token for association with test account.""" - - def associate_token_with_account(client, token_id, account_id, account_key): - """Associate the token with the test account.""" - - def main(): - client, operator_id, operator_key = setup_client() - account_id, account_private_key = create_test_account(client, operator_key) - token_id = create_fungible_token(client, operator_id, operator_key) - associate_token_with_account(client, token_id, account_id, account_private_key) - ``` - - - type: textarea - id: setup_steps - attributes: - label: 📋 Step-by-Step Setup Guide - description: Provide a step-by-step setup guide for new contributors - value: | - #### Suggestions: - - [ ] Visual Studio (VS) Code: [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/01_supporting_infrastructure.md) - - - [ ] GitHub Desktop: [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/01_supporting_infrastructure.md) - - - [ ] Hedera Testnet Account with root .env file: [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/setup/03_setting_up_env.md) - - - [ ] Create a GPG key linked to GitHub: [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/signing.md) - - #### Setup the Hiero Python SDK for development - - [ ] **Fork** Create an online and local copy of the repository: [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/02_forking_python_sdk.md) - - - [ ] **Connect** origin with upstream: [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/03_staying_in_sync.md) - - - [ ] **Install Packages** and protobufs: [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/setup/02_installing_hiero_python_sdk.md) (or [Windows Setup Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/setup_windows.md) for Windows users) - - - [ ] **Sync Main** pull any recent upstream changes: [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/rebasing.md) - - You are set up! 🎉 - validations: - required: true - - - type: textarea - id: contribution_steps - attributes: - label: 📋 Step-by-step contribution guide - description: Provide a contribution workflow suitable for new contributors. - value: | - #### ✅ Get ready - - [ ] **Claim the issue:** comment `/assign`: [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/04_assigning_issues.md). Pull requests created without being assigned will be automatically closed. - - - [ ] **Double check the Issue and AI plan:** carefully re-read the issue description and the CodeRabbit AI plan - - - [ ] **Ask questions early:** ask on [Discord](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md), your `@mentor` (Python SDK help) and the `@good_first_issue_support_team` (setup and workflow help) - - - [ ] **Sync with main:** pull the latest upstream changes [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/rebasing.md) - - - [ ] 💡 Tip: Before coding, leave a short comment describing what you plan to change. We’ll confirm you’re on the right track. - - #### 🛠️ Solve the Issue - - [ ] **Create a branch from `main`:** [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/05_working_branches.md) - - - [ ] **Implement the solution**: follow the implementation steps in the issue description. - - - [ ] **Commit with DCO and GPG signing:** commit changes using: `git commit -S -s -m "chore: your message"`, [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/signing.md) - - - [ ] **Add a `.CHANGELOG.md` entry:** under the appropriate **[UNRELEASED]** section and commit as `git commit -S -s -m "chore: changelog entry"` [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/changelog_entry.md) - - #### 🚀 Create the pull request - - [ ] **Push your commits:** push your branch to your fork `git push origin your-branch-name` - - - [ ] **Open a pull request:** [here](https://github.com/hiero-ledger/hiero-sdk-python/pulls) [guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/11_submit_pull_request.md) - - - [ ] **Complete the PR description:** briefly describe your changes, [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/11_submit_pull_request.md) - - - [ ] **Link the Issue:** link the issue the PR solves in the PR description, [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/how_to_link_issues.md). Pull requests created without a linked issue will be automatically closed. - - - [ ] **Submit the pull request:** click `**Create pull request**` 🎉 - - validations: - required: true - - - type: textarea - id: acceptance-criteria - attributes: - label: ✅ Acceptance criteria - description: | - Edit or expand this checklist with what is required to merge a pull request for this issue. - value: | - To be able to close this issue, the following criteria must be met: - - - [ ] **The issue is solved:** I’ve carefully read and implemented the issue requirements - - - [ ] **I did not add extra changes:** I did not modify anything beyond what is described in the issue - - - [ ] **Behavior:** All other existing features continue to work as before - - - [ ] **Checks and feedback:** All checks pass and any requested changes have been made - validations: - required: true - - - type: textarea - id: getting_help - attributes: - label: 🧭 Getting help if you’re stuck - description: How to get support while working on this issue. - value: | - If questions come up, don’t spend more than **20 minutes** blocked. - - > [!TIP] - > - > - Comment on this issue and tag `@good_first_issue_support_team` or `@mentor_name` - > - Ask for help in [Discord](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md) - > - - --- - - - type: textarea - id: pr_expectations - attributes: - label: 🤔 What to expect after submitting a PR - description: Explain what happens after a pull request is opened. - value: | - Once you open a pull request, here’s what happens next. - - **🤖 1. Automated checks** - A small set of automated checks must pass before merging (signing, changelog, tests, examples, code quality). - Open any failed check to see details. - - --- - - **🤝 2. AI feedback (CodeRabbit)** - CodeRabbit AI may suggest improvements or flag issues. - Feedback is advisory — use what’s relevant and helpful. - - --- - - **😎 3. Team review** - A Python SDK team member reviews your PR within **1–3 days**. - You may be asked to make changes or your PR may be approved. - Approved PRs are usually merged within **one day**. - - - **🔄 Merge conflicts (sometimes)** - Conflicts can happen and are normal as the SDK updates. - Changelog conflicts can be resolved online in the PR in the merge editor, accepting both entries - Others may require **[rebasing](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/rebasing.md)**. - - --- - - validations: - required: true - - - type: textarea - id: ai_usage_guidelines - attributes: - label: 🤖 AI usage guidelines - description: Guidance on using AI tools responsibly for this issue. - value: | - Humans are welcome to use AI tools while working on this issue but we do not accept AI bot-authored PRs. - - **Use AI responsibly:** - - review suggestions carefully - - apply changes incrementally - - test as you go - - If in doubt, ask — maintainers are happy to help. - - - type: textarea - id: information - attributes: - label: 🤔 Additional Help - description: Provide any extra resources or context for contributors to solve this good first issue - value: | - #### First Points of Contact: - - [Discord](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md) - - Comment with `@mentor_name` (for Python SDK questions) - - Comment with `@hiero-ledger/hiero-sdk-good-first-issue-support` (for setup and workflow questions) - The more you ask, the more you learn and so do we! - - #### Documentation: - - [README.md](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/README.md) - - [CONTRIBUTING.md](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/CONTRIBUTING.md) - - [Project Structure](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/setup/project_structure.md) - - [DCO and Verified Signing Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/signing.md) - - [Changelog Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/changelog_entry.md) - - [Rebasing Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/rebasing.md) - - [Merge Conflicts Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/merge_conflicts.md) - - [Linking Issues Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/how_to_link_issues.md) - - [Workflow Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/workflow.md) - - - [Pin Github Actions Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/how-to-pin-github-actions.md) - - [Running Examples](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/examples.md) - - [Testing on Forks](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/testing_forks.md) - - - [General Training](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training) - - [General SDK Developer Docs](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers) - - #### Calls: - - Get hands-on-help by our expert team at our [Office Hours](https://zoom-lfx.platform.linuxfoundation.org/meeting/99912667426?password=5b584a0e-1ed7-49d3-b2fc-dc5ddc888338) - - Learn, raise issues and provide feedback at [Community Calls](https://zoom-lfx.platform.linuxfoundation.org/meeting/92041330205?password=2f345bee-0c14-4dd5-9883-06fbc9c60581) diff --git a/.github/ISSUE_TEMPLATE/02-beginner-issue.yml b/.github/ISSUE_TEMPLATE/02-beginner-issue.yml new file mode 100644 index 000000000..fffe1c838 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-beginner-issue.yml @@ -0,0 +1,239 @@ +name: Beginner Issue Template +description: Create a beginner issue for contributors ready to learn the codebase and own a small implementation. +labels: ["beginner"] +assignees: [] +body: + + - type: markdown + attributes: + value: | + --- + ## **Thanks for creating a beginner issue!** 😊 + + Beginner issues are well-scoped tasks designed to help you learn the codebase and follow best-practices. + + Read more: + > [Applying Beginner Label](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-maintainers/labelling-issues-examples.md) + > [Issue Progression System](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-maintainers/README.md) + --- + + - type: textarea + id: intro + attributes: + label: 🧑‍🎓 Beginner Issue + description: | + Adapt as needed. Welcome message. + value: | + Welcome! This is a **[Beginner Issue](https://github.com/issues?q=is%3Aopen%20is%3Aissue%20org%3Ahiero-ledger%20archived%3Afalse%20no%3Aassignee%20(label%3A%22beginner%22%20OR%20label%3A%22skill%3A%20beginner%22)%20(repo%3Ahiero-ledger%2Fhiero-sdk-cpp%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-swift%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-python%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-js%20OR%20repo%3Ahiero-ledger%2Fhiero-website))** designed to help you learn the codebase. + + 🏁 **When this issue is complete, you will have:** + + ✅ Researched a file or method in detail + ✅ Followed SDK patterns to implement a solution + ✅ Researched and identified appropriate programming constructs to solve the problem + ✅ Created basic tests + ✅ Delivered a clean, review-ready pull request + + - type: textarea + id: problem + attributes: + label: 🐞 Problem Description + description: | + Describe the issue so it is easy for a beginner contributor to understand what needs to be done and why. + Include which file(s) to look at, what the current behaviour is, and what the desired behaviour is. + value: | + + + + + validations: + required: true + + + - type: textarea + id: solution + attributes: + label: 💡 Expected Solution + description: | + Briefly describe what the contributor should build or fix. + This should be a short summary of the expected outcome — not a detailed implementation guide. + value: | + + validations: + required: true + + - type: textarea + id: research + attributes: + label: 🔍 Research Pointers + description: | + Point the contributor toward relevant files, similar code, protobuf definitions, or packages to study. + This is the most important, "investigate before you code" step. + Think: what would a colleague tell you if you asked "where do I start?" + Include links to language documentation if the task involves a language concept. + value: | + + + + validations: + required: true + + - type: textarea + id: implementation + attributes: + label: 🛠️ Implementation Notes + description: | + Briefly outline the implementation, nudging them to research and make their own decisions. + value: | + + validations: + required: true + + - type: textarea + id: beginner_contributors + attributes: + label: 🧠 Beginner Contributors — Prerequisites & Expectations + description: | + Adapt as needed. Concrete requirements before claiming this beginner issue. Adapt as needed. + value: | + > [!CAUTION] + > **Beginner issues are low-risk but we expect a working solution.** + + > [!IMPORTANT] + > We recommend completing at least 3 good first issues before attempting a beginner issue. + + **Difficulty:** + + - Requires building knowledge of the specific file(s) + - Beginner to intermediate Coding skills + + > [!TIP] + > **You should be comfortable with:** + > - Forking, branching, committing (with DCO + GPG signing), and opening a pull request without a tutorial + > - Reading beginner to intermediate code you did not write and following its patterns + > - Handling simple merge conflicts + > - Keeping your fork up to date with main by [rebasing](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/rebasing.md) + > - Looking things up on your own before asking for help + + If any of that feels unfamiliar, good first issues might still be the most rewarding path for you right now — [find one here](https://github.com/issues?q=is%3Aopen%20is%3Aissue%20org%3Ahiero-ledger%20archived%3Afalse%20no%3Aassignee%20(label%3A%22good%20first%20issue%22%20OR%20label%3A%22skill%3A%20good%20first%20issue%22)%20(repo%3Ahiero-ledger%2Fhiero-sdk-cpp%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-swift%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-python%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-js%20OR%20repo%3Ahiero-ledger%2Fhiero-website)). + You can always come back when you are ready. + + ### ⚠️ AI Usage Policy + + - AI can help you understand the codebase and the problem + - Using AI to generate code for beginner issues is **strictly discouraged** + - Using AI as the main source of research is **strictly discouraged**, refer to language and library documentation, protobuf definitions and other SDKs. + + ### ⏱️ Timeline & Workflow + - **Typical time:** ~1 Week / ~8 hours. + 🧩 Difficulty: Manageable but requires investigation and testing + 🎓 Best for: Beginner contributors + + - 🔴 Completing a beginner issue in 1–3 hours is a **red flag**. + + > [!TIP] + > **Suggested:** share your proposed implementation approach as a comment before writing code to get early feedback and avoid wasted effort. + + - type: textarea + id: testing + attributes: + label: 🧪 Testing Requirements + description: | + Edit this section to set clear expectations for testing. + What types of tests should they write? How should they run them? What should they link in their PR as evidence? + value: | + > [!IMPORTANT] + > At the beginner level, **testing will be necessary but a small component**. + > The methods you implement should be verified (happy path at least) + + How you test depends on the type of change. + + **Source code changes (e.g. in `src/`):** + - Write basic unit tests + - Run tests locally: `uv run pytest tests/unit/_test.py -v` + - Integration tests will run automatically when you push + + **GitHub Actions / workflow changes (e.g. in `.github/`):** [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/github-action-workflows.md) + - Test by merging to your fork's `main` and simulating the scenario + + **Example script changes (e.g. in `examples/`):** + - Run the example script and confirm output matches expected behavior + - You will need a [Hedera Portal](https://portal.hedera.com/) account for testnet credentials + + + - type: textarea + id: quality_standards + attributes: + label: 🛡️ Quality & Review Standards + description: | + Adapt as needed. The quality standards expected for beginner PRs. + value: | + Beginner PRs must be **working and follow best practices**. + + 1. **Working:** The implementation must solve the problem and meet the acceptance criteria. + 2. **SDK Best Practices:** The solution should fit well with existing SDK patterns and abstractions. + 3. **Testing:** basic unit and possibly integration tests to demonstrate the implementation works as expected. + + **⚠️ Breaking changes** + - Watch for any changes that could impact existing users, even if they seem small. + + **🤖 AI notes** + - AI has gaps or incorrect understanding of repo-specific patterns, logic, and protobuf schemes. + + - type: textarea + id: acceptance + attributes: + label: ✅ PR Quality Checklist + description: | + Adapt as needed. These are the standards the contributor must meet before opening a PR. + value: | + Before opening your PR, confirm: + + - [ ] I have spent the majority of my time researching the problem and building my understanding of the codebase and methods. + - [ ] My implementation works and is tested (if applicable). + - [ ] Every line of code is personally understood and explainable. + - [ ] My changes address what the issue asked for — nothing more, nothing less + - [ ] I have applied appropriate linting, code quality, and formatting tools used in this repo. + + Before requesting a review, confirm: + - [ ] I did not modify files unrelated to this issue + - [ ] Clean git history — no rebase artifacts, merge commits, or unrelated files + - [ ] My commits are signed: `git commit -S -s -m "chore: description"` — [Signing guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/signing.md) + - [ ] I added a CHANGELOG.md entry (if appropriate) under `[Unreleased]` — [Changelog guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/changelog-guide.md) + - [ ] I have included appropriate tests and all CI checks pass. + + - type: textarea + id: contribution_steps + attributes: + label: 📋 Workflow quick reference + description: | + Pre-filled reminders. + value: | + You have done this before — here are the links if you need them: + + | Step | Guide | + |------|-------| + | Claim this issue | Comment `/assign` below | + | Sync with main | [Rebasing guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/rebasing.md) | + | Open a PR and link this issue | [PR guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/contributor-workflow.md) | + | Resolve merge conflicts | [Merge conflicts guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/merge_conflicts.md) | + + - type: textarea + id: resources + attributes: + label: 📚 Resources & Support + value: | + **🆘 Stuck?** + > [!TIP] + > **Comment on this issue:** and describe what you have tried. A maintainer will respond. + + **Python SDK References:** + - [Windows Setup Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/setup_windows.md) — platform-specific installation steps + - [Pylance guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/pylance.md) — inline type checking during development + - [Running examples](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/examples.md) + - [Browse closed beginner PRs](https://github.com/hiero-ledger/hiero-sdk-python/pulls?q=is%3Apr+is%3Amerged+label%3Abeginner) — see how others did it + + **References:** + - [Hedera Protobufs](https://github.com/hashgraph/hedera-protobufs) + - [Community Calls](https://zoom-lfx.platform.linuxfoundation.org/meetings/hiero?view=week) + - [Discord](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/discord.md) diff --git a/.github/ISSUE_TEMPLATE/02_good_first_issue.yml b/.github/ISSUE_TEMPLATE/02_good_first_issue.yml deleted file mode 100644 index 85aab60d9..000000000 --- a/.github/ISSUE_TEMPLATE/02_good_first_issue.yml +++ /dev/null @@ -1,422 +0,0 @@ -name: Good First Issue Template -description: Create a Good First Issue for new contributors -title: "[Good First Issue]: " -labels: ["Good First Issue"] -assignees: [] -body: - - type: markdown - attributes: - value: | - --- - ## **Thanks for creating a good first issue!** 😊 - - We truly appreciate your time and effort, welcome! - This template is designed to help you create a Good First Issue (GFI) : a small, well-scoped task that helps new contributors learn the codebase and workflow. - --- - - - type: textarea - id: intro - attributes: - label: 🆕🐥 Newcomer Friendly - description: Who is this issue for? - value: | - This **[Good First Issue](https://github.com/hiero-ledger/hiero-sdk-python/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22Good%20First%20Issue%22%20no%3Aassignee)** is a guided, well-scoped task intended for new human contributors to the Hiero Python SDK. - - #### What you’ll do - - ✅ understand how the repository is structured - - ✅ practice the standard contribution workflow - - ✅ submit and merge a pull request - - #### Support - A maintainer or mentor actively monitors this issue and will help **guide it to completion**. - - > [!IMPORTANT] - > **This issue does not require prior domain knowledge.** - > - > - No Hiero or Hedera experience needed - > - No distributed ledger background required - > - **Basic Python and Git are sufficient** - - > [!NOTE] - > ⏱️ **Typical time to complete:** 30–90 minutes (once setup is done) - > 🧩 **Difficulty:** Small, well-contained change - > 🎓 **Best for:** New contributors - - **🏁 Completion** - When this issue is complete, you will have: - - - ✅ Solved a real issue - - ✅ A merged pull request in the Hiero Python SDK - - ✅ Your name in the project history - - ✅ Confidence to take on larger issues next - - - type: markdown - attributes: - value: | - > [!IMPORTANT] - > #### 📋 Good First Issue (GFI) Guidelines - > A **Good First Issue [Guidelines](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/maintainers/good_first_issue_guidelines.md)** is a small, well-scoped task that helps new contributors get familiar with the codebase and contribution workflow. - > - > **Often a good fit for Good First Issues:** - > - Spelling or grammar fixes - > - Small documentation formatting improvements - > - Purely structural refactors of examples - > - Minor, clearly described edits to docstrings or comments - > - Light file moves or renames for clarity - > - Applying formatting tools (e.g. `black`, `ruff`) in limited areas - > - > More involved tasks may be a better fit for **Beginner**, **Intermediate**, or **Advanced** issues. - > - > If you’re unsure, the **Good First Issue Candidate Template** can be used. - - - type: textarea - id: issue - attributes: - label: 👾 Issue description - description: | - Describe the issue in a way that’s easy for new contributors to understand. - Briefly explain why this change is useful or needed, even if the impact seems small. - Please avoid assuming prior knowledge of the language, codebase, or Hiero, as Good First Issues are often surfaced to new developers. - Links to relevant documentation or code are very welcome. - value: | - Edit here. Example provided below. - validations: - required: true - - - type: markdown - attributes: - value: | - - ## 👾 Description of the issue - Example - - The example for Token Associate Transaction located at examples/tokens/token_associate_transaction.py can be improved. It correctly illustrates how to associate a token, however, it does so all from one function main() - - As everything is grouped together in main(), it is difficult for a user to understand all the individual steps required to associate a token. - - For example: - ```python - - def run_demo(): - """Monolithic token association demo.""" - print(f"🚀 Connecting to Hedera {network_name} network!") - client = Client(Network(network_name)) - operator_id = AccountId.from_string(os.getenv("OPERATOR_ID", "")) - operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY", "")) - client.set_operator(operator_id, operator_key) - print(f"✅ Client ready (operator {operator_id})") - - test_key = PrivateKey.generate_ed25519() - receipt = ( - AccountCreateTransaction() - .set_key(test_key.public_key()) - .set_initial_balance(Hbar(1)) - .set_account_memo("Test account for token association demo") - .freeze_with(client) - .sign(operator_key) - .execute(client) - ) - if receipt.status != ResponseCode.SUCCESS: - raise Exception(receipt.status) - account_id = receipt.account_id - print(f"✅ Created test account {account_id}") - - # Create tokens - tokens = [] - for i in range(3): - try: - receipt = ( - TokenCreateTransaction() - .set_token_name(f"DemoToken{i}") - .set_token_symbol(f"DTK{i}") - .set_decimals(2) - .set_initial_supply(100_000) - .set_treasury_account_id(operator_id) - .freeze_with(client) - .sign(operator_key) - .execute(client) - ) - if receipt.status != ResponseCode.SUCCESS: - raise Exception(receipt.status) - token_id = receipt.token_id - tokens.append(token_id) - print(f"✅ Created token {token_id}") - except Exception as e: - print(f"❌ Token creation failed: {e}") - sys.exit(1) - - # Associate first token - try: - TokenAssociateTransaction().set_account_id(account_id).add_token_id(tokens[0]).freeze_with(client).sign(test_key).execute(client) - print(f"✅ Token {tokens[0]} associated with account {account_id}") - except Exception as e: - print(f"❌ Token association failed: {e}") - sys.exit(1) - ``` - - - type: textarea - id: solution - attributes: - label: 💡 Proposed Solution - description: | - Describe what a good solution would look like. - Keep this high-level and easy to understand. Implementation details can go in the subsequent Implementation Steps section. - value: | - Edit here. Example provided below. - validations: - required: true - - - type: markdown - attributes: - value: | - - ## 💡 Solution - Example - - For the TokenAssociateTransaction example, the solution is to split the monolithic main() function for illustrating TokenAssociateTransaction into separate smaller functions which are called from main(). - Such as: - - Setting up the client - - Creating an account - - Creating a token - - Associating the account to the token - - - type: textarea - id: implementation - attributes: - label: 🛠️ Implementation Steps - description: | - To make this issue easy to pick up and complete, please include: - - Which files need to be changed or added - - Any functions, classes, or modules involved - - The complete steps to implement the solution - - What the final result or output should look like - - Links to relevant documentation or code (if helpful) - For good first issues, please keep this as guided and clear as possible. - value: | - Edit here. Example provided below. - - --- - validations: - required: true - - - type: markdown - attributes: - value: | - - #### 👩‍💻 Implementation - Example - - To break down the monolithic main function, you need to: - - [ ] Extract the Key Steps (set up a client, create a test account, create a token, associate the token) - - [ ] Copy and paste the functionality for each key step into its own function - - [ ] Pass to each function the variables you need to run it - - [ ] Call each function in main() - - [ ] Ensure you return the values you'll need to pass on to the next step in main - - [ ] Ensure the example still runs and has the same output! - - For example: - ```python - - def setup_client(): - """Initialize and set up the client with operator account.""" - - def create_test_account(client, operator_key): - """Create a new test account for demonstration.""" - - def create_fungible_token(client, operator_id, operator_key): - """Create a fungible token for association with test account.""" - - def associate_token_with_account(client, token_id, account_id, account_key): - """Associate the token with the test account.""" - - def main(): - client, operator_id, operator_key = setup_client() - account_id, account_private_key = create_test_account(client, operator_key) - token_id = create_fungible_token(client, operator_id, operator_key) - associate_token_with_account(client, token_id, account_id, account_private_key) - ``` - - - type: textarea - id: setup_steps - attributes: - label: 📋 Step-by-Step Setup Guide - description: Provide a step-by-step setup guide for new contributors - value: | - #### Suggestions: - - [ ] Visual Studio (VS) Code: [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/01_supporting_infrastructure.md) - - - [ ] GitHub Desktop: [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/01_supporting_infrastructure.md) - - - [ ] Hedera Testnet Account with root .env file: [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/setup/03_setting_up_env.md) - - - [ ] Create a GPG key linked to GitHub: [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/signing.md) - - #### Setup the Hiero Python SDK for development - - [ ] **Fork** Create an online and local copy of the repository: [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/02_forking_python_sdk.md) - - - [ ] **Connect** origin with upstream: [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/03_staying_in_sync.md) - - - [ ] **Install Packages** and protobufs: [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/setup/02_installing_hiero_python_sdk.md) (or [Windows Setup Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/setup_windows.md) for Windows users) - - - [ ] **Sync Main** pull any recent upstream changes: [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/rebasing.md) - - You are set up! 🎉 - validations: - required: true - - - type: textarea - id: contribution_steps - attributes: - label: 📋 Step-by-step contribution guide - description: Provide a contribution workflow suitable for new contributors. - value: | - #### ✅ Get ready - - [ ] **Claim the issue:** comment `/assign`: [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/04_assigning_issues.md). Pull requests created without being assigned will be automatically closed. - - - [ ] **Double check the Issue and AI plan:** carefully re-read the issue description and the CodeRabbit AI plan - - - [ ] **Ask questions early:** ask on [Discord](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md), your `@mentor` (Python SDK help) and the `@good_first_issue_support_team` (setup and workflow help) - - - [ ] **Sync with main:** pull the latest upstream changes [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/rebasing.md) - - - [ ] 💡 Tip: Before coding, leave a short comment describing what you plan to change. We’ll confirm you’re on the right track. - - #### 🛠️ Solve the Issue - - [ ] **Create a branch from `main`:** [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/05_working_branches.md) - - - [ ] **Implement the solution**: follow the implementation steps in the issue description. - - - [ ] **Commit with DCO and GPG signing:** commit changes using: `git commit -S -s -m "chore: your message"`, [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/signing.md) - - - [ ] **Add a `.CHANGELOG.md` entry:** under the appropriate **[UNRELEASED]** section and commit as `git commit -S -s -m "chore: changelog entry"` [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/changelog_entry.md) - - #### 🚀 Create the pull request - - [ ] **Push your commits:** push your branch to your fork `git push origin your-branch-name` - - - [ ] **Open a pull request:** [here](https://github.com/hiero-ledger/hiero-sdk-python/pulls) [guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/11_submit_pull_request.md) - - - [ ] **Complete the PR description:** briefly describe your changes, [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/11_submit_pull_request.md) - - - [ ] **Link the Issue:** link the issue the PR solves in the PR description, [Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/how_to_link_issues.md). Pull requests created without a linked issue will be automatically closed. - - - [ ] **Submit the pull request:** click `**Create pull request**` 🎉 - - validations: - required: true - - - type: textarea - id: acceptance-criteria - attributes: - label: ✅ Acceptance criteria - description: | - Edit or expand this checklist with what is required to merge a pull request for this issue. - value: | - To be able to close this issue, the following criteria must be met: - - - [ ] **The issue is solved:** I’ve carefully read and implemented the issue requirements - - - [ ] **I did not add extra changes:** I did not modify anything beyond what is described in the issue - - - [ ] **Behavior:** All other existing features continue to work as before - - - [ ] **Checks and feedback:** All checks pass and any requested changes have been made - validations: - required: true - - - type: textarea - id: getting_help - attributes: - label: 🧭 Getting help if you’re stuck - description: How to get support while working on this issue. - value: | - If questions come up, don’t spend more than **20 minutes** blocked. - - > [!TIP] - > - > - Comment on this issue and tag `@good_first_issue_support_team` or `@mentor_name` - > - Ask for help in [Discord](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md) - > - - --- - - - type: textarea - id: pr_expectations - attributes: - label: 🤔 What to expect after submitting a PR - description: Explain what happens after a pull request is opened. - value: | - Once you open a pull request, here’s what happens next. - - **🤖 1. Automated checks** - A small set of automated checks must pass before merging (signing, changelog, tests, examples, code quality). - Open any failed check to see details. - - --- - - **🤝 2. AI feedback (CodeRabbit)** - CodeRabbit AI may suggest improvements or flag issues. - Feedback is advisory — use what’s relevant and helpful. - - --- - - **😎 3. Team review** - A Python SDK team member reviews your PR within **1–3 days**. - You may be asked to make changes or your PR may be approved. - Approved PRs are usually merged within **one day**. - - - **🔄 Merge conflicts (sometimes)** - Conflicts can happen and are normal as the SDK updates. - Changelog conflicts can be resolved online in the PR in the merge editor, accepting both entries - Others may require **[rebasing](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/rebasing.md)**. - - --- - - validations: - required: true - - - type: textarea - id: ai_usage_guidelines - attributes: - label: 🤖 AI usage guidelines - description: Guidance on using AI tools responsibly for this issue. - value: | - Humans are welcome to use AI tools while working on this issue but we do not accept AI bot-authored PRs. - - **Use AI responsibly:** - - review suggestions carefully - - apply changes incrementally - - test as you go - - If in doubt, ask — maintainers are happy to help. - - - type: textarea - id: information - attributes: - label: 🤔 Additional Help - description: Provide any extra resources or context for contributors to solve this good first issue - value: | - #### First Points of Contact: - - [Discord](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md) - - Comment with `@mentor_name` (for Python SDK questions) - - Comment with `@hiero-ledger/hiero-sdk-good-first-issue-support` (for setup and workflow questions) - The more you ask, the more you learn and so do we! - - #### Documentation: - - [README.md](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/README.md) - - [CONTRIBUTING.md](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/CONTRIBUTING.md) - - [Project Structure](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/setup/project_structure.md) - - [DCO and Verified Signing Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/signing.md) - - [Changelog Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/changelog_entry.md) - - [Rebasing Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/rebasing.md) - - [Merge Conflicts Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/merge_conflicts.md) - - [Linking Issues Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/how_to_link_issues.md) - - [Workflow Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/workflow.md) - - - [Pin Github Actions Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/how-to-pin-github-actions.md) - - [Running Examples](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/examples.md) - - [Testing on Forks](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/testing_forks.md) - - - [General Training](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training) - - [General SDK Developer Docs](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers) - - #### Calls: - - Get hands-on-help by our expert team at our [Office Hours](https://zoom-lfx.platform.linuxfoundation.org/meeting/99912667426?password=5b584a0e-1ed7-49d3-b2fc-dc5ddc888338) - - Learn, raise issues and provide feedback at [Community Calls](https://zoom-lfx.platform.linuxfoundation.org/meeting/92041330205?password=2f345bee-0c14-4dd5-9883-06fbc9c60581) diff --git a/.github/ISSUE_TEMPLATE/03-intermediate-issue.yml b/.github/ISSUE_TEMPLATE/03-intermediate-issue.yml new file mode 100644 index 000000000..ce861a3f5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03-intermediate-issue.yml @@ -0,0 +1,270 @@ +name: Intermediate Issue Template +description: Create a well-documented issue for contributors ready to own larger tasks independently +labels: ["intermediate"] +assignees: [] +body: + + - type: markdown + attributes: + value: | + --- + ## **Thanks for creating an intermediate issue!** 😊 + + Intermediate issues are complex tasks requiring significant independent research. + + Read more: + > [Applying Intermediate Label](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-maintainers/labelling-issues-examples.md) + > [Issue Progression System](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-maintainers/README.md) + --- + + - type: textarea + id: intro + attributes: + label: 🧑‍💻 Intermediate Issue + description: | + Adapt as needed. Welcome message. + value: | + Welcome! This is an **[Intermediate Issue](https://github.com/issues?q=is%3Aopen%20is%3Aissue%20org%3Ahiero-ledger%20archived%3Afalse%20no%3Aassignee%20(label%3A%22intermediate%22%20OR%20label%3A%22skill%3A%20intermediate%22)%20(repo%3Ahiero-ledger%2Fhiero-sdk-cpp%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-swift%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-python%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-js%20OR%20repo%3Ahiero-ledger%2Fhiero-website))** touching core SDK architecture. + + 🏁 **When this issue is complete, you will have:** + + ✅ Researched across multiple files and modules + ✅ Owned implementation decisions + ✅ Written thorough, meaningful tests + ✅ Delivered a clean, review-ready pull request + + - type: textarea + id: problem + attributes: + label: 🐞 Problem Description + description: | + Clearly articulate the problem and its impact. + Assume the reader can navigate src/, tests/, and examples/. + Explain: what is wrong or missing, where it lives, and why it matters. + value: | + + validations: + required: true + + - type: textarea + id: solution + attributes: + label: 💡 Expected Solution + description: | + Briefly state what the contributor should build or fix. + This should be a short summary of the expected outcome — not a detailed implementation guide. + value: | + + validations: + required: true + + - type: textarea + id: research + attributes: + label: 🔍 Background Research + description: | + Point the contributor toward relevant files, similar code, protobuf definitions, or packages to study. + This is the most important, "investigate before you code" step. + value: | + + + + + + + validations: + required: true + + - type: textarea + id: implementation + attributes: + label: 🛠️ Implementation Notes + description: | + Guide the quality of their work with clear constraints. + value: | + + + + + validations: + required: true + + - type: textarea + id: domains + attributes: + label: 🔬 Technical Domains + description: | + Adapt as needed. Technical areas this issue touches to help developers self-select based on expertise. + value: | + + + - [ ] **Intermediate to Advanced Programming** (Object oriented design, inheritance, complex type systems) + - [ ] **File Specific Knowledge** (request → serialization → execution → response mapping) + - [ ] **Backward Compatibility** (preserving method signatures, defaults, and return types) + - [ ] **Protobuf Alignment** (reading `.proto` files, `_to_proto()` / `_from_proto()` correctness) + - [ ] **Testing** (unit, integration, mocking, test coverage for edge cases and failure modes) + + + - type: textarea + id: intermediate_contributors + attributes: + label: 🧠 Intermediate Contributors — Prerequisites & Expectations + description: | + Adapt as needed. Concrete requirements before claiming this intermediate issue. Adapt as needed. + value: | + > [!CAUTION] + > **Intermediate issues are high-risk. We expect more than just a 'working solution' and will recommend beginner issues if the PR does not meet these standards.** + + ### 🏁 Concrete Prerequisites + - **Advanced Programming Language:** Higher level intermediate or advanced programming language. + - **Expertise:** Strong understanding of files related to this issue (research before claiming!). + - **Proven History:** Successfully completed **≥ 5 beginner issues** in this repo. + + > [!NOTE] + > **CI/CD Exception:** For issues focused on **GitHub Actions / Workflows**, the repo-specific thresholds above may be waived if the contributor demonstrates intermediate-level proficiency in CI/CD. + + If this feels like too big a step, that is completely fine — try a [Beginner Issue](https://github.com/issues?q=is%3Aopen%20is%3Aissue%20org%3Ahiero-ledger%20archived%3Afalse%20no%3Aassignee%20(label%3A%22beginner%22%20OR%20label%3A%22skill%3A%20beginner%22)%20(repo%3Ahiero-ledger%2Fhiero-sdk-cpp%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-swift%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-python%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-js%20OR%20repo%3Ahiero-ledger%2Fhiero-website)) first or a different **[Intermediate Issue](https://github.com/issues?q=is%3Aopen%20is%3Aissue%20org%3Ahiero-ledger%20archived%3Afalse%20no%3Aassignee%20(label%3A%22intermediate%22%20OR%20label%3A%22skill%3A%20intermediate%22)%20(repo%3Ahiero-ledger%2Fhiero-sdk-cpp%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-swift%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-python%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-js%20OR%20repo%3Ahiero-ledger%2Fhiero-website))**. You can always come back when you are ready. + + ### ⚠️ AI Usage Policy + + - Using AI to generate code for Intermediate issues is **strictly discouraged** + - Using AI as the main source of research is **strictly discouraged**, refer to language and library documentation, protobuf definitions and other SDKs. + + ### ⏱️ Timeline & Workflow + - **Typical time:** ~2 weeks / ~25 hours. + - 🔴 Completing an intermediate issue in 1–3 days is a **red flag**. + + > [!TIP] + > **Suggested:** share your proposed implementation approach as a comment before writing code to get early feedback and avoid wasted effort. + + - type: textarea + id: testing + attributes: + label: 🧪 Testing Requirements + description: | + Edit this section to set clear expectations for testing. + What types of tests should they write? How should they run them? What should they link in their PR as evidence? + value: | + How you test depends on the type of change. + + > [!IMPORTANT] + > At the intermediate level, **testing is a major component**. + > Each method you implement should be verified. Tests should cover happy paths, + > edge cases, and error handling. + + **Source code changes (e.g. in `src/`):** + - Write unit tests covering happy path, edge cases, and error handling + - Run tests locally: `uv run pytest tests/unit/_test.py -v` + - Verify naming, types, and field ordering match [protobuf definitions](https://github.com/hashgraph/hedera-protobufs/tree/main/services) + - Check consistency with similar classes already in the SDK — use the same patterns + - Integration tests will run automatically when you push + + **GitHub Actions / workflow changes (e.g. in `.github/`):** [Guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/github-action-workflows.md) + - Test by merging to your fork's `main` and simulating the scenario + - Create test issues or PRs as evidence and link them in your PR + - Include screenshots of workflow runs where applicable + + **Example script changes (e.g. in `examples/`):** + - Run the example script and confirm output matches expected behavior + - Compare your example with similar existing examples for consistency + - You will need a [Hedera Portal](https://portal.hedera.com/) account for testnet credentials + + + - type: textarea + id: quality_standards + attributes: + label: 🛡️ Quality & Review Standards + description: | + Adapt as needed. The quality standards expected for intermediate PRs. + value: | + Intermediate PRs must be **"working, maintainable, and aligned with SDK architecture."** + + 1. **Working:** The implementation must solve the problem and meet the acceptance criteria. + 2. **Maintainable:** Code should be clear and concise enough for others to understand and debug without your assistance. + 3. **SDK Alignment:** The solution should fit well with existing SDK patterns and abstractions. + 4. **Backward Compatibility:** Public API signatures must be preserved. + 5. **Comprehensive Testing:** Must include unit and integration tests covering all new logic paths, edge cases, and failure modes. AI generated tests based on AI generated code is grounds for immediate rejection. + + **⚠️ Breaking changes** + - Before changing any function signature, return type, or public API — stop and check + - If a breaking change is unavoidable: get explicit maintainer approval **before** implementing + - All existing tests should pass as-is. + + **🤖 AI notes** + - AI often has outdated real-world knowledge: it will not be able to identify current packages. + - AI has gaps or incorrect understanding of repo-specific patterns, logic, and protobuf schemes. + + - type: textarea + id: acceptance + attributes: + label: ✅ PR Quality Checklist + description: | + Adapt as needed. These are the standards the contributor must meet before opening a PR. + value: | + Before opening your PR, confirm: + + - [ ] I have spent the majority of my time researching the problem and solution space extensively, including reviewing relevant code, documentation, and external resources. + - [ ] My implementation works but is also of high quality - including maintainability, readability, and architectural fit. + - [ ] I have checked for breaking changes - no public APIs regress. + - [ ] The system design fits with current Hiero SDK architectural approaches. + - [ ] Every line of code is personally understood and explainable. + + Before requesting a review, confirm: + - [ ] I have reviewed the diff line by line. + - [ ] My implementation fully addresses the problem described above. + - [ ] I did not modify files unrelated to this issue + - [ ] Clean git history — no rebase artifacts, merge commits, or unrelated files + - [ ] My commits are signed: `git commit -S -s -m "chore: description"` — [Signing guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/signing.md) + - [ ] I added a CHANGELOG.md entry (if appropriate) under `[Unreleased]` — [Changelog guide](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/changelog-guide.md) + - [ ] I have verified naming, types, and field ordering against pinned Protobufs. + - [ ] I have applied appropriate linting, code quality, and formatting tools used in this repo. + - [ ] I have included appropriate tests and all CI checks pass. + - [ ] Double and triple check — intermediate PRs are time-consuming to review + + - type: textarea + id: contribution_steps + attributes: + label: 📋 Workflow quick reference + description: | + PRE-FILLED REFERENCE TABLE. LEAVE AS-IS. + value: | + You know the workflow — here are the links if you need them: + + | Step | Guide | + |------|-------| + | Claim this issue | Comment `/assign` below | + | Sync with main | [Rebasing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/rebasing.md) | + | Open a PR and link this issue | [PR guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/11_submit_pull_request.md) · [Linking guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/how_to_link_issues.md) | + | Resolve merge conflicts | [Merge conflicts guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/merge_conflicts.md) | + | Testing | [Testing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/testing.md) | + validations: + required: false + + - type: textarea + id: resources + attributes: + label: 📚 Resources & Support + value: | + **🆘 Stuck?** + > [!TIP] + > **Comment on this issue:** and describe what you have tried. A maintainer will respond. + + **Project references:** + - [Project structure](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/setup/project_structure.md) + - [CONTRIBUTING.md](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/CONTRIBUTING.md) + - [Browse closed intermediate PRs](https://github.com/hiero-ledger/hiero-sdk-python/pulls?q=is%3Apr+is%3Amerged+label%3Aintermediate) — see how others did it + - [Pylance guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/pylance.md) + + **Protobuf references:** + - [Hedera Protobufs](https://github.com/hashgraph/hedera-protobufs/tree/main/services) — source of truth for field names, types, and ordering + - [Protobuf language guide](https://protobuf.dev/programming-guides/proto3/) + + **Python references:** + - [Python official docs](https://docs.python.org/3/) + - [Data model (dunder methods)](https://docs.python.org/3/reference/datamodel.html) + - [Type hints](https://docs.python.org/3/library/typing.html) + - [unittest.mock](https://docs.python.org/3/library/unittest.mock.html) — for mocking in tests + + **Community:** + - [Community Calls](https://zoom-lfx.platform.linuxfoundation.org/meetings/hiero?view=week) + - [Discord](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/discord.md) diff --git a/.github/ISSUE_TEMPLATE/03_beginner_issue.yml b/.github/ISSUE_TEMPLATE/03_beginner_issue.yml deleted file mode 100644 index 37c5e282e..000000000 --- a/.github/ISSUE_TEMPLATE/03_beginner_issue.yml +++ /dev/null @@ -1,305 +0,0 @@ -name: Beginner Issue Template -description: Create a beginner issue for contributors ready to work more independently -title: "[Beginner]: " -labels: ["beginner"] -assignees: [] -body: - - type: markdown - attributes: - value: | - --- - **For issue creators:** Beginner issues sit between Good First Issues and Intermediate issues. - The contributor should need to read some code, look something up, or make a small decision — - but not design a solution from scratch. - - If the task can be completed by copy-pasting instructions, it belongs in a Good First Issue. - If it requires understanding multiple interacting systems, it belongs in Intermediate. - - For detailed guidance, see the - [Beginner Issue Guidelines](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/maintainers/beginner_issues_guidelines.md). - - **All links in this template must be absolute URLs** (starting with `https://`). - Relative links break when rendered in GitHub issues. - - --- - - - type: textarea - id: before_you_start - attributes: - label: 🐥 Beginner Friendly - description: | - THIS SECTION IS PRE-FILLED. LEAVE AS-IS UNLESS YOU NEED TO ADD ISSUE-SPECIFIC PREREQUISITES. - value: | - Welcome! This Beginner Issue is designed to be a natural next step after good first issues. - - It nudges you to: - - - Research existing patterns - - Own more implementation decisions - - Test your changes - - **Difficulty:** - - - Requires knowledge of our workflow - - Beginner to intermediate Python and Git - - Beginner DLT/Blockchain experience - - > [!IMPORTANT] - > We recommend completing at least 3 good first issues before attempting a beginner issue. - - > [!TIP] - > **You should be comfortable with:** - > - Forking, branching, committing (with DCO + GPG signing), and opening a pull request without a tutorial - > - Reading beginner Python code you did not write and following its patterns - > - Handling simple merge conflicts in CHANGELOG.md - > - Keeping your fork up to date with main by [rebasing](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/rebasing.md) - > - Looking things up on your own before asking for help - - If any of that feels unfamiliar, good first issues might still be the most rewarding path - for you right now — - [find one here](https://github.com/hiero-ledger/hiero-sdk-python/issues?q=is%3Aissue+state%3Aopen+label%3A%22Good+First+Issue%22+no%3Aassignee). - You can always come back when you are ready. - - **Support:** A maintainer actively monitors this issue and will help guide your implementation ideas. - - ⏱️ Typical time to complete: 4-6 hours - 🧩 Difficulty: Manageable but requires investigation and testing - 🎓 Best for: Beginner contributors - - --- - - 🏁 **When this issue is complete, you will have:** - - ✅ Improved your research abilities - ✅ Improved your knowledge of the codebase - ✅ Owned implementation decisions - ✅ Created basic tests - validations: - required: false - - - type: textarea - id: task - attributes: - label: 👾 Description of the issue - description: | - DESCRIBE WHAT NEEDS TO CHANGE AND WHY. - INCLUDE WHICH FILE(S) TO LOOK AT, WHAT THE CURRENT BEHAVIOR IS, AND WHAT THE DESIRED BEHAVIOR IS. - DO NOT WRITE A STEP-BY-STEP RECIPE. POINT THEM TOWARD THE RIGHT AREA AND LET THEM FIGURE OUT THE APPROACH. - IF A COPY-PASTE RECIPE WOULD SUFFICE, THIS IS A GOOD FIRST ISSUE, NOT A BEGINNER ISSUE. - PROVIDE ENOUGH CONTEXT SO THE CONTRIBUTOR CAN JUDGE WHETHER THEY ARE READY FOR THIS ISSUE. - CONSIDER ADDING: EXPECTED TIME (e.g. 2-4 HOURS), RELEVANT LABELS, AND A NOTE TO CHECK THE CODERABBIT AI PLAN. - value: | - _Replace this with the task description._ - - **Before claiming:** - - Check the issue labels to see what area this touches - - Read the CodeRabbit AI plan (posted as a comment) for a rough implementation overview - - If this feels like too big a step, that is fine — try another beginner issue or a Good First Issue first - validations: - required: true - - - type: markdown - attributes: - value: | - - ## Example — Description of the issue - - The `AccountId` class in `src/hiero_sdk_python/account/account_id.py` has a `__str__()` method, - but its `__repr__()` still uses the default object representation: - - ```python - >>> repr(account_id) - '' - ``` - - That is not useful when debugging. We want `repr()` to show the actual values: - - ```python - >>> repr(account_id) - 'AccountId(shard=0, realm=0, num=1234)' - ``` - - The class is initialized with `shard`, `realm`, `num`, and an optional `alias_key`. - Look at how other ID classes in the SDK handle `__repr__()` for the expected pattern. - - - type: textarea - id: solution - attributes: - label: 💡 Solution - description: | - BRIEFLY STATE WHAT THE CONTRIBUTOR SHOULD BUILD OR FIX. - THIS SHOULD BE A SHORT SUMMARY OF THE EXPECTED OUTCOME — NOT A DETAILED IMPLEMENTATION GUIDE. - EXAMPLE: "Add a __repr__ method to AccountId that shows shard, realm, and num." - value: | - _Replace this with a brief solution summary._ - validations: - required: true - - - type: markdown - attributes: - value: | - - ## Example — Solution - - Add a `__repr__()` method to `AccountId` that returns a string like - `AccountId(shard=0, realm=0, num=1234)`, including `alias_key` when set. - - - type: textarea - id: research - attributes: - label: 🔍 Background Research - description: | - POINT THE CONTRIBUTOR TOWARD RELEVANT FILES, SIMILAR CODE, PYTHON DOCS, OR PATTERNS TO STUDY. - THIS IS THE "INVESTIGATE BEFORE YOU CODE" STEP. - THINK: WHAT WOULD A COLLEAGUE TELL YOU IF YOU ASKED "WHERE DO I START?" - INCLUDE LINKS TO PYTHON DOCUMENTATION IF THE TASK INVOLVES A LANGUAGE CONCEPT. - value: | - _Replace this with research pointers._ - validations: - required: true - - - type: markdown - attributes: - value: | - - ## Example — Background Research - - - Open `src/hiero_sdk_python/account/account_id.py` and read the `__init__` and `__str__` methods - - Look at how `TopicId` and `ContractId` implement `__repr__()` — use the same pattern - - Python docs on `__repr__`: https://docs.python.org/3/reference/datamodel.html#object.__repr__ - - Ask yourself: what information would help you identify this object during debugging? - - - type: textarea - id: implementation - attributes: - label: 🛠️ Implementation - description: | - DESCRIBE WHAT THE CONTRIBUTOR SHOULD BUILD AFTER COMPLETING THEIR RESEARCH. - KEEP IT HIGH-LEVEL — THEY SHOULD FIGURE OUT THE DETAILS FROM THEIR RESEARCH. - value: | - _Replace this with implementation guidance._ - validations: - required: true - - - type: markdown - attributes: - value: | - - ## Example — Implementation - - - Implement `__repr__()` on `AccountId` following the pattern you found in similar classes - - Include `alias_key` in the output when the account uses an alias - - Add a test to the existing `AccountId` unit test file - - - type: textarea - id: testing - attributes: - label: 🧪 Testing your changes - description: | - EDIT THIS SECTION TO MATCH THE TYPE OF CHANGE. DELETE CATEGORIES THAT DO NOT APPLY. - value: | - How you test depends on the type of change. - See the [testing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/testing.md) for full details. - - **Source code changes (e.g. Python files in `src/`):** - - Write unit tests and run them locally: `uv run pytest tests/unit/_test.py -v` - - Integration tests will run automatically when you push to your fork or open a PR - - **Example script changes (e.g. files in `examples/`):** - - Run the example script to confirm it works: instructions are at the top of each script - - You will need a [Hedera Portal](https://portal.hedera.com/) account for testnet credentials — see [environment setup](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/setup/environment_setup.md) - - Example changes do not require unit or integration tests - - **Workflow / GitHub Actions changes (e.g. files in `.github/`):** - - Where possible, test by merging to your fork's `main` and simulating the scenario (e.g. creating a test issue or PR) - - Link to any test issues or PRs you created as evidence - - See the [workflow testing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/11_submit_pull_request.md) - validations: - required: false - - - type: textarea - id: acceptance - attributes: - label: ✅ Done Checklist - description: | - EDIT OR ADD CRITERIA SPECIFIC TO THIS ISSUE. THE DEFAULTS COVER THE STANDARD REQUIREMENTS. - value: | - Before opening your PR, confirm: - - - [ ] My changes address what the issue asked for — nothing more, nothing less - - [ ] I tested my changes locally (if relevant — see testing section above) - - [ ] My commits are signed: `git commit -S -s -m "chore: description"` — [Signing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/signing.md) - - [ ] I added a CHANGELOG.md entry under `[Unreleased]` — [Changelog guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/changelog_entry.md) - - [ ] All CI checks pass - validations: - required: true - - - type: textarea - id: contribution_steps - attributes: - label: 📋 Workflow quick reference - description: | - PRE-FILLED REFERENCE TABLE. LEAVE AS-IS. - value: | - You have done this before — here are the links if you need them: - - | Step | Guide | - |------|-------| - | Claim this issue | Comment `/assign` below | - | Sync with main | [Rebasing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/rebasing.md) | - | Open a PR and link this issue | [PR guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/11_submit_pull_request.md) · [Linking guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/how_to_link_issues.md) | - | Resolve merge conflicts | [Merge conflicts guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/merge_conflicts.md) | - validations: - required: false - - - type: textarea - id: ai_tips - attributes: - label: 🤖 Tips for using AI tools - description: | - PRE-FILLED GUIDANCE. LEAVE AS-IS. - value: | - Here is how to get the most out of AI tools: - - - Use [Pylance](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/pylance.md) in VS Code to catch errors and hallucinated methods in real time - - Build your solution in small steps — add print statements or logs to confirm each piece works before moving on - - When AI suggests code, check it against existing examples and similar classes in the SDK — that is the best source of Python SDK-specific patterns. AI sometimes borrows from other SDKs. - - If AI output and SDK patterns disagree, trust the SDK - validations: - required: false - - - type: textarea - id: stuck - attributes: - label: 🆘 Stuck? - description: | - PRE-FILLED GUIDANCE. LEAVE AS-IS. - value: | - Beginner issues can be tricky — it is completely normal to get blocked. Here is what to do: - - - **Comment on this issue** and describe what you have tried. A maintainer will respond. - - **Ask on [Discord](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md)** in #hiero-python-sdk for quick questions. - - **Join [Office Hours](https://zoom-lfx.platform.linuxfoundation.org/meeting/99912667426?password=5b584a0e-1ed7-49d3-b2fc-dc5ddc888338)** (Wednesdays, 2pm UTC) for live, hands-on help with a maintainer — screen sharing welcome. - - Do not spend more than 30 minutes blocked without asking. Asking good questions is a skill too. - validations: - required: false - - - type: textarea - id: resources - attributes: - label: 📚 Resources - description: | - ADD ANY TASK-SPECIFIC RESOURCES BELOW — RELEVANT PYTHON DOCS, STACK OVERFLOW ANSWERS, OR SPECIFIC FILES IN THE CODEBASE. - value: | - **Project references:** - - [Project structure](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/setup/project_structure.md) - - [CONTRIBUTING.md](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/CONTRIBUTING.md) - - [Browse closed beginner PRs](https://github.com/hiero-ledger/hiero-sdk-python/pulls?q=is%3Apr+is%3Amerged+label%3Abeginner) — see how others did it - - **Python references:** - - [Python official docs](https://docs.python.org/3/) - - [Data model (dunder methods)](https://docs.python.org/3/reference/datamodel.html) - - [Type hints](https://docs.python.org/3/library/typing.html) - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/04-advanced-issue.yml b/.github/ISSUE_TEMPLATE/04-advanced-issue.yml new file mode 100644 index 000000000..3cf00bd3a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/04-advanced-issue.yml @@ -0,0 +1,152 @@ +name: Advanced Issue Template +description: For expert contributors tackling architectural, multi-module, or core-logic changes. +labels: ["advanced"] +assignees: [] + +body: + + - type: markdown + attributes: + value: | + --- + ## **Thanks for creating an advanced issue!** 😊 + + Advanced issues are high-ownership, high-impact tasks requiring expert-level architectural understanding and significant independent research. + + Read more: + > [Applying Advanced Label](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-maintainers/labelling-issues-examples.md) + > [Issue Progression System](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-maintainers/README.md) + --- + + - type: textarea + id: intro + attributes: + label: 🧑‍🔬 Advanced Issue + description: | + Adapt as needed. Welcome message. + value: | + Welcome! This is an **[Advanced Issue](https://github.com/issues?q=is%3Aopen%20is%3Aissue%20org%3Ahiero-ledger%20archived%3Afalse%20no%3Aassignee%20(label%3A%22advanced%22%20OR%20label%3A%22skill%3A%20advanced%22)%20(repo%3Ahiero-ledger%2Fhiero-sdk-cpp%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-swift%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-python%20OR%20repo%3Ahiero-ledger%2Fhiero-sdk-js%20OR%20repo%3Ahiero-ledger%2Fhiero-website))** touching core SDK architecture. + + It is designed for expert contributors who have demonstrated deep architectural understanding and a proven track record of high-quality contributions. + + - type: textarea + id: problem + attributes: + label: 🐞 Problem Description + description: | + Clearly articulate the problem and its impact. The solution may be unclear or require significant research. + value: | + + validations: + required: true + + - type: textarea + id: implementation + attributes: + label: 🛠️ Implementation Notes + description: | + Provide implementation context to understand the complexity and possible approaches. + + Add: + - Implementation ideas + - Clarify what is unknown or uncertain about the implementation + - Highlight any key risks or complexities + value: | + + validations: + required: true + + - type: textarea + id: advanced_contributors + attributes: + label: 🧠 Advanced Contributors — Prerequisites & Expectations + description: | + Adapt as needed. Concrete requirements before claiming this advanced issue. Adapt as needed. + value: | + > [!CAUTION] + > **Advanced issues are the highest-risk work in this project. We will reject PRs that do not meet these standards.** + + ### 🏁 Concrete Prerequisites + - **Advanced Language:** Proficient with Python. + - **Expertise:** Deep architectural understanding of `_Executable`, `Transaction`, and `Query` base classes. + - **Proven History:** Successfully completed **≥ 10 intermediate issues** in this repo. + - **Consistency:** **≥ 3–4 months** of active, human-led contributions to this SDK. + + > [!NOTE] + > **CI/CD Exception:** For issues focused on **GitHub Actions / Workflows**, the repo-specific thresholds above may be waived if the contributor demonstrates advanced-level proficiency in CI/CD. + + ### ⚠️ AI Usage Policy + + - Using AI to generate code for Advanced issues is **strictly discouraged** + - AI may be used to help explain file relationships, but cannot be the main source of research. + - Submitting AI-generated or unvalidated code is grounds for **immediate closure** + + ### ⏱️ Timeline & Workflow + - **Typical time:** ~1 month / ~50 hours. + - 🔴 Completing an advanced issue in 1–3 days is a **red flag** and will likely be rejected. + - **Suggested:** Post your proposed architectural approach as a comment and wait for explicit maintainer approval **before writing any code.** + + - type: textarea + id: domains + attributes: + label: 🔬 Technical Domains + description: | + Adapt as needed. Technical areas this issue touches to help developers self-select based on expertise. + value: | + + + - [ ] **API Client Architecture** (request → serialization → execution → response mapping) + - [ ] **Backward Compatibility** (preserving method signatures, defaults, and return types) + - [ ] **Protobuf Alignment** (reading `.proto` files, `_to_proto()` / `_from_proto()` correctness) + - [ ] **State & Immutability** (correct usage of guards like `_require_not_frozen`) + - [ ] **Execution Boundaries** (retry logic, backoff, node selection, gRPC deadlines) + - [ ] **Testing** (unit, integration, mocking, test coverage for edge cases and failure modes) + + - type: textarea + id: quality_standards + attributes: + label: 🛡️ Quality & Review Standards + description: | + Adapt as needed. The quality standards expected for advanced PRs. + value: | + Advanced PRs must be **"safe, maintainable, architecturally sound, and production-ready."** + + 1. **Architectural Fit:** The solution must fit naturally into the existing SDK abstractions. + 2. **Security & Correctness:** Evaluate all logic for injection risks, state corruption, or thread-safety issues. + 3. **Maintainability:** Code must be short and clear enough for any other maintainer to debug without your assistance. + 4. **Backward Compatibility:** Public API signatures must be preserved. If a breaking change is required, it must be explicitly managed through a deprecation cycle. + 5. **Comprehensive Testing:** Must include unit and integration tests covering all new logic paths, edge cases, and failure modes. AI generated tests based on AI generated code is grounds for immediate rejection. + + - type: textarea + id: acceptance + attributes: + label: ✅ PR Quality Checklist + description: | + Adapt as needed. These are the standards the contributor must meet before opening a PR. + value: | + Before opening your PR, the contributor must confirm: + - [ ] I have spent the majority of my time researching the problem and solution space extensively, including reviewing relevant code, documentation, and external resources. + - [ ] I understand the system-wide impact of these changes on affected modules and performance. + - [ ] The system design fits with current Hiero SDK architectural approaches. + - [ ] I have tested my changes extensively against both local and network environments. + - [ ] I have verified naming, types, and field ordering against pinned Protobufs. + - [ ] Every line of code is personally understood and explainable. + + - type: textarea + id: resources + attributes: + label: 📚 Resources & Support + value: | + **References:** + - [Hedera Protobufs](https://github.com/hashgraph/hedera-protobufs) + + **Python SDK References:** + - [SDK Project Structure](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/setup/project_structure.md) + - [Transaction Lifecycle](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/transaction_lifecycle.md) + - [Executable Architecture](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/executable.md) + - [Hedera Protobufs (services)](https://github.com/hashgraph/hedera-protobufs/tree/main/services) + - [Browse closed advanced PRs](https://github.com/hiero-ledger/hiero-sdk-python/pulls?q=is%3Apr+is%3Amerged+label%3Aadvanced) — see how others did it + + **🆘 Stuck?** + - [Community Calls](https://zoom-lfx.platform.linuxfoundation.org/meetings/hiero?view=week) + - [Discord](https://github.com/hiero-ledger/sdk-collaboration-hub/blob/main/guides/issue-progression/for-developers/discord.md) diff --git a/.github/ISSUE_TEMPLATE/04_intermediate_issue.yml b/.github/ISSUE_TEMPLATE/04_intermediate_issue.yml deleted file mode 100644 index eef1a8fe8..000000000 --- a/.github/ISSUE_TEMPLATE/04_intermediate_issue.yml +++ /dev/null @@ -1,369 +0,0 @@ -name: Intermediate Issue Template -description: Create a well-documented issue for contributors ready to own larger tasks independently -title: "[Intermediate]: " -labels: ["intermediate"] -assignees: [] -body: - - type: markdown - attributes: - value: | - --- - **For issue creators:** Intermediate issues sit between Beginner Issues and Advanced Issues. - The contributor should be able to research, implement, and test independently — - but should not need to make architectural decisions or redesign core abstractions. - - If the task can be solved by following examples alone, it belongs in a Beginner Issue. - If it requires cross-module architectural reasoning, it belongs in Advanced. - - **All links in this template must be absolute URLs** (starting with `https://`). - Relative links break when rendered in GitHub issues. - - --- - - - type: textarea - id: before_you_start - attributes: - label: 🧩 Intermediate Contributors - description: | - THIS SECTION IS PRE-FILLED. LEAVE AS-IS UNLESS YOU NEED TO ADD ISSUE-SPECIFIC PREREQUISITES. - value: | - Welcome! This Intermediate Issue is designed for contributors who have completed beginner-level work and are ready to take on more ownership. - - It nudges you to: - - - Research across multiple files and modules - - Own implementation decisions — not just follow a recipe - - Write thorough, meaningful tests - - Deliver a clean, review-ready pull request - - **Prerequisites:** - - > [!IMPORTANT] - > **Before claiming this issue, you must have:** - > - Completed at least 1 Good First Issue **and** 1 Beginner Issue - > - The contribution workflow nailed: signing, rebasing, CHANGELOG, linking issues - > - The ability to operate independently — you may ask high-level architecture questions, but should not need step-by-step guidance - - > [!TIP] - > **You should be comfortable with:** - > - Navigating multiple modules across `src/`, `tests/`, and `examples/` - > - Reading and following [protobuf definitions](https://github.com/hashgraph/hedera-protobufs/tree/main/services) for correct naming, types, and field ordering - > - Writing meaningful tests — not just AI-generated tests of AI-generated code - > - Identifying and avoiding breaking changes to public APIs - > - Self-reviewing your own PR before submitting - - If this feels like too big a step, that is completely fine — try a - [Beginner Issue](https://github.com/hiero-ledger/hiero-sdk-python/issues?q=is%3Aissue+state%3Aopen+label%3Abeginner+no%3Aassignee) - first. You can always come back when you are ready. - - **Support:** A maintainer is available for high-level guidance — implementation approach, test strategy, or design questions. Don't hesitate to ask. - - ⏱️ Typical time to complete: 1 week part-time / 3 days full-time - 🧩 Difficulty: Requires investigation, testing, and polish — most time should go to research and refinement, not coding - 🎓 Best for: Contributors who've completed beginner issues - - --- - - 🏁 **When this issue is complete, you will have:** - - ✅ Researched across multiple files and modules - ✅ Owned implementation decisions - ✅ Written thorough, meaningful tests - ✅ Delivered a clean, review-ready pull request - ✅ Confidence to take on advanced issues - validations: - required: false - - - type: textarea - id: problem - attributes: - label: 🐞 Problem Description - description: | - DESCRIBE THE PROBLEM CLEARLY. - YOU MAY ASSUME THE READER CAN NAVIGATE src/, tests/, AND examples/. - EXPLAIN: WHAT IS WRONG OR MISSING, WHERE IT LIVES, AND WHY IT MATTERS. - INCLUDE RELEVANT FILE PATHS AND LINKS. - CONSIDER ADDING: EXPECTED TIME (e.g. 1-2 WEEKS), RELEVANT LABELS, AND A NOTE TO CHECK THE CODERABBIT AI PLAN. - value: | - _Replace this with the problem description._ - - **Before claiming:** - - Check the issue labels to see what area this touches - - Read the CodeRabbit AI plan (posted as a comment) for a rough implementation overview - - If this feels like too big a step, try a beginner issue first - validations: - required: true - - - type: markdown - attributes: - value: | - - ## Example — Problem Description - - The `TransactionGetReceiptQuery` currently exposes the `get_children()` method, - but the behavior is inconsistent with how child receipts are returned by the Mirror Node. - - In particular: - - The SDK always returns only the parent receipt - - There is no way to opt-in to retrieving child receipts - - Similar query objects already support optional flags to extend the response - - This limitation makes it difficult to inspect scheduled or child transactions - without issuing additional manual queries. - - Relevant files: - - `src/hiero_sdk_python/query/transaction_get_receipt_query.py` - - `examples/query/transaction_get_receipt_query.py` - - - type: textarea - id: solution - attributes: - label: 💡 Expected Solution - description: | - BRIEFLY STATE WHAT THE CONTRIBUTOR SHOULD BUILD OR FIX. - THIS SHOULD BE A SHORT SUMMARY OF THE EXPECTED OUTCOME — NOT A DETAILED IMPLEMENTATION GUIDE. - EXAMPLE: "Add a __repr__ method to AccountId that shows shard, realm, and num." - value: | - _Replace this with a brief solution summary._ - validations: - required: true - - - type: markdown - attributes: - value: | - - ## Example — Expected Solution - - Introduce an optional configuration flag on `TransactionGetReceiptQuery` - that allows callers to explicitly request child receipts. - - The change should: - - Be opt-in (default behavior must remain unchanged) - - Reuse existing response parsing logic where possible - - Avoid introducing breaking API changes - - Example usage: - - ```python - receipt = ( - TransactionGetReceiptQuery() - .set_transaction_id(tx_id) - .set_include_children(True) - .execute(client) - ) - ``` - - - type: textarea - id: research - attributes: - label: 🔍 Background Research - description: | - POINT THE CONTRIBUTOR TOWARD RELEVANT FILES, SIMILAR CODE, PROTOBUF DEFINITIONS, OR PATTERNS TO STUDY. - THIS IS THE "INVESTIGATE BEFORE YOU CODE" STEP. - value: | - _Replace this with research pointers._ - validations: - required: true - - - type: markdown - attributes: - value: | - - ## Example — Background Research - - - Open `src/hiero_sdk_python/query/transaction_get_receipt_query.py` and read how the query is built - - Compare with `TransactionGetRecordQuery` which already supports similar flags - - Check the [protobuf definition](https://github.com/hashgraph/hedera-protobufs/blob/main/services/transaction_get_receipt.proto) for the response structure - - Look at how child receipts are structured in the proto response - - - type: textarea - id: implementation - attributes: - label: 🛠️ Implementation - description: | - DESCRIBE WHAT THE CONTRIBUTOR SHOULD BUILD AFTER COMPLETING THEIR RESEARCH. - KEEP IT HIGH-LEVEL — THEY SHOULD FIGURE OUT THE DETAILS FROM THEIR RESEARCH. - value: | - _Replace this with implementation guidance._ - validations: - required: true - - - type: markdown - attributes: - value: | - - ## Example — Implementation - - - Add an optional boolean field (e.g. `_include_children`) to `TransactionGetReceiptQuery` - - Ensure the flag is passed to the mirror node request - - Update response parsing to include child receipts when present - - Keep default behavior unchanged (backwards compatible) - - Extend the existing example to demonstrate the new behavior - - - type: textarea - id: testing - attributes: - label: 🧪 Testing Requirements - description: | - EDIT THIS SECTION TO MATCH THE TYPE OF CHANGE. DELETE CATEGORIES THAT DO NOT APPLY. - TESTING IS A MAJOR COMPONENT OF INTERMEDIATE ISSUES — SET CLEAR EXPECTATIONS. - value: | - How you test depends on the type of change. - See the [testing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/testing.md) for full details. - - > [!IMPORTANT] - > At the intermediate level, **testing is a major component** — not an afterthought. - > Each method you implement should be verified. Tests should cover happy paths, - > edge cases, and error handling. Tests must be understandable and reviewable, - > not just generated by AI. - - **Source code changes (e.g. Python files in `src/`):** - - Write unit tests covering happy path, edge cases, and error handling - - Run tests locally: `uv run pytest tests/unit/_test.py -v` - - Verify naming, types, and field ordering match [protobuf definitions](https://github.com/hashgraph/hedera-protobufs/tree/main/services) - - Check consistency with similar classes already in the SDK — use the same patterns - - Integration tests will run automatically when you push - - **GitHub Actions / workflow changes (e.g. files in `.github/`):** - - Test by merging to your fork's `main` and simulating the scenario - - Create test issues or PRs as evidence and link them in your PR - - Include screenshots of workflow runs where applicable - - **Example script changes (e.g. files in `examples/`):** - - Run the example script and confirm output matches expected behavior - - Compare your example with similar existing examples for consistency - - You will need a [Hedera Portal](https://portal.hedera.com/) account for testnet credentials - - **In your PR, include links to test evidence:** screenshots, terminal output, test issue/PRs, or script results. - validations: - required: true - - - type: textarea - id: quality_standards - attributes: - label: 🛡️ Quality & Review Standards - description: | - PRE-FILLED GUIDANCE. LEAVE AS-IS. - value: | - Intermediate PRs cover more code and touch more sensitive areas. This section helps you - deliver a PR that is ready to merge on first or second review. - - **⚠️ Breaking changes** - - Before changing any function signature, return type, or public API — stop and check - - If a breaking change is unavoidable: add backwards compatibility and get explicit maintainer approval **before** implementing - - Run existing tests to verify nothing breaks: `uv run pytest tests/unit/ -v` - - **🤖 AI-assisted code** - - AI is a powerful tool — but every suggestion must be verified, not just accepted - - Cross-reference AI output against [protobuf definitions](https://github.com/hashgraph/hedera-protobufs/tree/main/services) and similar SDK implementations - - Do not submit AI-generated tests that merely test AI-generated code — tests should verify real behavior and edge cases - - If AI output and existing SDK patterns disagree, trust the SDK - - **📦 PR packaging** - - Self-review your diff line by line before submitting - - Clean git history — no rebase artifacts, merge commits, or unrelated files - - CHANGELOG entry under `[Unreleased]` - - Link test evidence (screenshots, terminal output, test PRs) in your PR description - - Ensure all CI checks pass before requesting review - - Double and triple check — intermediate PRs are time-consuming to review - validations: - required: false - - - type: textarea - id: acceptance - attributes: - label: ✅ Done Checklist - description: | - EDIT OR ADD CRITERIA SPECIFIC TO THIS ISSUE. THE DEFAULTS COVER THE STANDARD REQUIREMENTS. - value: | - Before opening your PR, confirm: - - - [ ] My changes fully address the problem described above - - [ ] I verified naming, types, and patterns against protobuf definitions and similar SDK classes - - [ ] I tested my changes thoroughly (see testing section above) and linked evidence in my PR - - [ ] I checked for breaking changes — no existing tests fail, no public APIs changed without approval - - [ ] I did not modify files unrelated to this issue - - [ ] My commits are signed: `git commit -S -s -m "chore: description"` — [Signing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/signing.md) - - [ ] I added a CHANGELOG.md entry under `[Unreleased]` — [Changelog guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/changelog_entry.md) - - [ ] All CI checks pass - - [ ] I self-reviewed my PR before requesting review - validations: - required: true - - - type: textarea - id: contribution_steps - attributes: - label: 📋 Workflow quick reference - description: | - PRE-FILLED REFERENCE TABLE. LEAVE AS-IS. - value: | - You know the workflow — here are the links if you need them: - - | Step | Guide | - |------|-------| - | Claim this issue | Comment `/assign` below | - | Sync with main | [Rebasing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/rebasing.md) | - | Open a PR and link this issue | [PR guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/11_submit_pull_request.md) · [Linking guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/how_to_link_issues.md) | - | Resolve merge conflicts | [Merge conflicts guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/merge_conflicts.md) | - | Testing | [Testing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/testing.md) | - validations: - required: false - - - type: textarea - id: ai_tips - attributes: - label: 🤖 Tips for using AI tools - description: | - PRE-FILLED GUIDANCE. LEAVE AS-IS. - value: | - Here is how to get the most out of AI tools at the intermediate level: - - - Use [Pylance](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/pylance.md) in VS Code to catch type errors and hallucinated methods in real time - - Always verify AI suggestions against [protobuf definitions](https://github.com/hashgraph/hedera-protobufs/tree/main/services) — AI often gets field names, types, or ordering wrong - - Build your solution in small steps — add print statements or logs to confirm each piece works before moving on - - When AI suggests code, check it against similar classes in the SDK — that is the best source of Python SDK-specific patterns - - If AI output and SDK patterns disagree, trust the SDK - - Write tests that verify real behavior, not tests that test AI-generated code - validations: - required: false - - - type: textarea - id: stuck - attributes: - label: 🆘 Stuck? - description: | - PRE-FILLED GUIDANCE. LEAVE AS-IS. - value: | - Intermediate issues can be challenging — it is completely normal to get blocked. Here is what to do: - - - **Comment on this issue** and describe what you have tried. A maintainer will respond. - - **Ask on [Discord](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md)** in #hiero-python-sdk for quick questions. - - **Join [Office Hours](https://zoom-lfx.platform.linuxfoundation.org/meeting/99912667426?password=5b584a0e-1ed7-49d3-b2fc-dc5ddc888338)** (Wednesdays, 2pm UTC) for live, hands-on help with a maintainer — screen sharing welcome. - - At this level, you are expected to research independently — but asking good, high-level questions is a strength, not a weakness. Don't spend more than an hour blocked without asking. - validations: - required: false - - - type: textarea - id: resources - attributes: - label: 📚 Resources - description: | - ADD ANY TASK-SPECIFIC RESOURCES BELOW — RELEVANT PROTOBUF DEFINITIONS, PYTHON DOCS, OR SPECIFIC FILES IN THE CODEBASE. - value: | - **Project references:** - - [Project structure](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/setup/project_structure.md) - - [CONTRIBUTING.md](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/CONTRIBUTING.md) - - [Browse closed intermediate PRs](https://github.com/hiero-ledger/hiero-sdk-python/pulls?q=is%3Apr+is%3Amerged+label%3Aintermediate) — see how others did it - - **Protobuf references:** - - [Hedera Protobufs](https://github.com/hashgraph/hedera-protobufs/tree/main/services) — source of truth for field names, types, and ordering - - [Protobuf language guide](https://protobuf.dev/programming-guides/proto3/) - - **Python references:** - - [Python official docs](https://docs.python.org/3/) - - [Data model (dunder methods)](https://docs.python.org/3/reference/datamodel.html) - - [Type hints](https://docs.python.org/3/library/typing.html) - - [unittest.mock](https://docs.python.org/3/library/unittest.mock.html) — for mocking in tests - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/05_advanced_issue.yml b/.github/ISSUE_TEMPLATE/05_advanced_issue.yml deleted file mode 100644 index b91b3805b..000000000 --- a/.github/ISSUE_TEMPLATE/05_advanced_issue.yml +++ /dev/null @@ -1,104 +0,0 @@ -name: Advanced Issue Template -description: For expert contributors tackling architectural, multi-module, or core-logic changes. -title: "[Advanced]: " -labels: ["advanced"] -assignees: [] - -body: - - type: textarea - id: advanced_contributors - attributes: - label: 🧠 Advanced Contributors — Prerequisites & Expectations - description: | - REQUIRED: Contributors must meet these thresholds before claiming. - value: | - > [!CAUTION] - > **Advanced issues are the highest-risk work in this project. We will reject PRs that do not meet these standards.** - - ### 🏁 Concrete Prerequisites - - **Proven History:** Successfully completed **≥ 10 non-trivial intermediate issues** in this repo. - - **Consistency:** **≥ 3–4 months** of active, human-led contributions to this SDK. - - **Expertise:** Deep architectural understanding of `_Executable`, `Transaction`, and `Query` base classes. - - **Advanced Python:** Demonstrated proficiency with complex patterns (e.g., async concurrency, decorators, or state management) used in the core SDK. - - > [!NOTE] - > **Workflow Exception:** For issues focused on **GitHub Actions / Workflows**, the Python-specific thresholds above may be waived if the contributor demonstrates expert-level proficiency in CI/CD security and automation. - - ### ⚠️ AI Usage Policy - Using AI to generate code for Advanced issues is **strictly forbidden**. AI may be used only to help explain file relationships. Submitting AI-mined code or unvalidated generated output is grounds for **immediate rejection**. - - ### ⏱️ Timeline & Workflow - - **Typical time:** ~1 month / ~50 hours. - - 🔴 Completing an advanced issue in 1–3 days is a **red flag** and will likely be rejected. - - **Mandatory:** Post your proposed architectural approach as a comment and wait for explicit maintainer approval **before writing any code.** - validations: - required: false - - - type: textarea - id: problem - attributes: - label: 🐞 Problem Description - placeholder: "Describe the current limitation or architectural risk here..." - validations: - required: true - - - type: textarea - id: implementation - attributes: - label: 🛠️ Implementation Notes - value: | - ### Technical domains involved in this issue: - - [ ] **API Client Architecture** (request → serialization → execution → response mapping) - - [ ] **Backward Compatibility** (preserving method signatures, defaults, and return types) - - [ ] **Protobuf Alignment** (reading `.proto` files, `_to_proto()` / `_from_proto()` correctness) - - [ ] **State & Immutability** (correct usage of guards like `_require_not_frozen`) - - [ ] **Execution Boundaries** (retry logic, backoff, node selection, gRPC deadlines) - - _Replace this with specific implementation notes._ - validations: - required: false - - - type: textarea - id: quality_standards - attributes: - label: 🛡️ Quality & Review Standards - value: | - The bar for advanced PRs is **"safe, maintainable, architecturally sound, and production-ready."** - - ### 🚀 Defining "Production Ready" - 1. **Architectural Fit:** The solution must fit naturally into the existing SDK abstractions. Avoid "hacks" or isolated logic that bypasses the core execution model. - 2. **Security & Correctness:** Evaluate all logic for injection risks, state corruption, or thread-safety issues. Every line of code must be manually verified. - 3. **Maintainability:** Code must be clear enough for any other maintainer to debug without your assistance. Prefer standard patterns over "clever" one-liners. - 4. **Backward Compatibility:** Public API signatures must be preserved. If a breaking change is required, it must be explicitly managed through a deprecation cycle. - validations: - required: false - - - type: textarea - id: acceptance - attributes: - label: ✅ PR Quality Checklist - description: | - PRE-FILLED. These are the standards the contributor must meet before opening a PR. - value: | - Before opening your PR, the contributor must confirm: - - [ ] I understand the system-wide impact of these changes on affected modules and performance. - - [ ] The system design fits with current Hiero SDK architectural approaches. - - [ ] I have tested my changes extensively against both local and network environments. - - [ ] I have verified naming, types, and field ordering against pinned Protobufs (v0.72.0-rc.2). - - [ ] Every line of code is personally understood and explainable (no unvalidated AI code). - - - type: textarea - id: resources - attributes: - label: 📚 Resources & Support - value: | - **Project References:** - - [SDK Project Structure](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/setup/project_structure.md) - - [Transaction Lifecycle](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/transaction_lifecycle.md) - - [Hedera Protobufs (v0.72.0-rc.2)](https://github.com/hashgraph/hedera-protobufs/tree/v0.72.0-rc.2/services) - - **🆘 Stuck?** - - [Office Hours](https://zoom-lfx.platform.linuxfoundation.org/meeting/99912667426?password=5b584a0e-1ed7-49d3-b2fc-dc5ddc888338) (Wednesdays, 2pm UTC) - - [Discord #hiero-python-sdk](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md) - validations: - required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 87d1218c8..e91ecd9eb 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,7 +1,6 @@ name: Bug report description: Create a report to help us improve labels: [ bug ] -type: Bug body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 04a0310a7..e4257e4c7 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -1,7 +1,6 @@ name: Feature description: Suggest an idea for this project labels: [enhancement] -type: Feature body: - type: markdown attributes: diff --git a/CHANGELOG.md b/CHANGELOG.md index b99be60bf..b71ecc53f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Changelog +# Changelog All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org). @@ -23,6 +23,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ### .github +- chore: Replace issue templates with upstream sdk-collaboration-hub versions; fix broken `tree/main` links, YAML indentation bugs, `.CHANGELOG.md` typo, duplicate step numbering, and misnumbered list in beginner quality standards; add Python SDK-specific resources and Windows setup guide links (#2088) - chore: Added check for the discussion label in the inactivity bot (#1583) - chore: pin pip packages to exact versions in publish.yml to improve supply chain security and reproducibility (#2056) - chore: update GitHub Actions runners from ubuntu-latest to hl-sdk-py-lin-md (#2021) From e6ea500047f0bc1a28eea9622b1a177e3618c7f8 Mon Sep 17 00:00:00 2001 From: Om Swastik Panda Date: Sat, 11 Apr 2026 18:27:10 +0530 Subject: [PATCH 38/60] chore: rename community call and office hour bots to cron prefix (#2077) Signed-off-by: Omswastik-11 Signed-off-by: exploreriii <133720349+exploreriii@users.noreply.github.com> Signed-off-by: Om Swastik Panda Co-authored-by: exploreriii <133720349+exploreriii@users.noreply.github.com> --- .../scripts/{bot-community-calls.sh => cron-community-calls.sh} | 0 .github/scripts/{bot-office-hours.sh => cron-office-hours.sh} | 0 .../{bot-community-calls.yml => cron-community-calls.yml} | 2 +- .../workflows/{bot-office-hours.yml => cron-office-hours.yml} | 2 +- CHANGELOG.md | 1 + 5 files changed, 3 insertions(+), 2 deletions(-) rename .github/scripts/{bot-community-calls.sh => cron-community-calls.sh} (100%) mode change 100755 => 100644 rename .github/scripts/{bot-office-hours.sh => cron-office-hours.sh} (100%) mode change 100755 => 100644 rename .github/workflows/{bot-community-calls.yml => cron-community-calls.yml} (94%) rename .github/workflows/{bot-office-hours.yml => cron-office-hours.yml} (95%) diff --git a/.github/scripts/bot-community-calls.sh b/.github/scripts/cron-community-calls.sh old mode 100755 new mode 100644 similarity index 100% rename from .github/scripts/bot-community-calls.sh rename to .github/scripts/cron-community-calls.sh diff --git a/.github/scripts/bot-office-hours.sh b/.github/scripts/cron-office-hours.sh old mode 100755 new mode 100644 similarity index 100% rename from .github/scripts/bot-office-hours.sh rename to .github/scripts/cron-office-hours.sh diff --git a/.github/workflows/bot-community-calls.yml b/.github/workflows/cron-community-calls.yml similarity index 94% rename from .github/workflows/bot-community-calls.yml rename to .github/workflows/cron-community-calls.yml index 831f4f1f3..a707469de 100644 --- a/.github/workflows/bot-community-calls.yml +++ b/.github/workflows/cron-community-calls.yml @@ -39,4 +39,4 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || 'false' }} run: | - bash .github/scripts/bot-community-calls.sh + bash .github/scripts/cron-community-calls.sh diff --git a/.github/workflows/bot-office-hours.yml b/.github/workflows/cron-office-hours.yml similarity index 95% rename from .github/workflows/bot-office-hours.yml rename to .github/workflows/cron-office-hours.yml index 3c5f621aa..82ebbcbcf 100644 --- a/.github/workflows/bot-office-hours.yml +++ b/.github/workflows/cron-office-hours.yml @@ -38,4 +38,4 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || 'false' }} run: | - bash .github/scripts/bot-office-hours.sh + bash .github/scripts/cron-office-hours.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index b71ecc53f..fcaa881b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ### .github +- Renamed community call and office hour bots to use the `cron-` prefix (#2068) - chore: Replace issue templates with upstream sdk-collaboration-hub versions; fix broken `tree/main` links, YAML indentation bugs, `.CHANGELOG.md` typo, duplicate step numbering, and misnumbered list in beginner quality standards; add Python SDK-specific resources and Windows setup guide links (#2088) - chore: Added check for the discussion label in the inactivity bot (#1583) - chore: pin pip packages to exact versions in publish.yml to improve supply chain security and reproducibility (#2056) From e12f9ccfd64a15087a3eab0ff309d910e3b56701 Mon Sep 17 00:00:00 2001 From: "stepsecurity-app[bot]" <188008098+stepsecurity-app[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 19:03:27 +0530 Subject: [PATCH 39/60] chore: [StepSecurity] Apply security best practices (#2092) Signed-off-by: StepSecurity Bot Co-authored-by: stepsecurity-app[bot] <188008098+stepsecurity-app[bot]@users.noreply.github.com> Co-authored-by: exploreriii <133720349+exploreriii@users.noreply.github.com> --- .github/workflows/codeql.yml | 3 +++ .../workflows/pr-check-secondary-unit-integration-test.yml | 5 +++++ .github/workflows/sync-issue-labels-compute.yml | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ac152026d..5dd09ae85 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -10,6 +10,9 @@ on: schedule: - cron: "28 23 * * *" # Runs every day at 23:28 UTC. +permissions: + contents: read + jobs: analyze: name: Analyze (${{ matrix.language }}) diff --git a/.github/workflows/pr-check-secondary-unit-integration-test.yml b/.github/workflows/pr-check-secondary-unit-integration-test.yml index 8a2dc1dc5..5f0c2724f 100644 --- a/.github/workflows/pr-check-secondary-unit-integration-test.yml +++ b/.github/workflows/pr-check-secondary-unit-integration-test.yml @@ -174,6 +174,11 @@ jobs: if: always() steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + with: + egress-policy: audit + - name: Print summary and fail when any test job failed shell: bash run: | diff --git a/.github/workflows/sync-issue-labels-compute.yml b/.github/workflows/sync-issue-labels-compute.yml index e3db19955..cc35e097f 100644 --- a/.github/workflows/sync-issue-labels-compute.yml +++ b/.github/workflows/sync-issue-labels-compute.yml @@ -190,6 +190,11 @@ jobs: if: ${{ needs.compute-labels.outputs.is_fork_pr != 'true' }} runs-on: ubuntu-latest steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + with: + egress-policy: audit + - name: Trigger add workflow uses: step-security/workflow-dispatch@acca1a315af3bf7f33dd116d3cb405cb83f5cbdc # v1.2.8 with: From 4fd216970a175ed6ba6fa20933a4c0d589122466 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 19:41:40 +0530 Subject: [PATCH 40/60] chore(deps): bump step-security/harden-runner from 2.14.1 to 2.17.0 (#2098) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Manish Dait <90558243+manishdait@users.noreply.github.com> --- .github/workflows/bot-advanced-check.yml | 2 +- .github/workflows/bot-assignment-check.yml | 2 +- .github/workflows/bot-beginner-assign-on-comment.yml | 2 +- .github/workflows/bot-coderabbit-plan-trigger.yml | 2 +- .github/workflows/bot-gfi-assign-on-comment.yml | 2 +- .github/workflows/bot-gfi-candidate-notification.yaml | 2 +- .github/workflows/bot-inactivity-unassign.yml | 2 +- .github/workflows/bot-intermediate-assignment.yml | 2 +- .github/workflows/bot-issue-reminder-no-pr.yml | 2 +- .github/workflows/bot-linked-issue-enforcer.yml | 2 +- .github/workflows/bot-p0-issues-notify-team.yml | 2 +- .github/workflows/bot-pr-draft-explainer.yaml | 2 +- .github/workflows/bot-pr-inactivity-reminder.yml | 2 +- .github/workflows/bot-workflows.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/cron-check-broken-links.yml | 2 +- .github/workflows/cron-community-calls.yml | 2 +- .github/workflows/cron-office-hours.yml | 2 +- .github/workflows/cron-update-spam-list.yml | 2 +- .github/workflows/deps-check.yml | 2 +- .github/workflows/pr-check-broken-links.yml | 2 +- .github/workflows/pr-check-changelog.yml | 2 +- .github/workflows/pr-check-codecov.yml | 2 +- .github/workflows/pr-check-examples.yml | 2 +- .../workflows/pr-check-secondary-unit-integration-test.yml | 4 ++-- .github/workflows/pr-check-test-files.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/release-pr-coderabbit-gate.yml | 2 +- .github/workflows/sync-issue-labels-add.yml | 2 +- .github/workflows/sync-issue-labels-compute.yml | 2 +- .github/workflows/tck-test.yml | 2 +- .github/workflows/unassign-on-comment.yml | 2 +- .github/workflows/working-on-comment.yml | 2 +- 33 files changed, 34 insertions(+), 34 deletions(-) diff --git a/.github/workflows/bot-advanced-check.yml b/.github/workflows/bot-advanced-check.yml index cd41a943b..7eadbee2b 100644 --- a/.github/workflows/bot-advanced-check.yml +++ b/.github/workflows/bot-advanced-check.yml @@ -28,7 +28,7 @@ jobs: runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/bot-assignment-check.yml b/.github/workflows/bot-assignment-check.yml index 13db23e17..dadda56a8 100644 --- a/.github/workflows/bot-assignment-check.yml +++ b/.github/workflows/bot-assignment-check.yml @@ -12,7 +12,7 @@ jobs: runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/bot-beginner-assign-on-comment.yml b/.github/workflows/bot-beginner-assign-on-comment.yml index 2ea2ef679..305382313 100644 --- a/.github/workflows/bot-beginner-assign-on-comment.yml +++ b/.github/workflows/bot-beginner-assign-on-comment.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Harden runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/bot-coderabbit-plan-trigger.yml b/.github/workflows/bot-coderabbit-plan-trigger.yml index bf88d0b39..14cf30ce3 100644 --- a/.github/workflows/bot-coderabbit-plan-trigger.yml +++ b/.github/workflows/bot-coderabbit-plan-trigger.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/bot-gfi-assign-on-comment.yml b/.github/workflows/bot-gfi-assign-on-comment.yml index 8342676ba..f321867d7 100644 --- a/.github/workflows/bot-gfi-assign-on-comment.yml +++ b/.github/workflows/bot-gfi-assign-on-comment.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Harden runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/bot-gfi-candidate-notification.yaml b/.github/workflows/bot-gfi-candidate-notification.yaml index f393f1a0a..60885b453 100644 --- a/.github/workflows/bot-gfi-candidate-notification.yaml +++ b/.github/workflows/bot-gfi-candidate-notification.yaml @@ -21,7 +21,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 with: egress-policy: audit diff --git a/.github/workflows/bot-inactivity-unassign.yml b/.github/workflows/bot-inactivity-unassign.yml index 88a122332..8ecd7e357 100644 --- a/.github/workflows/bot-inactivity-unassign.yml +++ b/.github/workflows/bot-inactivity-unassign.yml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Harden the runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 with: egress-policy: audit diff --git a/.github/workflows/bot-intermediate-assignment.yml b/.github/workflows/bot-intermediate-assignment.yml index 339c6054a..bd1f9ebce 100644 --- a/.github/workflows/bot-intermediate-assignment.yml +++ b/.github/workflows/bot-intermediate-assignment.yml @@ -26,7 +26,7 @@ jobs: runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/bot-issue-reminder-no-pr.yml b/.github/workflows/bot-issue-reminder-no-pr.yml index c9e32b45d..7809bd21f 100644 --- a/.github/workflows/bot-issue-reminder-no-pr.yml +++ b/.github/workflows/bot-issue-reminder-no-pr.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 with: egress-policy: audit diff --git a/.github/workflows/bot-linked-issue-enforcer.yml b/.github/workflows/bot-linked-issue-enforcer.yml index fbac3a03e..c4ed1dd63 100644 --- a/.github/workflows/bot-linked-issue-enforcer.yml +++ b/.github/workflows/bot-linked-issue-enforcer.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit - name: Checkout repository diff --git a/.github/workflows/bot-p0-issues-notify-team.yml b/.github/workflows/bot-p0-issues-notify-team.yml index 4a753cc20..164d65175 100644 --- a/.github/workflows/bot-p0-issues-notify-team.yml +++ b/.github/workflows/bot-p0-issues-notify-team.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/bot-pr-draft-explainer.yaml b/.github/workflows/bot-pr-draft-explainer.yaml index 57bbc8a46..8a5e5c6e6 100644 --- a/.github/workflows/bot-pr-draft-explainer.yaml +++ b/.github/workflows/bot-pr-draft-explainer.yaml @@ -29,7 +29,7 @@ jobs: cancel-in-progress: true steps: - name: Harden the runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/bot-pr-inactivity-reminder.yml b/.github/workflows/bot-pr-inactivity-reminder.yml index 11f288b72..90f09b88c 100644 --- a/.github/workflows/bot-pr-inactivity-reminder.yml +++ b/.github/workflows/bot-pr-inactivity-reminder.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit - name: Checkout repository diff --git a/.github/workflows/bot-workflows.yml b/.github/workflows/bot-workflows.yml index 2de71473a..45a78175b 100644 --- a/.github/workflows/bot-workflows.yml +++ b/.github/workflows/bot-workflows.yml @@ -21,7 +21,7 @@ jobs: runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5dd09ae85..a7a774a3d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -37,7 +37,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/cron-check-broken-links.yml b/.github/workflows/cron-check-broken-links.yml index 95977a5ea..9dc38d465 100644 --- a/.github/workflows/cron-check-broken-links.yml +++ b/.github/workflows/cron-check-broken-links.yml @@ -20,7 +20,7 @@ jobs: runs-on: hl-sdk-py-lin-md steps: - name: Harden runner (audit outbound calls) - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/cron-community-calls.yml b/.github/workflows/cron-community-calls.yml index a707469de..4dc797ec9 100644 --- a/.github/workflows/cron-community-calls.yml +++ b/.github/workflows/cron-community-calls.yml @@ -27,7 +27,7 @@ jobs: runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d #2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 #2.17.0 with: egress-policy: audit diff --git a/.github/workflows/cron-office-hours.yml b/.github/workflows/cron-office-hours.yml index 82ebbcbcf..53648ddab 100644 --- a/.github/workflows/cron-office-hours.yml +++ b/.github/workflows/cron-office-hours.yml @@ -26,7 +26,7 @@ jobs: cancel-in-progress: false steps: - name: Harden the runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d #2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 #2.17.0 with: egress-policy: audit diff --git a/.github/workflows/cron-update-spam-list.yml b/.github/workflows/cron-update-spam-list.yml index 6e2e89490..1638236a7 100644 --- a/.github/workflows/cron-update-spam-list.yml +++ b/.github/workflows/cron-update-spam-list.yml @@ -23,7 +23,7 @@ jobs: runs-on: hl-sdk-py-lin-md steps: - name: Harden runner (audit outbound calls) - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/deps-check.yml b/.github/workflows/deps-check.yml index aa90413b5..7289f3c52 100644 --- a/.github/workflows/deps-check.yml +++ b/.github/workflows/deps-check.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/pr-check-broken-links.yml b/.github/workflows/pr-check-broken-links.yml index 33b6f1241..b3984c8c0 100644 --- a/.github/workflows/pr-check-broken-links.yml +++ b/.github/workflows/pr-check-broken-links.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Harden runner (audit outbound calls) - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/pr-check-changelog.yml b/.github/workflows/pr-check-changelog.yml index 10ea4d79f..68abd4c13 100644 --- a/.github/workflows/pr-check-changelog.yml +++ b/.github/workflows/pr-check-changelog.yml @@ -16,7 +16,7 @@ jobs: fetch-depth: 0 - name: Harden the runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/pr-check-codecov.yml b/.github/workflows/pr-check-codecov.yml index b46e80a36..e2dcbed5c 100644 --- a/.github/workflows/pr-check-codecov.yml +++ b/.github/workflows/pr-check-codecov.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/pr-check-examples.yml b/.github/workflows/pr-check-examples.yml index 2ffedf599..580181757 100644 --- a/.github/workflows/pr-check-examples.yml +++ b/.github/workflows/pr-check-examples.yml @@ -14,7 +14,7 @@ jobs: runs-on: ${{ (github.repository_owner != 'hiero-ledger' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork)) && 'ubuntu-latest' || 'hl-sdk-py-lin-md' }} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/pr-check-secondary-unit-integration-test.yml b/.github/workflows/pr-check-secondary-unit-integration-test.yml index 5f0c2724f..662a44308 100644 --- a/.github/workflows/pr-check-secondary-unit-integration-test.yml +++ b/.github/workflows/pr-check-secondary-unit-integration-test.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit @@ -104,7 +104,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/pr-check-test-files.yml b/.github/workflows/pr-check-test-files.yml index c8de51b8d..3d46aee39 100644 --- a/.github/workflows/pr-check-test-files.yml +++ b/.github/workflows/pr-check-test-files.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1d317b4f3..feafa2b70 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,7 @@ jobs: id-token: write steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/release-pr-coderabbit-gate.yml b/.github/workflows/release-pr-coderabbit-gate.yml index eb096a0d2..a35ec1992 100644 --- a/.github/workflows/release-pr-coderabbit-gate.yml +++ b/.github/workflows/release-pr-coderabbit-gate.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/sync-issue-labels-add.yml b/.github/workflows/sync-issue-labels-add.yml index cc7bd4b1c..49bc8e796 100644 --- a/.github/workflows/sync-issue-labels-add.yml +++ b/.github/workflows/sync-issue-labels-add.yml @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden the runner - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/sync-issue-labels-compute.yml b/.github/workflows/sync-issue-labels-compute.yml index cc35e097f..3739a1b05 100644 --- a/.github/workflows/sync-issue-labels-compute.yml +++ b/.github/workflows/sync-issue-labels-compute.yml @@ -33,7 +33,7 @@ jobs: is_fork_pr: ${{ steps.compute.outputs.is_fork_pr }} steps: - name: Harden the runner - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/tck-test.yml b/.github/workflows/tck-test.yml index d5717b155..2f47b9d63 100644 --- a/.github/workflows/tck-test.yml +++ b/.github/workflows/tck-test.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit diff --git a/.github/workflows/unassign-on-comment.yml b/.github/workflows/unassign-on-comment.yml index f99761e90..74087fcef 100644 --- a/.github/workflows/unassign-on-comment.yml +++ b/.github/workflows/unassign-on-comment.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Harden runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 with: egress-policy: audit diff --git a/.github/workflows/working-on-comment.yml b/.github/workflows/working-on-comment.yml index 85c121145..31417bded 100644 --- a/.github/workflows/working-on-comment.yml +++ b/.github/workflows/working-on-comment.yml @@ -26,7 +26,7 @@ jobs: cancel-in-progress: false steps: - name: Harden Runner - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit From 1638286e7227527c2f1d504919d91f7581efc888 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 19:59:39 +0530 Subject: [PATCH 41/60] chore(deps): bump actions/github-script from 8.0.0 to 9.0.0 (#2099) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/bot-beginner-assign-on-comment.yml | 2 +- .github/workflows/bot-coderabbit-plan-trigger.yml | 2 +- .github/workflows/bot-gfi-assign-on-comment.yml | 2 +- .github/workflows/bot-gfi-candidate-notification.yaml | 2 +- .github/workflows/bot-intermediate-assignment.yml | 2 +- .github/workflows/bot-linked-issue-enforcer.yml | 2 +- .github/workflows/bot-p0-issues-notify-team.yml | 2 +- .github/workflows/bot-pr-draft-explainer.yaml | 2 +- .github/workflows/bot-pr-inactivity-reminder.yml | 2 +- .github/workflows/cron-check-broken-links.yml | 2 +- .github/workflows/cron-update-spam-list.yml | 2 +- .github/workflows/release-pr-coderabbit-gate.yml | 2 +- .github/workflows/sync-issue-labels-compute.yml | 4 ++-- .github/workflows/unassign-on-comment.yml | 2 +- .github/workflows/working-on-comment.yml | 2 +- 15 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/bot-beginner-assign-on-comment.yml b/.github/workflows/bot-beginner-assign-on-comment.yml index 305382313..8147ded07 100644 --- a/.github/workflows/bot-beginner-assign-on-comment.yml +++ b/.github/workflows/bot-beginner-assign-on-comment.yml @@ -31,7 +31,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Run Beginner /assign handler - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0 with: script: | const script = require('./.github/scripts/bot-beginner-assign-on-comment.js'); diff --git a/.github/workflows/bot-coderabbit-plan-trigger.yml b/.github/workflows/bot-coderabbit-plan-trigger.yml index 14cf30ce3..9da678d98 100644 --- a/.github/workflows/bot-coderabbit-plan-trigger.yml +++ b/.github/workflows/bot-coderabbit-plan-trigger.yml @@ -46,7 +46,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || 'false' }} - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0 with: script: | const script = require('./.github/scripts/coderabbit_plan_trigger.js'); diff --git a/.github/workflows/bot-gfi-assign-on-comment.yml b/.github/workflows/bot-gfi-assign-on-comment.yml index f321867d7..6d238e7d2 100644 --- a/.github/workflows/bot-gfi-assign-on-comment.yml +++ b/.github/workflows/bot-gfi-assign-on-comment.yml @@ -32,7 +32,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Run GFI /assign handler - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0 with: script: | const script = require('./.github/scripts/bot-gfi-assign-on-comment.js'); diff --git a/.github/workflows/bot-gfi-candidate-notification.yaml b/.github/workflows/bot-gfi-candidate-notification.yaml index 60885b453..b4fa6ac63 100644 --- a/.github/workflows/bot-gfi-candidate-notification.yaml +++ b/.github/workflows/bot-gfi-candidate-notification.yaml @@ -32,7 +32,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DRY_RUN: ${{ github.repository_owner != 'hiero-ledger' }} - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0 with: script: | const script = require('./.github/scripts/bot-gfi-candidate-notification.js'); diff --git a/.github/workflows/bot-intermediate-assignment.yml b/.github/workflows/bot-intermediate-assignment.yml index bd1f9ebce..eb8c667f3 100644 --- a/.github/workflows/bot-intermediate-assignment.yml +++ b/.github/workflows/bot-intermediate-assignment.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Enforce intermediate assignment guard - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INTERMEDIATE_LABEL: intermediate GFI_LABEL: Good First Issue diff --git a/.github/workflows/bot-linked-issue-enforcer.yml b/.github/workflows/bot-linked-issue-enforcer.yml index c4ed1dd63..88686bb83 100644 --- a/.github/workflows/bot-linked-issue-enforcer.yml +++ b/.github/workflows/bot-linked-issue-enforcer.yml @@ -35,7 +35,7 @@ jobs: DRY_RUN: ${{ env.DRY_RUN }} HOURS_BEFORE_CLOSE: "12" REQUIRE_AUTHOR_ASSIGNED: "true" - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0 with: script: | const script = require('./.github/scripts/linked_issue_enforce.js'); diff --git a/.github/workflows/bot-p0-issues-notify-team.yml b/.github/workflows/bot-p0-issues-notify-team.yml index 164d65175..488249d4f 100644 --- a/.github/workflows/bot-p0-issues-notify-team.yml +++ b/.github/workflows/bot-p0-issues-notify-team.yml @@ -30,7 +30,7 @@ jobs: - name: Notify team of P0 issues env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0 with: script: | const script = require('./.github/scripts/bot-p0-issues-notify-team.js'); diff --git a/.github/workflows/bot-pr-draft-explainer.yaml b/.github/workflows/bot-pr-draft-explainer.yaml index 8a5e5c6e6..d2329fa68 100644 --- a/.github/workflows/bot-pr-draft-explainer.yaml +++ b/.github/workflows/bot-pr-draft-explainer.yaml @@ -43,7 +43,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0 with: script: | const script = require('./.github/scripts/bot-pr-draft-explainer.js'); diff --git a/.github/workflows/bot-pr-inactivity-reminder.yml b/.github/workflows/bot-pr-inactivity-reminder.yml index 90f09b88c..947be45fc 100644 --- a/.github/workflows/bot-pr-inactivity-reminder.yml +++ b/.github/workflows/bot-pr-inactivity-reminder.yml @@ -34,7 +34,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} DRY_RUN: ${{ env.DRY_RUN }} - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 with: script: | const script = require('./.github/scripts/pr_inactivity_reminder.js') diff --git a/.github/workflows/cron-check-broken-links.yml b/.github/workflows/cron-check-broken-links.yml index 9dc38d465..7d4c7e23a 100644 --- a/.github/workflows/cron-check-broken-links.yml +++ b/.github/workflows/cron-check-broken-links.yml @@ -39,7 +39,7 @@ jobs: - name: Report Broken Links (Idempotent Issue Management) if: steps.lychee.outcome == 'failure' - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | // Determine if this is a dry run diff --git a/.github/workflows/cron-update-spam-list.yml b/.github/workflows/cron-update-spam-list.yml index 1638236a7..cc98b49a9 100644 --- a/.github/workflows/cron-update-spam-list.yml +++ b/.github/workflows/cron-update-spam-list.yml @@ -32,7 +32,7 @@ jobs: - name: Update spam list id: update-spam-list - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | diff --git a/.github/workflows/release-pr-coderabbit-gate.yml b/.github/workflows/release-pr-coderabbit-gate.yml index a35ec1992..dab805d0b 100644 --- a/.github/workflows/release-pr-coderabbit-gate.yml +++ b/.github/workflows/release-pr-coderabbit-gate.yml @@ -35,7 +35,7 @@ jobs: - name: Post CodeRabbit release-gate prompt comment env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0 with: script: | const script = require('./.github/scripts/release-pr-coderabbit-gate.js'); diff --git a/.github/workflows/sync-issue-labels-compute.yml b/.github/workflows/sync-issue-labels-compute.yml index 3739a1b05..a979b4add 100644 --- a/.github/workflows/sync-issue-labels-compute.yml +++ b/.github/workflows/sync-issue-labels-compute.yml @@ -46,7 +46,7 @@ jobs: REQUESTED_DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && format('{0}', github.event.inputs['dry-run-enabled']) || 'true' }} IS_FORK_PR: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork || 'false' }} MAX_LINKED_ISSUES: '20' - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: result-encoding: json script: | @@ -163,7 +163,7 @@ jobs: IS_FORK_PR: ${{ steps.compute.outputs.is_fork_pr }} DRY_RUN: ${{ steps.compute.outputs.dry_run }} SOURCE_EVENT: ${{ steps.compute.outputs.source_event }} - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const fs = require('fs'); diff --git a/.github/workflows/unassign-on-comment.yml b/.github/workflows/unassign-on-comment.yml index 74087fcef..e33f705b4 100644 --- a/.github/workflows/unassign-on-comment.yml +++ b/.github/workflows/unassign-on-comment.yml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Run /unassign handler - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 with: script: | const script = require('./.github/scripts/bot-unassign-on-comment.js'); diff --git a/.github/workflows/working-on-comment.yml b/.github/workflows/working-on-comment.yml index 31417bded..95b67959d 100644 --- a/.github/workflows/working-on-comment.yml +++ b/.github/workflows/working-on-comment.yml @@ -35,7 +35,7 @@ jobs: - name: Handle /working command id: working - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: DRY_RUN: ${{ github.event.inputs.dry_run }} with: From bd47d4fdf1ba44922b7d79f27292618521cd57e3 Mon Sep 17 00:00:00 2001 From: Abhijeet Saharan Date: Tue, 7 Apr 2026 00:55:05 +0530 Subject: [PATCH 42/60] chore: track uv.lock for reproducible installs Signed-off-by: Abhijeet Saharan --- .gitignore | 1 - uv.lock | 1697 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1697 insertions(+), 1 deletion(-) create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index 3ab60d4c1..f1f06ce9d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,5 @@ src/hiero_sdk_python/hapi .hypothesis/ # Lock files -uv.lock pdm.lock pubkey.asc diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..e966d6055 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1697 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <4" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[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, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[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, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { 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, upload-time = "2025-09-08T23:22:44.795Z" }, + { 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, upload-time = "2025-09-08T23:22:45.938Z" }, + { 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, upload-time = "2025-09-08T23:22:47.349Z" }, + { 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, upload-time = "2025-09-08T23:22:48.677Z" }, + { 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, upload-time = "2025-09-08T23:22:50.06Z" }, + { 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, upload-time = "2025-09-08T23:22:51.364Z" }, + { 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, upload-time = "2025-09-08T23:22:52.902Z" }, + { 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, upload-time = "2025-09-08T23:22:54.518Z" }, + { 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, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { 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, upload-time = "2025-09-08T23:23:00.879Z" }, + { 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, upload-time = "2025-09-08T23:23:02.231Z" }, + { 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, upload-time = "2025-09-08T23:23:03.472Z" }, + { 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, upload-time = "2025-09-08T23:23:04.792Z" }, + { 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, upload-time = "2025-09-08T23:23:06.127Z" }, + { 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, upload-time = "2025-09-08T23:23:07.753Z" }, + { 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, upload-time = "2025-09-08T23:23:09.648Z" }, + { 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, upload-time = "2025-09-08T23:23:10.928Z" }, + { 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, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { 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, upload-time = "2025-09-08T23:23:18.087Z" }, + { 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, upload-time = "2025-09-08T23:23:19.622Z" }, + { 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, upload-time = "2025-09-08T23:23:20.853Z" }, + { 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, upload-time = "2025-09-08T23:23:22.08Z" }, + { 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, upload-time = "2025-09-08T23:23:23.314Z" }, + { 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, upload-time = "2025-09-08T23:23:24.541Z" }, + { 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, upload-time = "2025-09-08T23:23:26.143Z" }, + { 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, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { 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, upload-time = "2025-09-08T23:23:29.347Z" }, + { 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, upload-time = "2025-09-08T23:23:30.63Z" }, + { 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, upload-time = "2025-09-08T23:23:31.91Z" }, + { 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, upload-time = "2025-09-08T23:23:33.214Z" }, + { 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, upload-time = "2025-09-08T23:23:34.495Z" }, + { 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, upload-time = "2025-09-08T23:23:36.096Z" }, + { 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, upload-time = "2025-09-08T23:23:37.328Z" }, + { 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, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[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, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { 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, upload-time = "2025-10-14T04:40:53.353Z" }, + { 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, upload-time = "2025-10-14T04:40:54.558Z" }, + { 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, upload-time = "2025-10-14T04:40:55.677Z" }, + { 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, upload-time = "2025-10-14T04:40:57.217Z" }, + { 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, upload-time = "2025-10-14T04:40:58.358Z" }, + { 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, upload-time = "2025-10-14T04:40:59.468Z" }, + { 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, upload-time = "2025-10-14T04:41:00.623Z" }, + { 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, upload-time = "2025-10-14T04:41:01.754Z" }, + { 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, upload-time = "2025-10-14T04:41:03.231Z" }, + { 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, upload-time = "2025-10-14T04:41:04.715Z" }, + { 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, upload-time = "2025-10-14T04:41:05.827Z" }, + { 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, upload-time = "2025-10-14T04:41:06.938Z" }, + { 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, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { 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, upload-time = "2025-10-14T04:41:13.346Z" }, + { 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, upload-time = "2025-10-14T04:41:14.461Z" }, + { 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, upload-time = "2025-10-14T04:41:15.588Z" }, + { 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, upload-time = "2025-10-14T04:41:16.738Z" }, + { 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, upload-time = "2025-10-14T04:41:17.923Z" }, + { 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, upload-time = "2025-10-14T04:41:19.106Z" }, + { 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, upload-time = "2025-10-14T04:41:20.245Z" }, + { 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, upload-time = "2025-10-14T04:41:21.398Z" }, + { 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, upload-time = "2025-10-14T04:41:22.583Z" }, + { 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, upload-time = "2025-10-14T04:41:23.754Z" }, + { 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, upload-time = "2025-10-14T04:41:25.27Z" }, + { 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, upload-time = "2025-10-14T04:41:26.725Z" }, + { 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, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { 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, upload-time = "2025-10-14T04:41:33.773Z" }, + { 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, upload-time = "2025-10-14T04:41:34.897Z" }, + { 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, upload-time = "2025-10-14T04:41:36.116Z" }, + { 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, upload-time = "2025-10-14T04:41:37.229Z" }, + { 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, upload-time = "2025-10-14T04:41:38.368Z" }, + { 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, upload-time = "2025-10-14T04:41:39.862Z" }, + { 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, upload-time = "2025-10-14T04:41:41.319Z" }, + { 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, upload-time = "2025-10-14T04:41:42.539Z" }, + { 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, upload-time = "2025-10-14T04:41:43.661Z" }, + { 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, upload-time = "2025-10-14T04:41:44.821Z" }, + { 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, upload-time = "2025-10-14T04:41:46.442Z" }, + { 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, upload-time = "2025-10-14T04:41:47.631Z" }, + { 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, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + +[[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, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, + { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, + { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, + { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, + { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, + { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, +] + +[[package]] +name = "cytoolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/d4/16916f3dc20a3f5455b63c35dcb260b3716f59ce27a93586804e70e431d5/cytoolz-1.1.0.tar.gz", hash = "sha256:13a7bf254c3c0d28b12e2290b82aed0f0977a4c2a2bf84854fcdc7796a29f3b0", size = 642510, upload-time = "2025-10-19T00:44:56.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/7a/3244e6e3587be9abfee3b1c320e43a279831b3c3a31fe5d08c1ee6193e6b/cytoolz-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:72d7043a88ea5e61ba9d17ea0d1c1eff10f645d7edfcc4e56a31ef78be287644", size = 1307813, upload-time = "2025-10-19T00:39:34.198Z" }, + { url = "https://files.pythonhosted.org/packages/32/7e/eaf504ca59addce323ef4d4ffedc2913d83c121ec19f6419bc402f7702dc/cytoolz-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d759e9ed421bacfeb456d47af8d734c057b9912b5f2441f95b27ca35e5efab07", size = 985777, upload-time = "2025-10-19T00:39:36.545Z" }, + { url = "https://files.pythonhosted.org/packages/d4/a1/ec95443f0cf4cd0dbc574fa26ac85a0442d35f3b601a90a0e3dda077f614/cytoolz-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fdb5be8fbcc0396141189022724155a4c1c93712ac4aef8c03829af0c2a816d7", size = 982865, upload-time = "2025-10-19T00:39:38.19Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1b/8503604b0c0534977363fb77d371019395dfa031a216f9b1d8729d1280e4/cytoolz-1.1.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c8c0a513dc89bc05cc72893609118815bced5ef201f1a317b4cc3423b3a0e750", size = 2597969, upload-time = "2025-10-19T00:39:40.26Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e5/30748da06417cb2d4bc58e380b0c11d8c6539f4e289dc1e4f4b4fc248d0e/cytoolz-1.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce94db4f8ebe842c30c0ece42ff5de977c47859088c2c363dede5a68f6906484", size = 2692230, upload-time = "2025-10-19T00:39:42.327Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/e06580b74deb97dfd3513e4e6b660c2dedc220c7653f5bd3e4f772f4d885/cytoolz-1.1.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b622d4f54e370c853ded94a668f94fe72c6d70e06ac102f17a2746661c27ab52", size = 2565243, upload-time = "2025-10-19T00:39:44.403Z" }, + { url = "https://files.pythonhosted.org/packages/91/5e/79c0122a34c33afcb5aaee1fec35be24fe16cecefb9bb8890f2908feae56/cytoolz-1.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:375a65baa5a5b4ff6a0c5ff17e170cf23312e4c710755771ca966144c24216b5", size = 2868602, upload-time = "2025-10-19T00:39:46.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/84/404698ff02b32292db1e39cc4a2fbdabe15164b092cc364902984c3ce0f4/cytoolz-1.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c0d51bcdb3203a062a78f66bbe33db5e3123048e24a5f0e1402422d79df8ee2d", size = 2905121, upload-time = "2025-10-19T00:39:48.078Z" }, + { url = "https://files.pythonhosted.org/packages/9f/33/afad6593829ba73fc87b5ae64441e380fc937f79f24a1cda60d23cb99b8c/cytoolz-1.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1010869529bb05dc9802b6d776a34ca1b6d48b9deec70ad5e2918ae175be5c2f", size = 2684382, upload-time = "2025-10-19T00:39:49.766Z" }, + { url = "https://files.pythonhosted.org/packages/ce/86/7900013a82ca9c6cadbfb22bf50d0fbfc3b192915d2bdd9fab3f69a9afba/cytoolz-1.1.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11a8f2e83295bdb33f35454d6bafcb7845b03b5881dcaed66ecbd726c7f16772", size = 2518183, upload-time = "2025-10-19T00:39:51.433Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4b/acf9be2953fed6a6d795fb66de37c367915037a998a5b3d3b69476cf91fe/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0499c5e0a8e688ed367a2e51cc13792ae8f08226c15f7d168589fc44b9b9cada", size = 2609368, upload-time = "2025-10-19T00:39:53.458Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ec/3e30455fd526f5cc37bd3dd2a0e2aafb803ae4d271e50ce53bfc30810053/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:87d44e6033d4c5e95a7d39ba59b8e105ba1c29b1ccd1d215f26477cc1d64be39", size = 2561458, upload-time = "2025-10-19T00:39:55.493Z" }, + { url = "https://files.pythonhosted.org/packages/49/27/e5815c85bb18cdf95780f9596dcfd76dee910a4d635a1924648cb8a636c6/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a68cef396a7de237f7b97422a6a450dfb111722296ba217ba5b34551832f1f6e", size = 2578236, upload-time = "2025-10-19T00:39:57.512Z" }, + { url = "https://files.pythonhosted.org/packages/17/db/588e266eff397670398ea335a809152e77b02ee92e0ec42091115b42f09b/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:06ad4c95b258141f138a93ebfdc1d76ac087afc1a82f1401100a1f44b44ba656", size = 2770523, upload-time = "2025-10-19T00:39:59.194Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ad/82be0b999c7a0a0b362cedfc183eb090b872fd42937af2d6e97d58bc70f8/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ada59a4b3c59d4ac7162e0ed08667ffa78abf48e975c8a9f9d5b9bc50720f4fd", size = 2512909, upload-time = "2025-10-19T00:40:01.199Z" }, + { url = "https://files.pythonhosted.org/packages/25/21/45f07ab0339a20c518bc9006100922babc397ab7ea5ef40a395db83b9cdd/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a8957bcaea1ba01327a9b219d2adb84144377684f51444253890dab500ca171f", size = 2755345, upload-time = "2025-10-19T00:40:03.322Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a7/e530bf2b304206f79b36d793caba1ff9448348713a41bb1ad0197714a0f2/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6d8cdc299d67eb0f3b9ecdafeeb55eb3b7b7470e2d950ac34b05ed4c7a5572b8", size = 2617790, upload-time = "2025-10-19T00:40:05.03Z" }, + { url = "https://files.pythonhosted.org/packages/9f/77/7f53092121d7431589344c7d65c3d43c4111547aafabb21d3ca9032d126c/cytoolz-1.1.0-cp310-cp310-win32.whl", hash = "sha256:d8e08464c5cdea4f6df31e84b11ed6bfd79cedb99fbcbfdc15eb9361a6053c5a", size = 900209, upload-time = "2025-10-19T00:40:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/84/e4/902578658303b9bc76b1704d3ed85e6d307d311bd9fa0b919581bea56e62/cytoolz-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7e49922a7ed54262d41960bf3b835a7700327bf79cff1e9bfc73d79021132ff8", size = 944802, upload-time = "2025-10-19T00:40:08.983Z" }, + { url = "https://files.pythonhosted.org/packages/71/9f/56a7003617b4eabd8ddfb470aacc240425cbe6ddeb756adfbbaadaa175f1/cytoolz-1.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:943a662d2e72ffc4438d43ab5a1de8d852237775a423236594a3b3e381b8032c", size = 904835, upload-time = "2025-10-19T00:40:11.024Z" }, + { url = "https://files.pythonhosted.org/packages/69/82/edf1d0c32b6222f2c22e5618d6db855d44eb59f9b6f22436ff963c5d0a5c/cytoolz-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dba8e5a8c6e3c789d27b0eb5e7ce5ed7d032a7a9aae17ca4ba5147b871f6e327", size = 1314345, upload-time = "2025-10-19T00:40:13.273Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b5/0e3c1edaa26c2bd9db90cba0ac62c85bbca84224c7ae1c2e0072c4ea64c5/cytoolz-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44b31c05addb0889167a720123b3b497b28dd86f8a0aeaf3ae4ffa11e2c85d55", size = 989259, upload-time = "2025-10-19T00:40:15.196Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/e2b2ee9fc684867e817640764ea5807f9d25aa1e7bdba02dd4b249aab0f7/cytoolz-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:653cb18c4fc5d8a8cfce2bce650aabcbe82957cd0536827367d10810566d5294", size = 986551, upload-time = "2025-10-19T00:40:16.831Z" }, + { url = "https://files.pythonhosted.org/packages/39/9f/4e8ee41acf6674f10a9c2c9117b2f219429a5a0f09bba6135f34ca4f08a6/cytoolz-1.1.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:853a5b4806915020c890e1ce70cc056bbc1dd8bc44f2d74d555cccfd7aefba7d", size = 2688378, upload-time = "2025-10-19T00:40:18.552Z" }, + { url = "https://files.pythonhosted.org/packages/78/94/ef006f3412bc22444d855a0fc9ecb81424237fb4e5c1a1f8f5fb79ac978f/cytoolz-1.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7b44e9de86bea013fe84fd8c399d6016bbb96c37c5290769e5c99460b9c53e5", size = 2798299, upload-time = "2025-10-19T00:40:20.191Z" }, + { url = "https://files.pythonhosted.org/packages/df/aa/365953926ee8b4f2e07df7200c0d73632155908c8867af14b2d19cc9f1f7/cytoolz-1.1.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:098d628a801dc142e9740126be5624eb7aef1d732bc7a5719f60a2095547b485", size = 2639311, upload-time = "2025-10-19T00:40:22.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ee/62beaaee7df208f22590ad07ef8875519af49c52ca39d99460b14a00f15a/cytoolz-1.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:779ee4096ed7a82cffab89372ffc339631c285079dbf33dbe7aff1f6174985df", size = 2979532, upload-time = "2025-10-19T00:40:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/c5/04/2211251e450bed111ada1194dc42c461da9aea441de62a01e4085ea6de9f/cytoolz-1.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f2ce18dd99533d077e9712f9faa852f389f560351b1efd2f2bdb193a95eddde2", size = 3018632, upload-time = "2025-10-19T00:40:26.175Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a2/4a3400e4d07d3916172bf74fede08020d7b4df01595d8a97f1e9507af5ae/cytoolz-1.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac266a34437812cf841cecbfe19f355ab9c3dd1ef231afc60415d40ff12a76e4", size = 2788579, upload-time = "2025-10-19T00:40:27.878Z" }, + { url = "https://files.pythonhosted.org/packages/fe/82/bb88caa53a41f600e7763c517d50e2efbbe6427ea395716a92b83f44882a/cytoolz-1.1.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1920b9b9c13d60d0bb6cd14594b3bce0870022eccb430618c37156da5f2b7a55", size = 2593024, upload-time = "2025-10-19T00:40:29.601Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/8b25e59570da16c7a0f173b8c6ec0aa6f3abd47fd385c007485acb459896/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47caa376dafd2bdc29f8a250acf59c810ec9105cd6f7680b9a9d070aae8490ec", size = 2715304, upload-time = "2025-10-19T00:40:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/faec7696f235521b926ffdf92c102f5b029f072d28e1020364e55b084820/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5ab2c97d8aaa522b038cca9187b1153347af22309e7c998b14750c6fdec7b1cb", size = 2654461, upload-time = "2025-10-19T00:40:32.884Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/f790ed167c04b8d2a33bed30770a9b7066fc4f573321d797190e5f05685f/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4bce006121b120e8b359244ee140bb0b1093908efc8b739db8dbaa3f8fb42139", size = 2672077, upload-time = "2025-10-19T00:40:34.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b3/80b8183e7eee44f45bfa3cdd3ebdadf3dd43ffc686f96d442a6c4dded45d/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fc0f1e4e9bb384d26e73c6657bbc26abdae4ff66a95933c00f3d578be89181b", size = 2881589, upload-time = "2025-10-19T00:40:36.315Z" }, + { url = "https://files.pythonhosted.org/packages/8f/05/ac5ba5ddb88a3ba7ecea4bf192194a838af564d22ea7a4812cbb6bd106ce/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:dd3f894ff972da1994d06ac6157d74e40dda19eb31fe5e9b7863ca4278c3a167", size = 2589924, upload-time = "2025-10-19T00:40:38.317Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/100483cae3849d24351c8333a815dc6adaf3f04912486e59386d86d9db9a/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0846f49cf8a4496bd42659040e68bd0484ce6af819709cae234938e039203ba0", size = 2868059, upload-time = "2025-10-19T00:40:40.025Z" }, + { url = "https://files.pythonhosted.org/packages/34/6e/3a7c56b325772d39397fc3aafb4dc054273982097178b6c3917c6dad48de/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:16a3af394ade1973226d64bb2f9eb3336adbdea03ed5b134c1bbec5a3b20028e", size = 2721692, upload-time = "2025-10-19T00:40:41.621Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ca/9fdaee32c3bc769dfb7e7991d9499136afccea67e423d097b8fb3c5acbc1/cytoolz-1.1.0-cp311-cp311-win32.whl", hash = "sha256:b786c9c8aeab76cc2f76011e986f7321a23a56d985b77d14f155d5e5514ea781", size = 899349, upload-time = "2025-10-19T00:40:43.183Z" }, + { url = "https://files.pythonhosted.org/packages/fd/04/2ab98edeea90311e4029e1643e43d2027b54da61453292d9ea51a103ee87/cytoolz-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:ebf06d1c5344fb22fee71bf664234733e55db72d74988f2ecb7294b05e4db30c", size = 945831, upload-time = "2025-10-19T00:40:44.693Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8d/777d86ea6bcc68b0fc926b0ef8ab51819e2176b37aadea072aac949d5231/cytoolz-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:b63f5f025fac893393b186e132e3e242de8ee7265d0cd3f5bdd4dda93f6616c9", size = 904076, upload-time = "2025-10-19T00:40:46.678Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ec/01426224f7acf60183d3921b25e1a8e71713d3d39cb464d64ac7aace6ea6/cytoolz-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:99f8e134c9be11649342853ec8c90837af4089fc8ff1e8f9a024a57d1fa08514", size = 1327800, upload-time = "2025-10-19T00:40:48.674Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/e07e8fedd332ac9626ad58bea31416dda19bfd14310731fa38b16a97e15f/cytoolz-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a6f44cf9319c30feb9a50aa513d777ef51efec16f31c404409e7deb8063df64", size = 997118, upload-time = "2025-10-19T00:40:50.919Z" }, + { url = "https://files.pythonhosted.org/packages/ab/72/c0f766d63ed2f9ea8dc8e1628d385d99b41fb834ce17ac3669e3f91e115d/cytoolz-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:945580dc158c557172fca899a35a99a16fbcebf6db0c77cb6621084bc82189f9", size = 991169, upload-time = "2025-10-19T00:40:52.887Z" }, + { url = "https://files.pythonhosted.org/packages/df/4b/1f757353d1bf33e56a7391ecc9bc49c1e529803b93a9d2f67fe5f92906fe/cytoolz-1.1.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:257905ec050d04f2f856854620d1e25556fd735064cebd81b460f54939b9f9d5", size = 2700680, upload-time = "2025-10-19T00:40:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/25/73/9b25bb7ed8d419b9d6ff2ae0b3d06694de79a3f98f5169a1293ff7ad3a3f/cytoolz-1.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82779049f352fb3ab5e8c993ab45edbb6e02efb1f17f0b50f4972c706cc51d76", size = 2824951, upload-time = "2025-10-19T00:40:56.137Z" }, + { url = "https://files.pythonhosted.org/packages/0c/93/9c787f7c909e75670fff467f2504725d06d8c3f51d6dfe22c55a08c8ccd4/cytoolz-1.1.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7d3e405e435320e08c5a1633afaf285a392e2d9cef35c925d91e2a31dfd7a688", size = 2679635, upload-time = "2025-10-19T00:40:57.799Z" }, + { url = "https://files.pythonhosted.org/packages/50/aa/9ee92c302cccf7a41a7311b325b51ebeff25d36c1f82bdc1bbe3f58dc947/cytoolz-1.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:923df8f5591e0d20543060c29909c149ab1963a7267037b39eee03a83dbc50a8", size = 2938352, upload-time = "2025-10-19T00:40:59.49Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a3/3b58c5c1692c3bacd65640d0d5c7267a7ebb76204f7507aec29de7063d2f/cytoolz-1.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:25db9e4862f22ea0ae2e56c8bec9fc9fd756b655ae13e8c7b5625d7ed1c582d4", size = 3022121, upload-time = "2025-10-19T00:41:01.209Z" }, + { url = "https://files.pythonhosted.org/packages/e1/93/c647bc3334355088c57351a536c2d4a83dd45f7de591fab383975e45bff9/cytoolz-1.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7a98deb11ccd8e5d9f9441ef2ff3352aab52226a2b7d04756caaa53cd612363", size = 2857656, upload-time = "2025-10-19T00:41:03.456Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c2/43fea146bf4141deea959e19dcddf268c5ed759dec5c2ed4a6941d711933/cytoolz-1.1.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dce4ee9fc99104bc77efdea80f32ca5a650cd653bcc8a1d984a931153d3d9b58", size = 2551284, upload-time = "2025-10-19T00:41:05.347Z" }, + { url = "https://files.pythonhosted.org/packages/6f/df/cdc7a81ce5cfcde7ef523143d545635fc37e80ccacce140ae58483a21da3/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80d6da158f7d20c15819701bbda1c041f0944ede2f564f5c739b1bc80a9ffb8b", size = 2721673, upload-time = "2025-10-19T00:41:07.528Z" }, + { url = "https://files.pythonhosted.org/packages/45/be/f8524bb9ad8812ad375e61238dcaa3177628234d1b908ad0b74e3657cafd/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3b5c5a192abda123ad45ef716ec9082b4cf7d95e9ada8291c5c2cc5558be858b", size = 2722884, upload-time = "2025-10-19T00:41:09.698Z" }, + { url = "https://files.pythonhosted.org/packages/23/e6/6bb8e4f9c267ad42d1ff77b6d2e4984665505afae50a216290e1d7311431/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5b399ce7d967b1cb6280250818b786be652aa8ddffd3c0bb5c48c6220d945ab5", size = 2685486, upload-time = "2025-10-19T00:41:11.349Z" }, + { url = "https://files.pythonhosted.org/packages/d7/dd/88619f9c8d2b682562c0c886bbb7c35720cb83fda2ac9a41bdd14073d9bd/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e7e29a1a03f00b4322196cfe8e2c38da9a6c8d573566052c586df83aacc5663c", size = 2839661, upload-time = "2025-10-19T00:41:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8d/4478ebf471ee78dd496d254dc0f4ad729cd8e6ba8257de4f0a98a2838ef2/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5291b117d71652a817ec164e7011f18e6a51f8a352cc9a70ed5b976c51102fda", size = 2547095, upload-time = "2025-10-19T00:41:16.054Z" }, + { url = "https://files.pythonhosted.org/packages/e6/68/f1dea33367b0b3f64e199c230a14a6b6f243c189020effafd31e970ca527/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8caef62f846a9011676c51bda9189ae394cdd6bb17f2946ecaedc23243268320", size = 2870901, upload-time = "2025-10-19T00:41:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/33591c09dfe799b8fb692cf2ad383e2c41ab6593cc960b00d1fc8a145655/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:de425c5a8e3be7bb3a195e19191d28d9eb3c2038046064a92edc4505033ec9cb", size = 2765422, upload-time = "2025-10-19T00:41:20.075Z" }, + { url = "https://files.pythonhosted.org/packages/60/2b/a8aa233c9416df87f004e57ae4280bd5e1f389b4943d179f01020c6ec629/cytoolz-1.1.0-cp312-cp312-win32.whl", hash = "sha256:296440a870e8d1f2e1d1edf98f60f1532b9d3ab8dfbd4b25ec08cd76311e79e5", size = 901933, upload-time = "2025-10-19T00:41:21.646Z" }, + { url = "https://files.pythonhosted.org/packages/ad/33/4c9bdf8390dc01d2617c7f11930697157164a52259b6818ddfa2f94f89f4/cytoolz-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:07156987f224c6dac59aa18fb8bf91e1412f5463961862716a3381bf429c8699", size = 947989, upload-time = "2025-10-19T00:41:23.288Z" }, + { url = "https://files.pythonhosted.org/packages/35/ac/6e2708835875f5acb52318462ed296bf94ed0cb8c7cb70e62fbd03f709e3/cytoolz-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:23e616b38f5b3160c7bb45b0f84a8f3deb4bd26b29fb2dfc716f241c738e27b8", size = 903913, upload-time = "2025-10-19T00:41:24.992Z" }, + { url = "https://files.pythonhosted.org/packages/71/4a/b3ddb3ee44fe0045e95dd973746f93f033b6f92cce1fc3cbbe24b329943c/cytoolz-1.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:76c9b58555300be6dde87a41faf1f97966d79b9a678b7a526fcff75d28ef4945", size = 976728, upload-time = "2025-10-19T00:41:26.5Z" }, + { url = "https://files.pythonhosted.org/packages/42/21/a3681434aa425875dd828bb515924b0f12c37a55c7d2bc5c0c5de3aeb0b4/cytoolz-1.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d1d638b10d3144795655e9395566ce35807df09219fd7cacd9e6acbdef67946a", size = 986057, upload-time = "2025-10-19T00:41:28.911Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cb/efc1b29e211e0670a6953222afaac84dcbba5cb940b130c0e49858978040/cytoolz-1.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:26801c1a165e84786a99e03c9c9973356caaca002d66727b761fb1042878ef06", size = 992632, upload-time = "2025-10-19T00:41:30.612Z" }, + { url = "https://files.pythonhosted.org/packages/be/b0/e50621d21e939338c97faab651f58ea7fa32101226a91de79ecfb89d71e1/cytoolz-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a9a464542912d3272f6dccc5142df057c71c6a5cbd30439389a732df401afb7", size = 1317534, upload-time = "2025-10-19T00:41:32.625Z" }, + { url = "https://files.pythonhosted.org/packages/0d/6b/25aa9739b0235a5bc4c1ea293186bc6822a4c6607acfe1422423287e7400/cytoolz-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed6104fa942aa5784bf54f339563de637557e3443b105760bc4de8f16a7fc79b", size = 992336, upload-time = "2025-10-19T00:41:34.073Z" }, + { url = "https://files.pythonhosted.org/packages/e1/53/5f4deb0ff958805309d135d899c764364c1e8a632ce4994bd7c45fb98df2/cytoolz-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56161f0ab60dc4159ec343509abaf809dc88e85c7e420e354442c62e3e7cbb77", size = 986118, upload-time = "2025-10-19T00:41:35.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e3/f6255b76c8cc0debbe1c0779130777dc0434da6d9b28a90d9f76f8cb67cd/cytoolz-1.1.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:832bd36cc9123535f1945acf6921f8a2a15acc19cfe4065b1c9b985a28671886", size = 2679563, upload-time = "2025-10-19T00:41:37.926Z" }, + { url = "https://files.pythonhosted.org/packages/59/8a/acc6e39a84e930522b965586ad3a36694f9bf247b23188ee0eb47b1c9ed1/cytoolz-1.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1842636b6e034f229bf084c2bcdcfd36c8437e752eefd2c74ce9e2f10415cb6e", size = 2813020, upload-time = "2025-10-19T00:41:39.935Z" }, + { url = "https://files.pythonhosted.org/packages/db/f5/0083608286ad1716eda7c41f868e85ac549f6fd6b7646993109fa0bdfd98/cytoolz-1.1.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:823df012ab90d2f2a0f92fea453528539bf71ac1879e518524cd0c86aa6df7b9", size = 2669312, upload-time = "2025-10-19T00:41:41.55Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/d16080b575520fe5da00cede1ece4e0a4180ec23f88dcdc6a2f5a90a7f7f/cytoolz-1.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f1fcf9e7e7b3487883ff3f815abc35b89dcc45c4cf81c72b7ee457aa72d197b", size = 2922147, upload-time = "2025-10-19T00:41:43.252Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bc/716c9c1243701e58cad511eb3937fd550e645293c5ed1907639c5d66f194/cytoolz-1.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4cdb3fa1772116827f263f25b0cdd44c663b6701346a56411960534a06c082de", size = 2981602, upload-time = "2025-10-19T00:41:45.354Z" }, + { url = "https://files.pythonhosted.org/packages/14/bc/571b232996846b27f4ac0c957dc8bf60261e9b4d0d01c8d955e82329544e/cytoolz-1.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1b5c95041741b81430454db65183e133976f45ac3c03454cfa8147952568529", size = 2830103, upload-time = "2025-10-19T00:41:47.959Z" }, + { url = "https://files.pythonhosted.org/packages/5b/55/c594afb46ecd78e4b7e1fb92c947ed041807875661ceda73baaf61baba4f/cytoolz-1.1.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b2079fd9f1a65f4c61e6278c8a6d4f85edf30c606df8d5b32f1add88cbbe2286", size = 2533802, upload-time = "2025-10-19T00:41:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/93/83/1edcf95832555a78fc43b975f3ebe8ceadcc9664dd47fd33747a14df5069/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a92a320d72bef1c7e2d4c6d875125cf57fc38be45feb3fac1bfa64ea401f54a4", size = 2706071, upload-time = "2025-10-19T00:41:51.386Z" }, + { url = "https://files.pythonhosted.org/packages/e2/df/035a408df87f25cfe3611557818b250126cd2281b2104cd88395de205583/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06d1c79aa51e6a92a90b0e456ebce2288f03dd6a76c7f582bfaa3eda7692e8a5", size = 2707575, upload-time = "2025-10-19T00:41:53.305Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a4/ef78e13e16e93bf695a9331321d75fbc834a088d941f1c19e6b63314e257/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e1d7be25f6971e986a52b6d3a0da28e1941850985417c35528f6823aef2cfec5", size = 2660486, upload-time = "2025-10-19T00:41:55.542Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/2c3d60682b26058d435416c4e90d4a94db854de5be944dfd069ed1be648a/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:964b248edc31efc50a65e9eaa0c845718503823439d2fa5f8d2c7e974c2b5409", size = 2819605, upload-time = "2025-10-19T00:41:58.257Z" }, + { url = "https://files.pythonhosted.org/packages/45/92/19b722a1d83cc443fbc0c16e0dc376f8a451437890d3d9ee370358cf0709/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c9ff2b3c57c79b65cb5be14a18c6fd4a06d5036fb3f33e973a9f70e9ac13ca28", size = 2533559, upload-time = "2025-10-19T00:42:00.324Z" }, + { url = "https://files.pythonhosted.org/packages/1d/15/fa3b7891da51115204416f14192081d3dea0eaee091f123fdc1347de8dd1/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:22290b73086af600042d99f5ce52a43d4ad9872c382610413176e19fc1d4fd2d", size = 2839171, upload-time = "2025-10-19T00:42:01.881Z" }, + { url = "https://files.pythonhosted.org/packages/46/40/d3519d5cd86eebebf1e8b7174ec32dfb6ecec67b48b0cfb92bf226659b5a/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a2ade74fccd080ea793382968913ee38d7a35c921df435bbf0a6aeecf0d17574", size = 2743379, upload-time = "2025-10-19T00:42:03.809Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/a9e7511f0a13fdbefa5bf73cf8e4763878140de9453fd3e50d6ac57b6be7/cytoolz-1.1.0-cp313-cp313-win32.whl", hash = "sha256:db5dbcfda1c00e937426cbf9bdc63c24ebbc358c3263bfcbc1ab4a88dc52aa8e", size = 900844, upload-time = "2025-10-19T00:42:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/fb7eb403c6a4c81e5a30363f34a71adcc8bf5292dc8ea32e2440aa5668f2/cytoolz-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9e2d3fe3b45c3eb7233746f7aca37789be3dceec3e07dcc406d3e045ea0f7bdc", size = 946461, upload-time = "2025-10-19T00:42:07.983Z" }, + { url = "https://files.pythonhosted.org/packages/93/bb/1c8c33d353548d240bc6e8677ee8c3560ce5fa2f084e928facf7c35a6dcf/cytoolz-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:32c559f95ff44a9ebcbd934acaa1e6dc8f3e6ffce4762a79a88528064873d6d5", size = 902673, upload-time = "2025-10-19T00:42:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/4a53acc60f59030fcaf48c7766e3c4c81bd997379425aa45b129396557b5/cytoolz-1.1.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9e2cd93b28f667c5870a070ab2b8bb4397470a85c4b204f2454b0ad001cd1ca3", size = 1372336, upload-time = "2025-10-19T00:42:12.104Z" }, + { url = "https://files.pythonhosted.org/packages/ac/90/f28fd8ad8319d8f5c8da69a2c29b8cf52a6d2c0161602d92b366d58926ab/cytoolz-1.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f494124e141a9361f31d79875fe7ea459a3be2b9dadd90480427c0c52a0943d4", size = 1011930, upload-time = "2025-10-19T00:42:14.231Z" }, + { url = "https://files.pythonhosted.org/packages/c9/95/4561c4e0ad1c944f7673d6d916405d68080f10552cfc5d69a1cf2475a9a1/cytoolz-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53a3262bf221f19437ed544bf8c0e1980c81ac8e2a53d87a9bc075dba943d36f", size = 1020610, upload-time = "2025-10-19T00:42:15.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/14/b2e1ffa4995ec36e1372e243411ff36325e4e6d7ffa34eb4098f5357d176/cytoolz-1.1.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:47663e57d3f3f124921f38055e86a1022d0844c444ede2e8f090d3bbf80deb65", size = 2917327, upload-time = "2025-10-19T00:42:17.706Z" }, + { url = "https://files.pythonhosted.org/packages/4a/29/7cab6c609b4514ac84cca2f7dca6c509977a8fc16d27c3a50e97f105fa6a/cytoolz-1.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5a8755c4104ee4e3d5ba434c543b5f85fdee6a1f1df33d93f518294da793a60", size = 3108951, upload-time = "2025-10-19T00:42:19.363Z" }, + { url = "https://files.pythonhosted.org/packages/9a/71/1d1103b819458679277206ad07d78ca6b31c4bb88d6463fd193e19bfb270/cytoolz-1.1.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d96ff3d381423af1b105295f97de86d1db51732c9566eb37378bab6670c5010", size = 2807149, upload-time = "2025-10-19T00:42:20.964Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d4/3d83a05a21e7d2ed2b9e6daf489999c29934b005de9190272b8a2e3735d0/cytoolz-1.1.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0ec96b3d537cdf47d4e76ded199f7440715f4c71029b45445cff92c1248808c2", size = 3111608, upload-time = "2025-10-19T00:42:22.684Z" }, + { url = "https://files.pythonhosted.org/packages/51/88/96f68354c3d4af68de41f0db4fe41a23b96a50a4a416636cea325490cfeb/cytoolz-1.1.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:208e2f2ef90a32b0acbff3303d90d89b13570a228d491d2e622a7883a3c68148", size = 3179373, upload-time = "2025-10-19T00:42:24.395Z" }, + { url = "https://files.pythonhosted.org/packages/ce/50/ed87a5cd8e6f27ffbb64c39e9730e18ec66c37631db2888ae711909f10c9/cytoolz-1.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d416a81bb0bd517558668e49d30a7475b5445f9bbafaab7dcf066f1e9adba36", size = 3003120, upload-time = "2025-10-19T00:42:26.18Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a7/acde155b050d6eaa8e9c7845c98fc5fb28501568e78e83ebbf44f8855274/cytoolz-1.1.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f32e94c91ffe49af04835ee713ebd8e005c85ebe83e7e1fdcc00f27164c2d636", size = 2703225, upload-time = "2025-10-19T00:42:27.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b6/9d518597c5bdea626b61101e8d2ff94124787a42259dafd9f5fc396f346a/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15d0c6405efc040499c46df44056a5c382f551a7624a41cf3e4c84a96b988a15", size = 2956033, upload-time = "2025-10-19T00:42:29.993Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/93e5f860926165538c85e1c5e1670ad3424f158df810f8ccd269da652138/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:bf069c5381d757debae891401b88b3a346ba3a28ca45ba9251103b282463fad8", size = 2862950, upload-time = "2025-10-19T00:42:31.803Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/99d6af00487bedc27597b54c9fcbfd5c833a69c6b7a9b9f0fff777bfc7aa/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d5cf15892e63411ec1bd67deff0e84317d974e6ab2cdfefdd4a7cea2989df66", size = 2861757, upload-time = "2025-10-19T00:42:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/71/ca/adfa1fb7949478135a37755cb8e88c20cd6b75c22a05f1128f05f3ab2c60/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3e3872c21170f8341656f8692f8939e8800dcee6549ad2474d4c817bdefd62cd", size = 2979049, upload-time = "2025-10-19T00:42:35.377Z" }, + { url = "https://files.pythonhosted.org/packages/70/4c/7bf47a03a4497d500bc73d4204e2d907771a017fa4457741b2a1d7c09319/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b9ddeff8e8fd65eb1fcefa61018100b2b627e759ea6ad275d2e2a93ffac147bf", size = 2699492, upload-time = "2025-10-19T00:42:37.133Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e7/3d034b0e4817314f07aa465d5864e9b8df9d25cb260a53dd84583e491558/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:02feeeda93e1fa3b33414eb57c2b0aefd1db8f558dd33fdfcce664a0f86056e4", size = 2995646, upload-time = "2025-10-19T00:42:38.912Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/be357181c71648d9fe1d1ce91cd42c63457dcf3c158e144416fd51dced83/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d08154ad45349162b6c37f12d5d1b2e6eef338e657b85e1621e4e6a4a69d64cb", size = 2919481, upload-time = "2025-10-19T00:42:40.85Z" }, + { url = "https://files.pythonhosted.org/packages/62/d5/bf5434fde726c4f80cb99912b2d8e0afa1587557e2a2d7e0315eb942f2de/cytoolz-1.1.0-cp313-cp313t-win32.whl", hash = "sha256:10ae4718a056948d73ca3e1bb9ab1f95f897ec1e362f829b9d37cc29ab566c60", size = 951595, upload-time = "2025-10-19T00:42:42.877Z" }, + { url = "https://files.pythonhosted.org/packages/64/29/39c161e9204a9715321ddea698cbd0abc317e78522c7c642363c20589e71/cytoolz-1.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:1bb77bc6197e5cb19784b6a42bb0f8427e81737a630d9d7dda62ed31733f9e6c", size = 1004445, upload-time = "2025-10-19T00:42:44.855Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5a/7cbff5e9a689f558cb0bdf277f9562b2ac51acf7cd15e055b8c3efb0e1ef/cytoolz-1.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:563dda652c6ff52d215704fbe6b491879b78d7bbbb3a9524ec8e763483cb459f", size = 926207, upload-time = "2025-10-19T00:42:46.456Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e8/297a85ba700f437c01eba962428e6ab4572f6c3e68e8ff442ce5c9d3a496/cytoolz-1.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d542cee7c7882d2a914a33dec4d3600416fb336734df979473249d4c53d207a1", size = 980613, upload-time = "2025-10-19T00:42:47.988Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d7/2b02c9d18e9cc263a0e22690f78080809f1eafe72f26b29ccc115d3bf5c8/cytoolz-1.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31922849b701b0f24bb62e56eb2488dcd3aa6ae3057694bd6b3b7c4c2bc27c2f", size = 990476, upload-time = "2025-10-19T00:42:49.653Z" }, + { url = "https://files.pythonhosted.org/packages/89/26/b6b159d2929310fca0eff8a4989cd4b1ecbdf7c46fdff46c7a20fcae55c8/cytoolz-1.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e68308d32afd31943314735c1335e4ab5696110e96b405f6bdb8f2a8dc771a16", size = 992712, upload-time = "2025-10-19T00:42:51.306Z" }, + { url = "https://files.pythonhosted.org/packages/42/a0/f7c572aa151ed466b0fce4a327c3cc916d3ef3c82e341be59ea4b9bee9e4/cytoolz-1.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fc4bb48b3b866e1867f7c6411a4229e5b44be3989060663713e10efc24c9bd5f", size = 1322596, upload-time = "2025-10-19T00:42:52.978Z" }, + { url = "https://files.pythonhosted.org/packages/72/7c/a55d035e20b77b6725e85c8f1a418b3a4c23967288b8b0c2d1a40f158cbe/cytoolz-1.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:456f77207d1445025d7ef262b8370a05492dcb1490cb428b0f3bf1bd744a89b0", size = 992825, upload-time = "2025-10-19T00:42:55.026Z" }, + { url = "https://files.pythonhosted.org/packages/03/af/39d2d3db322136e12e9336a1f13bab51eab88b386bfb11f91d3faff8ba34/cytoolz-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:174ebc71ebb20a9baeffce6ee07ee2cd913754325c93f99d767380d8317930f7", size = 990525, upload-time = "2025-10-19T00:42:56.666Z" }, + { url = "https://files.pythonhosted.org/packages/a6/bd/65d7a869d307f9b10ad45c2c1cbb40b81a8d0ed1138fa17fd904f5c83298/cytoolz-1.1.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8b3604fef602bcd53415055a4f68468339192fd17be39e687ae24f476d23d56e", size = 2672409, upload-time = "2025-10-19T00:42:58.81Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fb/74dfd844bfd67e810bd36e8e3903a143035447245828e7fcd7c81351d775/cytoolz-1.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3604b959a01f64c366e7d10ec7634d5f5cfe10301e27a8f090f6eb3b2a628a18", size = 2808477, upload-time = "2025-10-19T00:43:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/d6/1f/587686c43e31c19241ec317da66438d093523921ea7749bbc65558a30df9/cytoolz-1.1.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6db2127a3c1bc2f59f08010d2ae53a760771a9de2f67423ad8d400e9ba4276e8", size = 2636881, upload-time = "2025-10-19T00:43:02.24Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6d/90468cd34f77cb38a11af52c4dc6199efcc97a486395a21bef72e9b7602e/cytoolz-1.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56584745ac647993a016a21bc76399113b7595e312f8d0a1b140c9fcf9b58a27", size = 2937315, upload-time = "2025-10-19T00:43:03.954Z" }, + { url = "https://files.pythonhosted.org/packages/d9/50/7b92cd78c613b92e3509e6291d3fb7e0d72ebda999a8df806a96c40ca9ab/cytoolz-1.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db2c4c3a7f7bd7e03bb1a236a125c8feb86c75802f4ecda6ecfaf946610b2930", size = 2959988, upload-time = "2025-10-19T00:43:05.758Z" }, + { url = "https://files.pythonhosted.org/packages/44/d5/34b5a28a8d9bb329f984b4c2259407ca3f501d1abeb01bacea07937d85d1/cytoolz-1.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48cb8a692111a285d2b9acd16d185428176bfbffa8a7c274308525fccd01dd42", size = 2795116, upload-time = "2025-10-19T00:43:07.411Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d9/5dd829e33273ec03bdc3c812e6c3281987ae2c5c91645582f6c331544a64/cytoolz-1.1.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d2f344ba5eb17dcf38ee37fdde726f69053f54927db8f8a1bed6ac61e5b1890d", size = 2535390, upload-time = "2025-10-19T00:43:09.104Z" }, + { url = "https://files.pythonhosted.org/packages/87/1f/7f9c58068a8eec2183110df051bc6b69dd621143f84473eeb6dc1b32905a/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abf76b1c1abd031f098f293b6d90ee08bdaa45f8b5678430e331d991b82684b1", size = 2704834, upload-time = "2025-10-19T00:43:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/d2/90/667def5665333575d01a65fe3ec0ca31b897895f6e3bc1a42d6ea3659369/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ddf9a38a5b686091265ff45b53d142e44a538cd6c2e70610d3bc6be094219032", size = 2658441, upload-time = "2025-10-19T00:43:12.655Z" }, + { url = "https://files.pythonhosted.org/packages/23/79/6615f9a14960bd29ac98b823777b6589357833f65cf1a11b5abc1587c120/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:946786755274f07bb2be0400f28adb31d7d85a7c7001873c0a8e24a503428fb3", size = 2654766, upload-time = "2025-10-19T00:43:14.325Z" }, + { url = "https://files.pythonhosted.org/packages/b0/99/be59c6e0ae02153ef10ae1ff0f380fb19d973c651b50cf829a731f6c9e79/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b8f78b9fed79cf185ad4ddec099abeef45951bdcb416c5835ba05f0a1242c7", size = 2827649, upload-time = "2025-10-19T00:43:16.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/b7/854ddcf9f9618844108677c20d48f4611b5c636956adea0f0e85e027608f/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fccde6efefdbc02e676ccb352a2ccc8a8e929f59a1c6d3d60bb78e923a49ca44", size = 2533456, upload-time = "2025-10-19T00:43:17.764Z" }, + { url = "https://files.pythonhosted.org/packages/45/66/bfe6fbb2bdcf03c8377c8c2f542576e15f3340c905a09d78a6cb3badd39a/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:717b7775313da5f51b0fbf50d865aa9c39cb241bd4cb605df3cf2246d6567397", size = 2826455, upload-time = "2025-10-19T00:43:19.561Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0c/cce4047bd927e95f59e73319c02c9bc86bd3d76392e0eb9e41a1147a479c/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5158744a09d0e0e4a4f82225e3a3c4ebf38f9ae74467aaa905467270e52f2794", size = 2714897, upload-time = "2025-10-19T00:43:21.291Z" }, + { url = "https://files.pythonhosted.org/packages/ac/9a/061323bb289b565802bad14fb7ab59fcd8713105df142bcf4dd9ff64f8ac/cytoolz-1.1.0-cp314-cp314-win32.whl", hash = "sha256:1ed534bdbbf063b2bb28fca7d0f6723a3e5a72b086e7c7fe6d74ae8c3e4d00e2", size = 901490, upload-time = "2025-10-19T00:43:22.895Z" }, + { url = "https://files.pythonhosted.org/packages/a3/20/1f3a733d710d2a25d6f10b463bef55ada52fe6392a5d233c8d770191f48a/cytoolz-1.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:472c1c9a085f5ad973ec0ad7f0b9ba0969faea6f96c9e397f6293d386f3a25ec", size = 946730, upload-time = "2025-10-19T00:43:24.838Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/2d657db4a5d1c10a152061800f812caba9ef20d7bd2406f51a5fd800c180/cytoolz-1.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:a7ad7ca3386fa86bd301be3fa36e7f0acb024f412f665937955acfc8eb42deff", size = 905722, upload-time = "2025-10-19T00:43:26.439Z" }, + { url = "https://files.pythonhosted.org/packages/19/97/b4a8c76796a9a8b9bc90c7992840fa1589a1af8e0426562dea4ce9b384a7/cytoolz-1.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:64b63ed4b71b1ba813300ad0f06b8aff19a12cf51116e0e4f1ed837cea4debcf", size = 1372606, upload-time = "2025-10-19T00:43:28.491Z" }, + { url = "https://files.pythonhosted.org/packages/08/d4/a1bb1a32b454a2d650db8374ff3bf875ba0fc1c36e6446ec02a83b9140a1/cytoolz-1.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a60ba6f2ed9eb0003a737e1ee1e9fa2258e749da6477946008d4324efa25149f", size = 1012189, upload-time = "2025-10-19T00:43:30.177Z" }, + { url = "https://files.pythonhosted.org/packages/21/4b/2f5cbbd81588918ee7dd70cffb66731608f578a9b72166aafa991071af7d/cytoolz-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1aa58e2434d732241f7f051e6f17657e969a89971025e24578b5cbc6f1346485", size = 1020624, upload-time = "2025-10-19T00:43:31.712Z" }, + { url = "https://files.pythonhosted.org/packages/f5/99/c4954dd86cd593cd776a038b36795a259b8b5c12cbab6363edf5f6d9c909/cytoolz-1.1.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6965af3fc7214645970e312deb9bd35a213a1eaabcfef4f39115e60bf2f76867", size = 2917016, upload-time = "2025-10-19T00:43:33.531Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/f1f70a17e272b433232bc8a27df97e46b202d6cc07e3b0d63f7f41ba0f2d/cytoolz-1.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddd2863f321d67527d3b67a93000a378ad6f967056f68c06467fe011278a6d0e", size = 3107634, upload-time = "2025-10-19T00:43:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/8f/bd/c3226a57474b4aef1f90040510cba30d0decd3515fed48dc229b37c2f898/cytoolz-1.1.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4e6b428e9eb5126053c2ae0efa62512ff4b38ed3951f4d0888ca7005d63e56f5", size = 2806221, upload-time = "2025-10-19T00:43:37.707Z" }, + { url = "https://files.pythonhosted.org/packages/c3/47/2f7bfe4aaa1e07dc9828bea228ed744faf73b26aee0c1bdf3b5520bf1909/cytoolz-1.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d758e5ef311d2671e0ae8c214c52e44617cf1e58bef8f022b547b9802a5a7f30", size = 3107671, upload-time = "2025-10-19T00:43:39.401Z" }, + { url = "https://files.pythonhosted.org/packages/4d/12/6ff3b04fbd1369d0fcd5f8b5910ba6e427e33bf113754c4c35ec3f747924/cytoolz-1.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a95416eca473e6c1179b48d86adcf528b59c63ce78f4cb9934f2e413afa9b56b", size = 3176350, upload-time = "2025-10-19T00:43:41.148Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/6691d986b728e77b5d2872743ebcd962d37a2d0f7e9ad95a81b284fbf905/cytoolz-1.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36c8ede93525cf11e2cc787b7156e5cecd7340193ef800b816a16f1404a8dc6d", size = 3001173, upload-time = "2025-10-19T00:43:42.923Z" }, + { url = "https://files.pythonhosted.org/packages/7a/cb/f59d83a5058e1198db5a1f04e4a124c94d60390e4fa89b6d2e38ee8288a0/cytoolz-1.1.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c949755b6d8a649c5fbc888bc30915926f1b09fe42fea9f289e297c2f6ddd3", size = 2701374, upload-time = "2025-10-19T00:43:44.716Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f0/1ae6d28df503b0bdae094879da2072b8ba13db5919cd3798918761578411/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1b6d37545816905a76d9ed59fa4e332f929e879f062a39ea0f6f620405cdc27", size = 2953081, upload-time = "2025-10-19T00:43:47.103Z" }, + { url = "https://files.pythonhosted.org/packages/f4/06/d86fe811c6222dc32d3e08f5d88d2be598a6055b4d0590e7c1428d55c386/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:05332112d4087904842b36954cd1d3fc0e463a2f4a7ef9477bd241427c593c3b", size = 2862228, upload-time = "2025-10-19T00:43:49.353Z" }, + { url = "https://files.pythonhosted.org/packages/ae/32/978ef6f42623be44a0a03ae9de875ab54aa26c7e38c5c4cd505460b0927d/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:31538ca2fad2d688cbd962ccc3f1da847329e2258a52940f10a2ac0719e526be", size = 2861971, upload-time = "2025-10-19T00:43:51.028Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f7/74c69497e756b752b359925d1feef68b91df024a4124a823740f675dacd3/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:747562aa70abf219ea16f07d50ac0157db856d447f7f498f592e097cbc77df0b", size = 2975304, upload-time = "2025-10-19T00:43:52.99Z" }, + { url = "https://files.pythonhosted.org/packages/5b/2b/3ce0e6889a6491f3418ad4d84ae407b8456b02169a5a1f87990dbba7433b/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:3dc15c48b20c0f467e15e341e102896c8422dccf8efc6322def5c1b02f074629", size = 2697371, upload-time = "2025-10-19T00:43:55.312Z" }, + { url = "https://files.pythonhosted.org/packages/15/87/c616577f0891d97860643c845f7221e95240aa589586de727e28a5eb6e52/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3c03137ee6103ba92d5d6ad6a510e86fded69cd67050bd8a1843f15283be17ac", size = 2992436, upload-time = "2025-10-19T00:43:57.253Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9f/490c81bffb3428ab1fa114051fbb5ba18aaa2e2fe4da5bf4170ca524e6b3/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be8e298d88f88bd172b59912240558be3b7a04959375646e7fd4996401452941", size = 2917612, upload-time = "2025-10-19T00:43:59.423Z" }, + { url = "https://files.pythonhosted.org/packages/66/35/0fec2769660ca6472bbf3317ab634675827bb706d193e3240aaf20eab961/cytoolz-1.1.0-cp314-cp314t-win32.whl", hash = "sha256:3d407140f5604a89578285d4aac7b18b8eafa055cf776e781aabb89c48738fad", size = 960842, upload-time = "2025-10-19T00:44:01.143Z" }, + { url = "https://files.pythonhosted.org/packages/46/b4/b7ce3d3cd20337becfec978ecfa6d0ef64884d0cf32d44edfed8700914b9/cytoolz-1.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:56e5afb69eb6e1b3ffc34716ee5f92ffbdb5cb003b3a5ca4d4b0fe700e217162", size = 1020835, upload-time = "2025-10-19T00:44:03.246Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1f/0498009aa563a9c5d04f520aadc6e1c0942434d089d0b2f51ea986470f55/cytoolz-1.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:27b19b4a286b3ff52040efa42dbe403730aebe5fdfd2def704eb285e2125c63e", size = 927963, upload-time = "2025-10-19T00:44:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/84/32/0522207170294cf691112a93c70a8ef942f60fa9ff8e793b63b1f09cedc0/cytoolz-1.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f32e93a55681d782fc6af939f6df36509d65122423cbc930be39b141064adff8", size = 922014, upload-time = "2025-10-19T00:44:44.911Z" }, + { url = "https://files.pythonhosted.org/packages/4c/49/9be2d24adaa18fa307ff14e3e43f02b2ae4b69c4ce51cee6889eb2114990/cytoolz-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5d9bc596751cbda8073e65be02ca11706f00029768fbbbc81e11a8c290bb41aa", size = 918134, upload-time = "2025-10-19T00:44:47.122Z" }, + { url = "https://files.pythonhosted.org/packages/5c/b3/6a76c3b94c6c87c72ea822e7e67405be6b649c2e37778eeac7c0c0c69de8/cytoolz-1.1.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b16660d01c3931951fab49db422c627897c38c1a1f0393a97582004019a4887", size = 981970, upload-time = "2025-10-19T00:44:48.906Z" }, + { url = "https://files.pythonhosted.org/packages/f6/8a/606e4c7ed14aa6a86aee6ca84a2cb804754dc6c4905b8f94e09e49f1ce60/cytoolz-1.1.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b7de5718e2113d4efccea3f06055758cdbc17388ecc3341ba4d1d812837d7c1a", size = 978877, upload-time = "2025-10-19T00:44:50.819Z" }, + { url = "https://files.pythonhosted.org/packages/97/ec/ad474dcb1f6c1ebfdda3c2ad2edbb1af122a0e79c9ff2cb901ffb5f59662/cytoolz-1.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a12a2a1a6bc44099491c05a12039efa08cc33a3d0f8c7b0566185e085e139283", size = 964279, upload-time = "2025-10-19T00:44:52.476Z" }, + { url = "https://files.pythonhosted.org/packages/68/8c/d245fd416c69d27d51f14d5ad62acc4ee5971088ee31c40ffe1cc109af68/cytoolz-1.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:047defa7f5f9a32f82373dbc3957289562e8a3fa58ae02ec8e4dca4f43a33a21", size = 916630, upload-time = "2025-10-19T00:44:54.059Z" }, +] + +[[package]] +name = "eth-abi" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-typing" }, + { name = "eth-utils" }, + { name = "parsimonious" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/71/d9e1380bd77fd22f98b534699af564f189b56d539cc2b9dab908d4e4c242/eth_abi-5.2.0.tar.gz", hash = "sha256:178703fa98c07d8eecd5ae569e7e8d159e493ebb6eeb534a8fe973fbc4e40ef0", size = 49797, upload-time = "2025-01-14T16:29:34.629Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/b4/2f3982c4cbcbf5eeb6aec62df1533c0e63c653b3021ff338d44944405676/eth_abi-5.2.0-py3-none-any.whl", hash = "sha256:17abe47560ad753f18054f5b3089fcb588f3e3a092136a416b6c1502cb7e8877", size = 28511, upload-time = "2025-01-14T16:29:31.862Z" }, +] + +[[package]] +name = "eth-hash" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/38/577b7bc9380ef9dff0f1dffefe0c9a1ded2385e7a06c306fd95afb6f9451/eth_hash-0.7.1.tar.gz", hash = "sha256:d2411a403a0b0a62e8247b4117932d900ffb4c8c64b15f92620547ca5ce46be5", size = 12227, upload-time = "2025-01-13T21:29:21.765Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/db/f8775490669d28aca24871c67dd56b3e72105cb3bcae9a4ec65dd70859b3/eth_hash-0.7.1-py3-none-any.whl", hash = "sha256:0fb1add2adf99ef28883fd6228eb447ef519ea72933535ad1a0b28c6f65f868a", size = 8028, upload-time = "2025-01-13T21:29:19.365Z" }, +] + +[[package]] +name = "eth-keys" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-typing" }, + { name = "eth-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/11/1ed831c50bd74f57829aa06e58bd82a809c37e070ee501c953b9ac1f1552/eth_keys-0.7.0.tar.gz", hash = "sha256:79d24fd876201df67741de3e3fefb3f4dbcbb6ace66e47e6fe662851a4547814", size = 30166, upload-time = "2025-04-07T17:40:21.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/25/0ae00f2b0095e559d61ad3dc32171bd5a29dfd95ab04b4edd641f7c75f72/eth_keys-0.7.0-py3-none-any.whl", hash = "sha256:b0cdda8ffe8e5ba69c7c5ca33f153828edcace844f67aabd4542d7de38b159cf", size = 20656, upload-time = "2025-04-07T17:40:20.441Z" }, +] + +[[package]] +name = "eth-typing" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/54/62aa24b9cc708f06316167ee71c362779c8ed21fc8234a5cd94a8f53b623/eth_typing-5.2.1.tar.gz", hash = "sha256:7557300dbf02a93c70fa44af352b5c4a58f94e997a0fd6797fb7d1c29d9538ee", size = 21806, upload-time = "2025-04-14T20:39:28.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/72/c370bbe4c53da7bf998d3523f5a0f38867654923a82192df88d0705013d3/eth_typing-5.2.1-py3-none-any.whl", hash = "sha256:b0c2812ff978267563b80e9d701f487dd926f1d376d674f3b535cfe28b665d3d", size = 19163, upload-time = "2025-04-14T20:39:26.571Z" }, +] + +[[package]] +name = "eth-utils" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cytoolz", marker = "implementation_name == 'cpython'" }, + { name = "eth-hash" }, + { name = "eth-typing" }, + { name = "pydantic" }, + { name = "toolz", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/e1/ee3a8728227c3558853e63ff35bd4c449abdf5022a19601369400deacd39/eth_utils-5.3.1.tar.gz", hash = "sha256:c94e2d2abd024a9a42023b4ddc1c645814ff3d6a737b33d5cfd890ebf159c2d1", size = 123506, upload-time = "2025-08-27T16:37:17.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/4d/257cdc01ada430b8e84b9f2385c2553f33218f5b47da9adf0a616308d4b7/eth_utils-5.3.1-py3-none-any.whl", hash = "sha256:1f5476d8f29588d25b8ae4987e1ffdfae6d4c09026e476c4aad13b32dda3ead0", size = 102529, upload-time = "2025-08-27T16:37:15.449Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "flask" +version = "3.1.3" +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/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, +] + +[[package]] +name = "grpcio" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/a8/690a085b4d1fe066130de97a87de32c45062cf2ecd218df9675add895550/grpcio-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5", size = 5946986, upload-time = "2026-02-06T09:54:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1b/e5213c5c0ced9d2d92778d30529ad5bb2dcfb6c48c4e2d01b1f302d33d64/grpcio-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2", size = 11816533, upload-time = "2026-02-06T09:54:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/1ba32dccf0a324cc5ace744c44331e300b000a924bf14840f948c559ede7/grpcio-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d", size = 6519964, upload-time = "2026-02-06T09:54:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f5/c0e178721b818072f2e8b6fde13faaba942406c634009caf065121ce246b/grpcio-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb", size = 7198058, upload-time = "2026-02-06T09:54:42.389Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/40d43c91ae9cd667edc960135f9f08e58faa1576dc95af29f66ec912985f/grpcio-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7", size = 6727212, upload-time = "2026-02-06T09:54:44.91Z" }, + { url = "https://files.pythonhosted.org/packages/ed/88/9da42eed498f0efcfcd9156e48ae63c0cde3bea398a16c99fb5198c885b6/grpcio-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec", size = 7300845, upload-time = "2026-02-06T09:54:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/23/3f/1c66b7b1b19a8828890e37868411a6e6925df5a9030bfa87ab318f34095d/grpcio-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a", size = 8284605, upload-time = "2026-02-06T09:54:50.475Z" }, + { url = "https://files.pythonhosted.org/packages/94/c4/ca1bd87394f7b033e88525384b4d1e269e8424ab441ea2fba1a0c5b50986/grpcio-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813", size = 7726672, upload-time = "2026-02-06T09:54:53.11Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/f16e487d4cc65ccaf670f6ebdd1a17566b965c74fc3d93999d3b2821e052/grpcio-1.78.0-cp310-cp310-win32.whl", hash = "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de", size = 4076715, upload-time = "2026-02-06T09:54:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/2a/32/4ce60d94e242725fd3bcc5673c04502c82a8e87b21ea411a63992dc39f8f/grpcio-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf", size = 4799157, upload-time = "2026-02-06T09:54:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, + { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, + { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, + { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, + { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, + { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, + { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, + { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, +] + +[[package]] +name = "grpcio-tools" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/d1/cbefe328653f746fd319c4377836a25ba64226e41c6a1d7d5cdbc87a459f/grpcio_tools-1.78.0.tar.gz", hash = "sha256:4b0dd86560274316e155d925158276f8564508193088bc43e20d3f5dff956b2b", size = 5393026, upload-time = "2026-02-06T09:59:59.53Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/70/2118a814a62ab205c905d221064bc09021db83fceeb84764d35c00f0f633/grpcio_tools-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:ea64e38d1caa2b8468b08cb193f5a091d169b6dbfe1c7dac37d746651ab9d84e", size = 2545568, upload-time = "2026-02-06T09:57:30.308Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a9/68134839dd1a00f964185ead103646d6dd6a396b92ed264eaf521431b793/grpcio_tools-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:4003fcd5cbb5d578b06176fd45883a72a8f9203152149b7c680ce28653ad9e3a", size = 5708704, upload-time = "2026-02-06T09:57:33.512Z" }, + { url = "https://files.pythonhosted.org/packages/36/1b/b6135aa9534e22051c53e5b9c0853d18024a41c50aaff464b7b47c1ed379/grpcio_tools-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe6b0081775394c61ec633c9ff5dbc18337100eabb2e946b5c83967fe43b2748", size = 2591905, upload-time = "2026-02-06T09:57:35.338Z" }, + { url = "https://files.pythonhosted.org/packages/41/2b/6380df1390d62b1d18ae18d4d790115abf4997fa29498aa50ba644ecb9d8/grpcio_tools-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:7e989ad2cd93db52d7f1a643ecaa156ac55bf0484f1007b485979ce8aef62022", size = 2905271, upload-time = "2026-02-06T09:57:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/3a/07/9b369f37c8f4956b68778c044d57390a8f0f3b1cca590018809e75a4fce2/grpcio_tools-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b874991797e96c41a37e563236c3317ed41b915eff25b292b202d6277d30da85", size = 2656234, upload-time = "2026-02-06T09:57:41.157Z" }, + { url = "https://files.pythonhosted.org/packages/51/61/40eee40e7a54f775a0d4117536532713606b6b177fff5e327f33ad18746e/grpcio_tools-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:daa8c288b728228377aaf758925692fc6068939d9fa32f92ca13dedcbeb41f33", size = 3105770, upload-time = "2026-02-06T09:57:43.373Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ac/81ee4b728e70e8ba66a589f86469925ead02ed6f8973434e4a52e3576148/grpcio_tools-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:87e648759b06133199f4bc0c0053e3819f4ec3b900dc399e1097b6065db998b5", size = 3654896, upload-time = "2026-02-06T09:57:45.402Z" }, + { url = "https://files.pythonhosted.org/packages/be/b9/facb3430ee427c800bb1e39588c85685677ea649491d6e0874bd9f3a1c0e/grpcio_tools-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f3d3ced52bfe39eba3d24f5a8fab4e12d071959384861b41f0c52ca5399d6920", size = 3322529, upload-time = "2026-02-06T09:57:47.292Z" }, + { url = "https://files.pythonhosted.org/packages/c7/de/d7a011df9abfed8c30f0d2077b0562a6e3edc57cb3e5514718e2a81f370a/grpcio_tools-1.78.0-cp310-cp310-win32.whl", hash = "sha256:4bb6ed690d417b821808796221bde079377dff98fdc850ac157ad2f26cda7a36", size = 993518, upload-time = "2026-02-06T09:57:48.836Z" }, + { url = "https://files.pythonhosted.org/packages/c8/5e/f7f60c3ae2281c6b438c3a8455f4a5d5d2e677cf20207864cbee3763da22/grpcio_tools-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c676d8342fd53bd85a5d5f0d070cd785f93bc040510014708ede6fcb32fada1", size = 1158505, upload-time = "2026-02-06T09:57:50.633Z" }, + { url = "https://files.pythonhosted.org/packages/75/78/280184d19242ed6762bf453c47a70b869b3c5c72a24dc5bf2bf43909faa3/grpcio_tools-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:6a8b8b7b49f319d29dbcf507f62984fa382d1d10437d75c3f26db5f09c4ac0af", size = 2545904, upload-time = "2026-02-06T09:57:52.769Z" }, + { url = "https://files.pythonhosted.org/packages/5b/51/3c46dea5113f68fe879961cae62d34bb7a3c308a774301b45d614952ee98/grpcio_tools-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:d62cf3b68372b0c6d722a6165db41b976869811abeabc19c8522182978d8db10", size = 5709078, upload-time = "2026-02-06T09:57:56.389Z" }, + { url = "https://files.pythonhosted.org/packages/e0/2c/dc1ae9ec53182c96d56dfcbf3bcd3e55a8952ad508b188c75bf5fc8993d4/grpcio_tools-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fa9056742efeaf89d5fe14198af71e5cbc4fbf155d547b89507e19d6025906c6", size = 2591744, upload-time = "2026-02-06T09:57:58.341Z" }, + { url = "https://files.pythonhosted.org/packages/04/63/9b53fc9a9151dd24386785171a4191ee7cb5afb4d983b6a6a87408f41b28/grpcio_tools-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e3191af125dcb705aa6bc3856ba81ba99b94121c1b6ebee152e66ea084672831", size = 2905113, upload-time = "2026-02-06T09:58:00.38Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/0ad8d789f3a2a00893131c140865605fa91671a6e6fcf9da659e1fabba10/grpcio_tools-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:283239ddbb67ae83fac111c61b25d8527a1dbd355b377cbc8383b79f1329944d", size = 2656436, upload-time = "2026-02-06T09:58:03.038Z" }, + { url = "https://files.pythonhosted.org/packages/09/4d/580f47ce2fc61b093ade747b378595f51b4f59972dd39949f7444b464a03/grpcio_tools-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac977508c0db15301ef36d6c79769ec1a6cc4e3bc75735afca7fe7e360cead3a", size = 3106128, upload-time = "2026-02-06T09:58:05.064Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/d83b2d89f8d10e438bad36b1eb29356510fb97e81e6a608b22ae1890e8e6/grpcio_tools-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4ff605e25652a0bd13aa8a73a09bc48669c68170902f5d2bf1468a57d5e78771", size = 3654953, upload-time = "2026-02-06T09:58:07.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/917ce85633311e54fefd7e6eb1224fb780ef317a4d092766f5630c3fc419/grpcio_tools-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0197d7b561c79be78ab93d0fe2836c8def470683df594bae3ac89dd8e5c821b2", size = 3322630, upload-time = "2026-02-06T09:58:10.305Z" }, + { url = "https://files.pythonhosted.org/packages/b2/55/3fbf6b26ab46fc79e1e6f7f4e0993cf540263dad639290299fad374a0829/grpcio_tools-1.78.0-cp311-cp311-win32.whl", hash = "sha256:28f71f591f7f39555863ced84fcc209cbf4454e85ef957232f43271ee99af577", size = 993804, upload-time = "2026-02-06T09:58:13.698Z" }, + { url = "https://files.pythonhosted.org/packages/73/86/4affe006d9e1e9e1c6653d6aafe2f8b9188acb2b563cd8ed3a2c7c0e8aec/grpcio_tools-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a6de495dabf86a3b40b9a7492994e1232b077af9d63080811838b781abbe4e8", size = 1158566, upload-time = "2026-02-06T09:58:15.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ae/5b1fa5dd8d560a6925aa52de0de8731d319f121c276e35b9b2af7cc220a2/grpcio_tools-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:9eb122da57d4cad7d339fc75483116f0113af99e8d2c67f3ef9cae7501d806e4", size = 2546823, upload-time = "2026-02-06T09:58:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ed/d33ccf7fa701512efea7e7e23333b748848a123e9d3bbafde4e126784546/grpcio_tools-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d0c501b8249940b886420e6935045c44cb818fa6f265f4c2b97d5cff9cb5e796", size = 5706776, upload-time = "2026-02-06T09:58:20.944Z" }, + { url = "https://files.pythonhosted.org/packages/c6/69/4285583f40b37af28277fc6b867d636e3b10e1b6a7ebd29391a856e1279b/grpcio_tools-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:77e5aa2d2a7268d55b1b113f958264681ef1994c970f69d48db7d4683d040f57", size = 2593972, upload-time = "2026-02-06T09:58:23.29Z" }, + { url = "https://files.pythonhosted.org/packages/d7/eb/ecc1885bd6b3147f0a1b7dff5565cab72f01c8f8aa458f682a1c77a9fb08/grpcio_tools-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8e3c0b0e6ba5275322ba29a97bf890565a55f129f99a21b121145e9e93a22525", size = 2905531, upload-time = "2026-02-06T09:58:25.406Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a9/511d0040ced66960ca10ba0f082d6b2d2ee6dd61837b1709636fdd8e23b4/grpcio_tools-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975d4cb48694e20ebd78e1643e5f1cd94cdb6a3d38e677a8e84ae43665aa4790", size = 2656909, upload-time = "2026-02-06T09:58:28.022Z" }, + { url = "https://files.pythonhosted.org/packages/06/a3/3d2c707e7dee8df842c96fbb24feb2747e506e39f4a81b661def7fed107c/grpcio_tools-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:553ff18c5d52807dedecf25045ae70bad7a3dbba0b27a9a3cdd9bcf0a1b7baec", size = 3109778, upload-time = "2026-02-06T09:58:30.091Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4b/646811ba241bf05da1f0dc6f25764f1c837f78f75b4485a4210c84b79eae/grpcio_tools-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8c7f5e4af5a84d2e96c862b1a65e958a538237e268d5f8203a3a784340975b51", size = 3658763, upload-time = "2026-02-06T09:58:32.875Z" }, + { url = "https://files.pythonhosted.org/packages/45/de/0a5ef3b3e79d1011375f5580dfee3a9c1ccb96c5f5d1c74c8cee777a2483/grpcio_tools-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:96183e2b44afc3f9a761e9d0f985c3b44e03e8bb98e626241a6cbfb3b6f7e88f", size = 3325116, upload-time = "2026-02-06T09:58:34.894Z" }, + { url = "https://files.pythonhosted.org/packages/95/d2/6391b241ad571bc3e71d63f957c0b1860f0c47932d03c7f300028880f9b8/grpcio_tools-1.78.0-cp312-cp312-win32.whl", hash = "sha256:2250e8424c565a88573f7dc10659a0b92802e68c2a1d57e41872c9b88ccea7a6", size = 993493, upload-time = "2026-02-06T09:58:37.242Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8f/7d0d3a39ecad76ccc136be28274daa660569b244fa7d7d0bbb24d68e5ece/grpcio_tools-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:217d1fa29de14d9c567d616ead7cb0fef33cde36010edff5a9390b00d52e5094", size = 1158423, upload-time = "2026-02-06T09:58:40.072Z" }, + { url = "https://files.pythonhosted.org/packages/53/ce/17311fb77530420e2f441e916b347515133e83d21cd6cc77be04ce093d5b/grpcio_tools-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:2d6de1cc23bdc1baafc23e201b1e48c617b8c1418b4d8e34cebf72141676e5fb", size = 2546284, upload-time = "2026-02-06T09:58:43.073Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/79e101483115f0e78223397daef71751b75eba7e92a32060c10aae11ca64/grpcio_tools-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2afeaad88040894c76656202ff832cb151bceb05c0e6907e539d129188b1e456", size = 5705653, upload-time = "2026-02-06T09:58:45.533Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a7/52fa3ccb39ceeee6adc010056eadfbca8198651c113e418dafebbdf2b306/grpcio_tools-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:33cc593735c93c03d63efe7a8ba25f3c66f16c52f0651910712490244facad72", size = 2592788, upload-time = "2026-02-06T09:58:48.918Z" }, + { url = "https://files.pythonhosted.org/packages/68/08/682ff6bb548225513d73dc9403742d8975439d7469c673bc534b9bbc83a7/grpcio_tools-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2921d7989c4d83b71f03130ab415fa4d66e6693b8b8a1fcbb7a1c67cff19b812", size = 2905157, upload-time = "2026-02-06T09:58:51.478Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/264f3836a96423b7018e5ada79d62576a6401f6da4e1f4975b18b2be1265/grpcio_tools-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6a0df438e82c804c7b95e3f311c97c2f876dcc36376488d5b736b7bcf5a9b45", size = 2656166, upload-time = "2026-02-06T09:58:54.117Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/f108276611522e03e98386b668cc7e575eff6952f2db9caa15b2a3b3e883/grpcio_tools-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9c6070a9500798225191ef25d0055a15d2c01c9c8f2ee7b681fffa99c98c822", size = 3109110, upload-time = "2026-02-06T09:58:56.891Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c7/cf048dbcd64b3396b3c860a2ffbcc67a8f8c87e736aaa74c2e505a7eee4c/grpcio_tools-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:394e8b57d85370a62e5b0a4d64c96fcf7568345c345d8590c821814d227ecf1d", size = 3657863, upload-time = "2026-02-06T09:58:59.176Z" }, + { url = "https://files.pythonhosted.org/packages/b6/37/e2736912c8fda57e2e57a66ea5e0bc8eb9a5fb7ded00e866ad22d50afb08/grpcio_tools-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3ef700293ab375e111a2909d87434ed0a0b086adf0ce67a8d9cf12ea7765e63", size = 3324748, upload-time = "2026-02-06T09:59:01.242Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/726abc75bb5bfc2841e88ea05896e42f51ca7c30cb56da5c5b63058b3867/grpcio_tools-1.78.0-cp313-cp313-win32.whl", hash = "sha256:6993b960fec43a8d840ee5dc20247ef206c1a19587ea49fe5e6cc3d2a09c1585", size = 993074, upload-time = "2026-02-06T09:59:03.085Z" }, + { url = "https://files.pythonhosted.org/packages/c5/68/91b400bb360faf9b177ffb5540ec1c4d06ca923691ddf0f79e2c9683f4da/grpcio_tools-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:275ce3c2978842a8cf9dd88dce954e836e590cf7029649ad5d1145b779039ed5", size = 1158185, upload-time = "2026-02-06T09:59:05.036Z" }, + { url = "https://files.pythonhosted.org/packages/cf/5e/278f3831c8d56bae02e3acc570465648eccf0a6bbedcb1733789ac966803/grpcio_tools-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:8b080d0d072e6032708a3a91731b808074d7ab02ca8fb9847b6a011fdce64cd9", size = 2546270, upload-time = "2026-02-06T09:59:07.426Z" }, + { url = "https://files.pythonhosted.org/packages/a3/d9/68582f2952b914b60dddc18a2e3f9c6f09af9372b6f6120d6cf3ec7f8b4e/grpcio_tools-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8c0ad8f8f133145cd7008b49cb611a5c6a9d89ab276c28afa17050516e801f79", size = 5705731, upload-time = "2026-02-06T09:59:09.856Z" }, + { url = "https://files.pythonhosted.org/packages/70/68/feb0f9a48818ee1df1e8b644069379a1e6ef5447b9b347c24e96fd258e5d/grpcio_tools-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2f8ea092a7de74c6359335d36f0674d939a3c7e1a550f4c2c9e80e0226de8fe4", size = 2593896, upload-time = "2026-02-06T09:59:12.23Z" }, + { url = "https://files.pythonhosted.org/packages/1f/08/a430d8d06e1b8d33f3e48d3f0cc28236723af2f35e37bd5c8db05df6c3aa/grpcio_tools-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:da422985e0cac822b41822f43429c19ecb27c81ffe3126d0b74e77edec452608", size = 2905298, upload-time = "2026-02-06T09:59:14.458Z" }, + { url = "https://files.pythonhosted.org/packages/71/0a/348c36a3eae101ca0c090c9c3bc96f2179adf59ee0c9262d11cdc7bfe7db/grpcio_tools-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4fab1faa3fbcb246263e68da7a8177d73772283f9db063fb8008517480888d26", size = 2656186, upload-time = "2026-02-06T09:59:16.949Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3f/18219f331536fad4af6207ade04142292faa77b5cb4f4463787988963df8/grpcio_tools-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dd9c094f73f734becae3f20f27d4944d3cd8fb68db7338ee6c58e62fc5c3d99f", size = 3109859, upload-time = "2026-02-06T09:59:19.202Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d9/341ea20a44c8e5a3a18acc820b65014c2e3ea5b4f32a53d14864bcd236bc/grpcio_tools-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2ed51ce6b833068f6c580b73193fc2ec16468e6bc18354bc2f83a58721195a58", size = 3657915, upload-time = "2026-02-06T09:59:21.839Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f4/5978b0f91611a64371424c109dd0027b247e5b39260abad2eaee66b6aa37/grpcio_tools-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:05803a5cdafe77c8bdf36aa660ad7a6a1d9e49bc59ce45c1bade2a4698826599", size = 3324724, upload-time = "2026-02-06T09:59:24.402Z" }, + { url = "https://files.pythonhosted.org/packages/b2/80/96a324dba99cfbd20e291baf0b0ae719dbb62b76178c5ce6c788e7331cb1/grpcio_tools-1.78.0-cp314-cp314-win32.whl", hash = "sha256:f7c722e9ce6f11149ac5bddd5056e70aaccfd8168e74e9d34d8b8b588c3f5c7c", size = 1015505, upload-time = "2026-02-06T09:59:26.3Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d1/909e6a05bfd44d46327dc4b8a78beb2bae4fb245ffab2772e350081aaf7e/grpcio_tools-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d58ade518b546120ec8f0a8e006fc8076ae5df151250ebd7e82e9b5e152c229", size = 1190196, upload-time = "2026-02-06T09:59:28.359Z" }, +] + +[[package]] +name = "hiero-sdk-python" +source = { editable = "." } +dependencies = [ + { name = "cryptography" }, + { name = "eth-abi" }, + { name = "grpcio" }, + { name = "protobuf" }, + { name = "pycryptodome" }, + { name = "python-dotenv" }, + { name = "requests" }, +] + +[package.optional-dependencies] +eth = [ + { name = "eth-keys" }, + { name = "rlp" }, +] +tck = [ + { name = "flask" }, +] + +[package.dev-dependencies] +dev = [ + { name = "grpcio-tools" }, + { name = "hypothesis" }, + { name = "pytest" }, + { name = "pytest-cov" }, +] +lint = [ + { name = "mypy" }, + { name = "ruff" }, + { name = "typing-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "cryptography", specifier = ">=44.0.1,<47" }, + { name = "eth-abi", specifier = ">=5.1.0,<6" }, + { name = "eth-keys", marker = "extra == 'eth'", specifier = ">=0.7.0,<0.8" }, + { name = "flask", marker = "extra == 'tck'", specifier = ">=3.0.0,<4" }, + { name = "grpcio", specifier = ">=1.76.0,<2" }, + { name = "protobuf", specifier = ">=4.21.12,<8" }, + { name = "pycryptodome", specifier = ">=3.18.0,<4" }, + { name = "python-dotenv", specifier = ">=1.2.1,<3" }, + { name = "requests", specifier = ">=2.31.0,<3" }, + { name = "rlp", marker = "extra == 'eth'", specifier = ">=4.0.1,<5" }, +] +provides-extras = ["eth", "tck"] + +[package.metadata.requires-dev] +dev = [ + { name = "grpcio-tools", specifier = ">=1.76.0,<2" }, + { name = "hypothesis", specifier = ">=6.137.2" }, + { name = "pytest", specifier = ">=8.3.4,<10" }, + { name = "pytest-cov", specifier = ">=7.0.0,<8" }, +] +lint = [ + { name = "mypy", specifier = ">=1.18.2,<2" }, + { name = "ruff", specifier = ">=0.14.9,<1" }, + { name = "typing-extensions", specifier = ">=4.15.0,<5" }, +] + +[[package]] +name = "hypothesis" +version = "6.151.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/58/41af0d539b3c95644d1e4e353cbd6ac9473e892ea21802546a8886b79078/hypothesis-6.151.11.tar.gz", hash = "sha256:f33dcb68b62c7b07c9ac49664989be898fa8ce57583f0dc080259a197c6c7ff1", size = 463779, upload-time = "2026-04-05T17:35:55.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/06/f49393eca84b87b17a67aaebf9f6251190ba1e9fe9f2236504049fc43fee/hypothesis-6.151.11-py3-none-any.whl", hash = "sha256:7ac05173206746cec8312f95164a30a4eb4916815413a278922e63ff1e404648", size = 529572, upload-time = "2026-04-05T17:35:53.438Z" }, +] + +[[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, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[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, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[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, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[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, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "librt" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/3f/4ca7dd7819bf8ff303aca39c3c60e5320e46e766ab7f7dd627d3b9c11bdf/librt-0.8.0.tar.gz", hash = "sha256:cb74cdcbc0103fc988e04e5c58b0b31e8e5dd2babb9182b6f9490488eb36324b", size = 177306, upload-time = "2026-02-12T14:53:54.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/e9/018cfd60629e0404e6917943789800aa2231defbea540a17b90cc4547b97/librt-0.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db63cf3586a24241e89ca1ce0b56baaec9d371a328bd186c529b27c914c9a1ef", size = 65690, upload-time = "2026-02-12T14:51:57.761Z" }, + { url = "https://files.pythonhosted.org/packages/b5/80/8d39980860e4d1c9497ee50e5cd7c4766d8cfd90d105578eae418e8ffcbc/librt-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba9d9e60651615bc614be5e21a82cdb7b1769a029369cf4b4d861e4f19686fb6", size = 68373, upload-time = "2026-02-12T14:51:59.013Z" }, + { url = "https://files.pythonhosted.org/packages/2d/76/6e6f7a443af63977e421bd542551fec4072d9eaba02e671b05b238fe73bc/librt-0.8.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb4b3ad543084ed79f186741470b251b9d269cd8b03556f15a8d1a99a64b7de5", size = 197091, upload-time = "2026-02-12T14:52:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/14/40/fa064181c231334c9f4cb69eb338132d39510c8928e84beba34b861d0a71/librt-0.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d2720335020219197380ccfa5c895f079ac364b4c429e96952cd6509934d8eb", size = 207350, upload-time = "2026-02-12T14:52:02.32Z" }, + { url = "https://files.pythonhosted.org/packages/50/49/e7f8438dd226305e3e5955d495114ad01448e6a6ffc0303289b4153b5fc5/librt-0.8.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726305d3e53419d27fc8cdfcd3f9571f0ceae22fa6b5ea1b3662c2e538f833e", size = 219962, upload-time = "2026-02-12T14:52:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/1f/2c/74086fc5d52e77107a3cc80a9a3209be6ad1c9b6bc99969d8d9bbf9fdfe4/librt-0.8.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3d107f603b5ee7a79b6aa6f166551b99b32fb4a5303c4dfcb4222fc6a0335e", size = 212939, upload-time = "2026-02-12T14:52:05.537Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ae/d6917c0ebec9bc2e0293903d6a5ccc7cdb64c228e529e96520b277318f25/librt-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41064a0c07b4cc7a81355ccc305cb097d6027002209ffca51306e65ee8293630", size = 221393, upload-time = "2026-02-12T14:52:07.164Z" }, + { url = "https://files.pythonhosted.org/packages/04/97/15df8270f524ce09ad5c19cbbe0e8f95067582507149a6c90594e7795370/librt-0.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c6e4c10761ddbc0d67d2f6e2753daf99908db85d8b901729bf2bf5eaa60e0567", size = 216721, upload-time = "2026-02-12T14:52:08.857Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/17cbcf9b7a1bae5016d9d3561bc7169b32c3bd216c47d934d3f270602c0c/librt-0.8.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ba581acad5ac8f33e2ff1746e8a57e001b47c6721873121bf8bbcf7ba8bd3aa4", size = 214790, upload-time = "2026-02-12T14:52:10.033Z" }, + { url = "https://files.pythonhosted.org/packages/2a/2d/010a236e8dc4d717dd545c46fd036dcced2c7ede71ef85cf55325809ff92/librt-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bdab762e2c0b48bab76f1a08acb3f4c77afd2123bedac59446aeaaeed3d086cf", size = 237384, upload-time = "2026-02-12T14:52:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/f1c0eff3df8760dee761029efb72991c554d9f3282f1048e8c3d0eb60997/librt-0.8.0-cp310-cp310-win32.whl", hash = "sha256:6a3146c63220d814c4a2c7d6a1eacc8d5c14aed0ff85115c1dfea868080cd18f", size = 54289, upload-time = "2026-02-12T14:52:12.798Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0b/2684d473e64890882729f91866ed97ccc0a751a0afc3b4bf1a7b57094dbb/librt-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:bbebd2bba5c6ae02907df49150e55870fdd7440d727b6192c46b6f754723dde9", size = 61347, upload-time = "2026-02-12T14:52:13.793Z" }, + { url = "https://files.pythonhosted.org/packages/51/e9/42af181c89b65abfd557c1b017cba5b82098eef7bf26d1649d82ce93ccc7/librt-0.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ce33a9778e294507f3a0e3468eccb6a698b5166df7db85661543eca1cfc5369", size = 65314, upload-time = "2026-02-12T14:52:14.778Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4a/15a847fca119dc0334a4b8012b1e15fdc5fc19d505b71e227eaf1bcdba09/librt-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8070aa3368559de81061ef752770d03ca1f5fc9467d4d512d405bd0483bfffe6", size = 68015, upload-time = "2026-02-12T14:52:15.797Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/ffc8dbd6ab68dd91b736c88529411a6729649d2b74b887f91f3aaff8d992/librt-0.8.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:20f73d4fecba969efc15cdefd030e382502d56bb6f1fc66b580cce582836c9fa", size = 194508, upload-time = "2026-02-12T14:52:16.835Z" }, + { url = "https://files.pythonhosted.org/packages/89/92/a7355cea28d6c48ff6ff5083ac4a2a866fb9b07b786aa70d1f1116680cd5/librt-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a512c88900bdb1d448882f5623a0b1ad27ba81a9bd75dacfe17080b72272ca1f", size = 205630, upload-time = "2026-02-12T14:52:18.58Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5e/54509038d7ac527828db95b8ba1c8f5d2649bc32fd8f39b1718ec9957dce/librt-0.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:015e2dde6e096d27c10238bf9f6492ba6c65822dfb69d2bf74c41a8e88b7ddef", size = 218289, upload-time = "2026-02-12T14:52:20.134Z" }, + { url = "https://files.pythonhosted.org/packages/6d/17/0ee0d13685cefee6d6f2d47bb643ddad3c62387e2882139794e6a5f1288a/librt-0.8.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c25a131013eadd3c600686a0c0333eb2896483cbc7f65baa6a7ee761017aef9", size = 211508, upload-time = "2026-02-12T14:52:21.413Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a8/1714ef6e9325582e3727de3be27e4c1b2f428ea411d09f1396374180f130/librt-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:21b14464bee0b604d80a638cf1ee3148d84ca4cc163dcdcecb46060c1b3605e4", size = 219129, upload-time = "2026-02-12T14:52:22.61Z" }, + { url = "https://files.pythonhosted.org/packages/89/d3/2d9fe353edff91cdc0ece179348054a6fa61f3de992c44b9477cb973509b/librt-0.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:05a3dd3f116747f7e1a2b475ccdc6fb637fd4987126d109e03013a79d40bf9e6", size = 213126, upload-time = "2026-02-12T14:52:23.819Z" }, + { url = "https://files.pythonhosted.org/packages/ad/8e/9f5c60444880f6ad50e3ff7475e5529e787797e7f3ad5432241633733b92/librt-0.8.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fa37f99bff354ff191c6bcdffbc9d7cdd4fc37faccfc9be0ef3a4fd5613977da", size = 212279, upload-time = "2026-02-12T14:52:25.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/eb/d4a2cfa647da3022ae977f50d7eda1d91f70d7d1883cf958a4b6ef689eab/librt-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1566dbb9d1eb0987264c9b9460d212e809ba908d2f4a3999383a84d765f2f3f1", size = 234654, upload-time = "2026-02-12T14:52:26.204Z" }, + { url = "https://files.pythonhosted.org/packages/6a/31/26b978861c7983b036a3aea08bdbb2ec32bbaab1ad1d57c5e022be59afc1/librt-0.8.0-cp311-cp311-win32.whl", hash = "sha256:70defb797c4d5402166787a6b3c66dfb3fa7f93d118c0509ffafa35a392f4258", size = 54603, upload-time = "2026-02-12T14:52:27.342Z" }, + { url = "https://files.pythonhosted.org/packages/d0/78/f194ed7c48dacf875677e749c5d0d1d69a9daa7c994314a39466237fb1be/librt-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:db953b675079884ffda33d1dca7189fb961b6d372153750beb81880384300817", size = 61730, upload-time = "2026-02-12T14:52:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/97/ee/ad71095478d02137b6f49469dc808c595cfe89b50985f6b39c5345f0faab/librt-0.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:75d1a8cab20b2043f03f7aab730551e9e440adc034d776f15f6f8d582b0a5ad4", size = 52274, upload-time = "2026-02-12T14:52:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fb/53/f3bc0c4921adb0d4a5afa0656f2c0fbe20e18e3e0295e12985b9a5dc3f55/librt-0.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:17269dd2745dbe8e42475acb28e419ad92dfa38214224b1b01020b8cac70b645", size = 66511, upload-time = "2026-02-12T14:52:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/89/4b/4c96357432007c25a1b5e363045373a6c39481e49f6ba05234bb59a839c1/librt-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f4617cef654fca552f00ce5ffdf4f4b68770f18950e4246ce94629b789b92467", size = 68628, upload-time = "2026-02-12T14:52:31.491Z" }, + { url = "https://files.pythonhosted.org/packages/47/16/52d75374d1012e8fc709216b5eaa25f471370e2a2331b8be00f18670a6c7/librt-0.8.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5cb11061a736a9db45e3c1293cfcb1e3caf205912dfa085734ba750f2197ff9a", size = 198941, upload-time = "2026-02-12T14:52:32.489Z" }, + { url = "https://files.pythonhosted.org/packages/fc/11/d5dd89e5a2228567b1228d8602d896736247424484db086eea6b8010bcba/librt-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb00bd71b448f16749909b08a0ff16f58b079e2261c2e1000f2bbb2a4f0a45", size = 210009, upload-time = "2026-02-12T14:52:33.634Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/fc1a92a77c3020ee08ce2dc48aed4b42ab7c30fb43ce488d388673b0f164/librt-0.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95a719a049f0eefaf1952673223cf00d442952273cbd20cf2ed7ec423a0ef58d", size = 224461, upload-time = "2026-02-12T14:52:34.868Z" }, + { url = "https://files.pythonhosted.org/packages/7f/98/eb923e8b028cece924c246104aa800cf72e02d023a8ad4ca87135b05a2fe/librt-0.8.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bd32add59b58fba3439d48d6f36ac695830388e3da3e92e4fc26d2d02670d19c", size = 217538, upload-time = "2026-02-12T14:52:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/fd/67/24e80ab170674a1d8ee9f9a83081dca4635519dbd0473b8321deecddb5be/librt-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f764b2424cb04524ff7a486b9c391e93f93dc1bd8305b2136d25e582e99aa2f", size = 225110, upload-time = "2026-02-12T14:52:37.301Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c7/6fbdcbd1a6e5243c7989c21d68ab967c153b391351174b4729e359d9977f/librt-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f04ca50e847abc486fa8f4107250566441e693779a5374ba211e96e238f298b9", size = 217758, upload-time = "2026-02-12T14:52:38.89Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bd/4d6b36669db086e3d747434430073e14def032dd58ad97959bf7e2d06c67/librt-0.8.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9ab3a3475a55b89b87ffd7e6665838e8458e0b596c22e0177e0f961434ec474a", size = 218384, upload-time = "2026-02-12T14:52:40.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/2d/afe966beb0a8f179b132f3e95c8dd90738a23e9ebdba10f89a3f192f9366/librt-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e36a8da17134ffc29373775d88c04832f9ecfab1880470661813e6c7991ef79", size = 241187, upload-time = "2026-02-12T14:52:43.55Z" }, + { url = "https://files.pythonhosted.org/packages/02/d0/6172ea4af2b538462785ab1a68e52d5c99cfb9866a7caf00fdf388299734/librt-0.8.0-cp312-cp312-win32.whl", hash = "sha256:4eb5e06ebcc668677ed6389164f52f13f71737fc8be471101fa8b4ce77baeb0c", size = 54914, upload-time = "2026-02-12T14:52:44.676Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cb/ceb6ed6175612a4337ad49fb01ef594712b934b4bc88ce8a63554832eb44/librt-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:0a33335eb59921e77c9acc05d0e654e4e32e45b014a4d61517897c11591094f8", size = 62020, upload-time = "2026-02-12T14:52:45.676Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7e/61701acbc67da74ce06ddc7ba9483e81c70f44236b2d00f6a4bfee1aacbf/librt-0.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:24a01c13a2a9bdad20997a4443ebe6e329df063d1978bbe2ebbf637878a46d1e", size = 52443, upload-time = "2026-02-12T14:52:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/6d/32/3edb0bcb4113a9c8bdcd1750663a54565d255027657a5df9d90f13ee07fa/librt-0.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7f820210e21e3a8bf8fde2ae3c3d10106d4de9ead28cbfdf6d0f0f41f5b12fa1", size = 66522, upload-time = "2026-02-12T14:52:48.219Z" }, + { url = "https://files.pythonhosted.org/packages/30/ab/e8c3d05e281f5d405ebdcc5bc8ab36df23e1a4b40ac9da8c3eb9928b72b9/librt-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4831c44b8919e75ca0dfb52052897c1ef59fdae19d3589893fbd068f1e41afbf", size = 68658, upload-time = "2026-02-12T14:52:50.351Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d3/74a206c47b7748bbc8c43942de3ed67de4c231156e148b4f9250869593df/librt-0.8.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:88c6e75540f1f10f5e0fc5e87b4b6c290f0e90d1db8c6734f670840494764af8", size = 199287, upload-time = "2026-02-12T14:52:51.938Z" }, + { url = "https://files.pythonhosted.org/packages/fa/29/ef98a9131cf12cb95771d24e4c411fda96c89dc78b09c2de4704877ebee4/librt-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9646178cd794704d722306c2c920c221abbf080fede3ba539d5afdec16c46dad", size = 210293, upload-time = "2026-02-12T14:52:53.128Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3e/89b4968cb08c53d4c2d8b02517081dfe4b9e07a959ec143d333d76899f6c/librt-0.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e1af31a710e17891d9adf0dbd9a5fcd94901a3922a96499abdbf7ce658f4e01", size = 224801, upload-time = "2026-02-12T14:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/6d/28/f38526d501f9513f8b48d78e6be4a241e15dd4b000056dc8b3f06ee9ce5d/librt-0.8.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:507e94f4bec00b2f590fbe55f48cd518a208e2474a3b90a60aa8f29136ddbada", size = 218090, upload-time = "2026-02-12T14:52:55.758Z" }, + { url = "https://files.pythonhosted.org/packages/02/ec/64e29887c5009c24dc9c397116c680caffc50286f62bd99c39e3875a2854/librt-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f1178e0de0c271231a660fbef9be6acdfa1d596803464706862bef6644cc1cae", size = 225483, upload-time = "2026-02-12T14:52:57.375Z" }, + { url = "https://files.pythonhosted.org/packages/ee/16/7850bdbc9f1a32d3feff2708d90c56fc0490b13f1012e438532781aa598c/librt-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:71fc517efc14f75c2f74b1f0a5d5eb4a8e06aa135c34d18eaf3522f4a53cd62d", size = 218226, upload-time = "2026-02-12T14:52:58.534Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4a/166bffc992d65ddefa7c47052010a87c059b44a458ebaf8f5eba384b0533/librt-0.8.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0583aef7e9a720dd40f26a2ad5a1bf2ccbb90059dac2b32ac516df232c701db3", size = 218755, upload-time = "2026-02-12T14:52:59.701Z" }, + { url = "https://files.pythonhosted.org/packages/da/5d/9aeee038bcc72a9cfaaee934463fe9280a73c5440d36bd3175069d2cb97b/librt-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d0f76fc73480d42285c609c0ea74d79856c160fa828ff9aceab574ea4ecfd7b", size = 241617, upload-time = "2026-02-12T14:53:00.966Z" }, + { url = "https://files.pythonhosted.org/packages/64/ff/2bec6b0296b9d0402aa6ec8540aa19ebcb875d669c37800cb43d10d9c3a3/librt-0.8.0-cp313-cp313-win32.whl", hash = "sha256:e79dbc8f57de360f0ed987dc7de7be814b4803ef0e8fc6d3ff86e16798c99935", size = 54966, upload-time = "2026-02-12T14:53:02.042Z" }, + { url = "https://files.pythonhosted.org/packages/08/8d/bf44633b0182996b2c7ea69a03a5c529683fa1f6b8e45c03fe874ff40d56/librt-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:25b3e667cbfc9000c4740b282df599ebd91dbdcc1aa6785050e4c1d6be5329ab", size = 62000, upload-time = "2026-02-12T14:53:03.822Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fd/c6472b8e0eac0925001f75e366cf5500bcb975357a65ef1f6b5749389d3a/librt-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:e9a3a38eb4134ad33122a6d575e6324831f930a771d951a15ce232e0237412c2", size = 52496, upload-time = "2026-02-12T14:53:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/13/79ebfe30cd273d7c0ce37a5f14dc489c5fb8b722a008983db2cfd57270bb/librt-0.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:421765e8c6b18e64d21c8ead315708a56fc24f44075059702e421d164575fdda", size = 66078, upload-time = "2026-02-12T14:53:06.085Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8f/d11eca40b62a8d5e759239a80636386ef88adecb10d1a050b38cc0da9f9e/librt-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:48f84830a8f8ad7918afd743fd7c4eb558728bceab7b0e38fd5a5cf78206a556", size = 68309, upload-time = "2026-02-12T14:53:07.121Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b4/f12ee70a3596db40ff3c88ec9eaa4e323f3b92f77505b4d900746706ec6a/librt-0.8.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f09d4884f882baa39a7e36bbf3eae124c4ca2a223efb91e567381d1c55c6b06", size = 196804, upload-time = "2026-02-12T14:53:08.164Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7e/70dbbdc0271fd626abe1671ad117bcd61a9a88cdc6a10ccfbfc703db1873/librt-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:693697133c3b32aa9b27f040e3691be210e9ac4d905061859a9ed519b1d5a376", size = 206915, upload-time = "2026-02-12T14:53:09.333Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/6b9e05a635d4327608d06b3c1702166e3b3e78315846373446cf90d7b0bf/librt-0.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5512aae4648152abaf4d48b59890503fcbe86e85abc12fb9b096fe948bdd816", size = 221200, upload-time = "2026-02-12T14:53:10.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/6c/e19a3ac53e9414de43a73d7507d2d766cd22d8ca763d29a4e072d628db42/librt-0.8.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:995d24caa6bbb34bcdd4a41df98ac6d1af637cfa8975cb0790e47d6623e70e3e", size = 214640, upload-time = "2026-02-12T14:53:12.342Z" }, + { url = "https://files.pythonhosted.org/packages/30/f0/23a78464788619e8c70f090cfd099cce4973eed142c4dccb99fc322283fd/librt-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b9aef96d7593584e31ef6ac1eb9775355b0099fee7651fae3a15bc8657b67b52", size = 221980, upload-time = "2026-02-12T14:53:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/32/38e21420c5d7aa8a8bd2c7a7d5252ab174a5a8aaec8b5551968979b747bf/librt-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4f6e975377fbc4c9567cb33ea9ab826031b6c7ec0515bfae66a4fb110d40d6da", size = 215146, upload-time = "2026-02-12T14:53:14.8Z" }, + { url = "https://files.pythonhosted.org/packages/bb/00/bd9ecf38b1824c25240b3ad982fb62c80f0a969e6679091ba2b3afb2b510/librt-0.8.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:daae5e955764be8fd70a93e9e5133c75297f8bce1e802e1d3683b98f77e1c5ab", size = 215203, upload-time = "2026-02-12T14:53:16.087Z" }, + { url = "https://files.pythonhosted.org/packages/b9/60/7559bcc5279d37810b98d4a52616febd7b8eef04391714fd6bdf629598b1/librt-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7bd68cebf3131bb920d5984f75fe302d758db33264e44b45ad139385662d7bc3", size = 237937, upload-time = "2026-02-12T14:53:17.236Z" }, + { url = "https://files.pythonhosted.org/packages/41/cc/be3e7da88f1abbe2642672af1dc00a0bccece11ca60241b1883f3018d8d5/librt-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1e6811cac1dcb27ca4c74e0ca4a5917a8e06db0d8408d30daee3a41724bfde7a", size = 50685, upload-time = "2026-02-12T14:53:18.888Z" }, + { url = "https://files.pythonhosted.org/packages/38/27/e381d0df182a8f61ef1f6025d8b138b3318cc9d18ad4d5f47c3bf7492523/librt-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:178707cda89d910c3b28bf5aa5f69d3d4734e0f6ae102f753ad79edef83a83c7", size = 57872, upload-time = "2026-02-12T14:53:19.942Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0c/ca9dfdf00554a44dea7d555001248269a4bab569e1590a91391feb863fa4/librt-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3e8b77b5f54d0937b26512774916041756c9eb3e66f1031971e626eea49d0bf4", size = 48056, upload-time = "2026-02-12T14:53:21.473Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ed/6cc9c4ad24f90c8e782193c7b4a857408fd49540800613d1356c63567d7b/librt-0.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:789911e8fa40a2e82f41120c936b1965f3213c67f5a483fc5a41f5839a05dcbb", size = 68307, upload-time = "2026-02-12T14:53:22.498Z" }, + { url = "https://files.pythonhosted.org/packages/84/d8/0e94292c6b3e00b6eeea39dd44d5703d1ec29b6dafce7eea19dc8f1aedbd/librt-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2b37437e7e4ef5e15a297b36ba9e577f73e29564131d86dd75875705e97402b5", size = 70999, upload-time = "2026-02-12T14:53:23.603Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f4/6be1afcbdeedbdbbf54a7c9d73ad43e1bf36897cebf3978308cd64922e02/librt-0.8.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:671a6152edf3b924d98a5ed5e6982ec9cb30894085482acadce0975f031d4c5c", size = 220782, upload-time = "2026-02-12T14:53:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8d/f306e8caa93cfaf5c6c9e0d940908d75dc6af4fd856baa5535c922ee02b1/librt-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8992ca186a1678107b0af3d0c9303d8c7305981b9914989b9788319ed4d89546", size = 235420, upload-time = "2026-02-12T14:53:27.047Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f2/65d86bd462e9c351326564ca805e8457442149f348496e25ccd94583ffa2/librt-0.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:001e5330093d887b8b9165823eca6c5c4db183fe4edea4fdc0680bbac5f46944", size = 246452, upload-time = "2026-02-12T14:53:28.341Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/39c88b503b4cb3fcbdeb3caa29672b6b44ebee8dcc8a54d49839ac280f3f/librt-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d920789eca7ef71df7f31fd547ec0d3002e04d77f30ba6881e08a630e7b2c30e", size = 238891, upload-time = "2026-02-12T14:53:29.625Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c6/6c0d68190893d01b71b9569b07a1c811e280c0065a791249921c83dc0290/librt-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:82fb4602d1b3e303a58bfe6165992b5a78d823ec646445356c332cd5f5bbaa61", size = 250249, upload-time = "2026-02-12T14:53:30.93Z" }, + { url = "https://files.pythonhosted.org/packages/52/7a/f715ed9e039035d0ea637579c3c0155ab3709a7046bc408c0fb05d337121/librt-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4d3e38797eb482485b486898f89415a6ab163bc291476bd95712e42cf4383c05", size = 240642, upload-time = "2026-02-12T14:53:32.174Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3c/609000a333debf5992efe087edc6467c1fdbdddca5b610355569bbea9589/librt-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a905091a13e0884701226860836d0386b88c72ce5c2fdfba6618e14c72be9f25", size = 239621, upload-time = "2026-02-12T14:53:33.39Z" }, + { url = "https://files.pythonhosted.org/packages/b9/df/87b0673d5c395a8f34f38569c116c93142d4dc7e04af2510620772d6bd4f/librt-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:375eda7acfce1f15f5ed56cfc960669eefa1ec8732e3e9087c3c4c3f2066759c", size = 262986, upload-time = "2026-02-12T14:53:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/09/7f/6bbbe9dcda649684773aaea78b87fff4d7e59550fbc2877faa83612087a3/librt-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:2ccdd20d9a72c562ffb73098ac411de351b53a6fbb3390903b2d33078ef90447", size = 51328, upload-time = "2026-02-12T14:53:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f3/e1981ab6fa9b41be0396648b5850267888a752d025313a9e929c4856208e/librt-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:25e82d920d4d62ad741592fcf8d0f3bda0e3fc388a184cb7d2f566c681c5f7b9", size = 58719, upload-time = "2026-02-12T14:53:37.183Z" }, + { url = "https://files.pythonhosted.org/packages/94/d1/433b3c06e78f23486fe4fdd19bc134657eb30997d2054b0dbf52bbf3382e/librt-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:92249938ab744a5890580d3cb2b22042f0dce71cdaa7c1369823df62bedf7cbc", size = 48753, upload-time = "2026-02-12T14:53:38.539Z" }, +] + +[[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, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { 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, upload-time = "2025-09-27T18:36:30.854Z" }, + { 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, upload-time = "2025-09-27T18:36:31.971Z" }, + { 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, upload-time = "2025-09-27T18:36:32.813Z" }, + { 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, upload-time = "2025-09-27T18:36:33.86Z" }, + { 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, upload-time = "2025-09-27T18:36:35.099Z" }, + { 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, upload-time = "2025-09-27T18:36:36.001Z" }, + { 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, upload-time = "2025-09-27T18:36:36.906Z" }, + { 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, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { 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, upload-time = "2025-09-27T18:36:41.777Z" }, + { 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, upload-time = "2025-09-27T18:36:43.257Z" }, + { 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, upload-time = "2025-09-27T18:36:44.508Z" }, + { 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, upload-time = "2025-09-27T18:36:45.385Z" }, + { 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, upload-time = "2025-09-27T18:36:46.916Z" }, + { 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, upload-time = "2025-09-27T18:36:47.884Z" }, + { 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, upload-time = "2025-09-27T18:36:48.82Z" }, + { 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, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { 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, upload-time = "2025-09-27T18:36:54.819Z" }, + { 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, upload-time = "2025-09-27T18:36:55.714Z" }, + { 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, upload-time = "2025-09-27T18:36:56.908Z" }, + { 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, upload-time = "2025-09-27T18:36:57.913Z" }, + { 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, upload-time = "2025-09-27T18:36:58.833Z" }, + { 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, upload-time = "2025-09-27T18:36:59.739Z" }, + { 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, upload-time = "2025-09-27T18:37:00.719Z" }, + { 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, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { 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, upload-time = "2025-09-27T18:37:06.342Z" }, + { 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, upload-time = "2025-09-27T18:37:07.213Z" }, + { 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, upload-time = "2025-09-27T18:37:09.572Z" }, + { 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, upload-time = "2025-09-27T18:37:10.58Z" }, + { 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, upload-time = "2025-09-27T18:37:11.547Z" }, + { 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, upload-time = "2025-09-27T18:37:12.48Z" }, + { 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, upload-time = "2025-09-27T18:37:13.485Z" }, + { 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, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { 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, upload-time = "2025-09-27T18:37:18.453Z" }, + { 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, upload-time = "2025-09-27T18:37:19.332Z" }, + { 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, upload-time = "2025-09-27T18:37:20.245Z" }, + { 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, upload-time = "2025-09-27T18:37:21.177Z" }, + { 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, upload-time = "2025-09-27T18:37:22.167Z" }, + { 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, upload-time = "2025-09-27T18:37:23.296Z" }, + { 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, upload-time = "2025-09-27T18:37:24.237Z" }, + { 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, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "parsimonious" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/91/abdc50c4ef06fdf8d047f60ee777ca9b2a7885e1a9cea81343fbecda52d7/parsimonious-0.10.0.tar.gz", hash = "sha256:8281600da180ec8ae35427a4ab4f7b82bfec1e3d1e52f80cb60ea82b9512501c", size = 52172, upload-time = "2022-09-03T17:01:17.004Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/0f/c8b64d9b54ea631fcad4e9e3c8dbe8c11bb32a623be94f22974c88e71eaf/parsimonious-0.10.0-py3-none-any.whl", hash = "sha256:982ab435fabe86519b57f6b35610aa4e4e977e9f02a14353edf4bbc75369fc0f", size = 48427, upload-time = "2022-09-03T17:01:13.814Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[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, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pycryptodome" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, + { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, + { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, + { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" }, + { url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" }, + { url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" }, + { url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[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, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[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, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "regex" +version = "2026.1.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/d2/e6ee96b7dff201a83f650241c52db8e5bd080967cb93211f57aa448dc9d6/regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e", size = 488166, upload-time = "2026-01-14T23:13:46.408Z" }, + { url = "https://files.pythonhosted.org/packages/23/8a/819e9ce14c9f87af026d0690901b3931f3101160833e5d4c8061fa3a1b67/regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f", size = 290632, upload-time = "2026-01-14T23:13:48.688Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c3/23dfe15af25d1d45b07dfd4caa6003ad710dcdcb4c4b279909bdfe7a2de8/regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b", size = 288500, upload-time = "2026-01-14T23:13:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/c6/31/1adc33e2f717df30d2f4d973f8776d2ba6ecf939301efab29fca57505c95/regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c", size = 781670, upload-time = "2026-01-14T23:13:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/23/ce/21a8a22d13bc4adcb927c27b840c948f15fc973e21ed2346c1bd0eae22dc/regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9", size = 850820, upload-time = "2026-01-14T23:13:54.894Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/3eeacdf587a4705a44484cd0b30e9230a0e602811fb3e2cc32268c70d509/regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c", size = 898777, upload-time = "2026-01-14T23:13:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/79/a9/1898a077e2965c35fc22796488141a22676eed2d73701e37c73ad7c0b459/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106", size = 791750, upload-time = "2026-01-14T23:13:58.527Z" }, + { url = "https://files.pythonhosted.org/packages/4c/84/e31f9d149a178889b3817212827f5e0e8c827a049ff31b4b381e76b26e2d/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618", size = 782674, upload-time = "2026-01-14T23:13:59.874Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ff/adf60063db24532add6a1676943754a5654dcac8237af024ede38244fd12/regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4", size = 767906, upload-time = "2026-01-14T23:14:01.298Z" }, + { url = "https://files.pythonhosted.org/packages/af/3e/e6a216cee1e2780fec11afe7fc47b6f3925d7264e8149c607ac389fd9b1a/regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79", size = 774798, upload-time = "2026-01-14T23:14:02.715Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/23a4a8378a9208514ed3efc7e7850c27fa01e00ed8557c958df0335edc4a/regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9", size = 845861, upload-time = "2026-01-14T23:14:04.824Z" }, + { url = "https://files.pythonhosted.org/packages/f8/57/d7605a9d53bd07421a8785d349cd29677fe660e13674fa4c6cbd624ae354/regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220", size = 755648, upload-time = "2026-01-14T23:14:06.371Z" }, + { url = "https://files.pythonhosted.org/packages/6f/76/6f2e24aa192da1e299cc1101674a60579d3912391867ce0b946ba83e2194/regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13", size = 836250, upload-time = "2026-01-14T23:14:08.343Z" }, + { url = "https://files.pythonhosted.org/packages/11/3a/1f2a1d29453299a7858eab7759045fc3d9d1b429b088dec2dc85b6fa16a2/regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3", size = 779919, upload-time = "2026-01-14T23:14:09.954Z" }, + { url = "https://files.pythonhosted.org/packages/c0/67/eab9bc955c9dcc58e9b222c801e39cff7ca0b04261792a2149166ce7e792/regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218", size = 265888, upload-time = "2026-01-14T23:14:11.35Z" }, + { url = "https://files.pythonhosted.org/packages/1d/62/31d16ae24e1f8803bddb0885508acecaec997fcdcde9c243787103119ae4/regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a", size = 277830, upload-time = "2026-01-14T23:14:12.908Z" }, + { url = "https://files.pythonhosted.org/packages/e5/36/5d9972bccd6417ecd5a8be319cebfd80b296875e7f116c37fb2a2deecebf/regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3", size = 270376, upload-time = "2026-01-14T23:14:14.782Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" }, + { url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" }, + { url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" }, + { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" }, + { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" }, + { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" }, + { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" }, + { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" }, + { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" }, + { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164, upload-time = "2026-01-14T23:15:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218, upload-time = "2026-01-14T23:15:15.647Z" }, + { url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895, upload-time = "2026-01-14T23:15:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680, upload-time = "2026-01-14T23:15:19.342Z" }, + { url = "https://files.pythonhosted.org/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210, upload-time = "2026-01-14T23:15:22.005Z" }, + { url = "https://files.pythonhosted.org/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358, upload-time = "2026-01-14T23:15:24.569Z" }, + { url = "https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583, upload-time = "2026-01-14T23:15:26.526Z" }, + { url = "https://files.pythonhosted.org/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782, upload-time = "2026-01-14T23:15:29.352Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978, upload-time = "2026-01-14T23:15:31.295Z" }, + { url = "https://files.pythonhosted.org/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550, upload-time = "2026-01-14T23:15:33.558Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747, upload-time = "2026-01-14T23:15:35.206Z" }, + { url = "https://files.pythonhosted.org/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615, upload-time = "2026-01-14T23:15:37.523Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951, upload-time = "2026-01-14T23:15:39.582Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2a/5928af114441e059f15b2f63e188bd00c6529b3051c974ade7444b85fcda/regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f", size = 266275, upload-time = "2026-01-14T23:15:42.108Z" }, + { url = "https://files.pythonhosted.org/packages/4f/16/5bfbb89e435897bff28cf0352a992ca719d9e55ebf8b629203c96b6ce4f7/regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e", size = 277145, upload-time = "2026-01-14T23:15:44.244Z" }, + { url = "https://files.pythonhosted.org/packages/56/c1/a09ff7392ef4233296e821aec5f78c51be5e91ffde0d163059e50fd75835/regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337", size = 270411, upload-time = "2026-01-14T23:15:45.858Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068, upload-time = "2026-01-14T23:15:47.616Z" }, + { url = "https://files.pythonhosted.org/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756, upload-time = "2026-01-14T23:15:49.307Z" }, + { url = "https://files.pythonhosted.org/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114, upload-time = "2026-01-14T23:15:51.484Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524, upload-time = "2026-01-14T23:15:53.102Z" }, + { url = "https://files.pythonhosted.org/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455, upload-time = "2026-01-14T23:15:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007, upload-time = "2026-01-14T23:15:57.041Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794, upload-time = "2026-01-14T23:15:58.916Z" }, + { url = "https://files.pythonhosted.org/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159, upload-time = "2026-01-14T23:16:00.817Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558, upload-time = "2026-01-14T23:16:03.267Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427, upload-time = "2026-01-14T23:16:05.646Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939, upload-time = "2026-01-14T23:16:07.542Z" }, + { url = "https://files.pythonhosted.org/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753, upload-time = "2026-01-14T23:16:10.395Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559, upload-time = "2026-01-14T23:16:12.347Z" }, + { url = "https://files.pythonhosted.org/packages/a5/61/1bba81ff6d50c86c65d9fd84ce9699dd106438ee4cdb105bf60374ee8412/regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952", size = 268879, upload-time = "2026-01-14T23:16:14.049Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/cef7d4c5fb0ea3ac5c775fd37db5747f7378b29526cc83f572198924ff47/regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10", size = 280317, upload-time = "2026-01-14T23:16:15.718Z" }, + { url = "https://files.pythonhosted.org/packages/b4/52/4317f7a5988544e34ab57b4bde0f04944c4786128c933fb09825924d3e82/regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829", size = 271551, upload-time = "2026-01-14T23:16:17.533Z" }, + { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170, upload-time = "2026-01-14T23:16:19.835Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146, upload-time = "2026-01-14T23:16:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986, upload-time = "2026-01-14T23:16:23.381Z" }, + { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098, upload-time = "2026-01-14T23:16:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980, upload-time = "2026-01-14T23:16:27.297Z" }, + { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607, upload-time = "2026-01-14T23:16:29.235Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358, upload-time = "2026-01-14T23:16:31.369Z" }, + { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833, upload-time = "2026-01-14T23:16:33.141Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045, upload-time = "2026-01-14T23:16:35.005Z" }, + { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374, upload-time = "2026-01-14T23:16:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940, upload-time = "2026-01-14T23:16:38.685Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112, upload-time = "2026-01-14T23:16:40.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586, upload-time = "2026-01-14T23:16:42.799Z" }, + { url = "https://files.pythonhosted.org/packages/93/38/77142422f631e013f316aaae83234c629555729a9fbc952b8a63ac91462a/regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1", size = 271691, upload-time = "2026-01-14T23:16:44.671Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a9/ab16b4649524ca9e05213c1cdbb7faa85cc2aa90a0230d2f796cbaf22736/regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903", size = 280422, upload-time = "2026-01-14T23:16:46.607Z" }, + { url = "https://files.pythonhosted.org/packages/be/2a/20fd057bf3521cb4791f69f869635f73e0aaf2b9ad2d260f728144f9047c/regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705", size = 273467, upload-time = "2026-01-14T23:16:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073, upload-time = "2026-01-14T23:16:51.154Z" }, + { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757, upload-time = "2026-01-14T23:16:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122, upload-time = "2026-01-14T23:16:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761, upload-time = "2026-01-14T23:16:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538, upload-time = "2026-01-14T23:16:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066, upload-time = "2026-01-14T23:17:01.607Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938, upload-time = "2026-01-14T23:17:04.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314, upload-time = "2026-01-14T23:17:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652, upload-time = "2026-01-14T23:17:08.521Z" }, + { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550, upload-time = "2026-01-14T23:17:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981, upload-time = "2026-01-14T23:17:12.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780, upload-time = "2026-01-14T23:17:14.837Z" }, + { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778, upload-time = "2026-01-14T23:17:17.369Z" }, + { url = "https://files.pythonhosted.org/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667, upload-time = "2026-01-14T23:17:19.301Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386, upload-time = "2026-01-14T23:17:21.231Z" }, + { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837, upload-time = "2026-01-14T23:17:23.146Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rlp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/2d/439b0728a92964a04d9c88ea1ca9ebb128893fbbd5834faa31f987f2fd4c/rlp-4.1.0.tar.gz", hash = "sha256:be07564270a96f3e225e2c107db263de96b5bc1f27722d2855bd3459a08e95a9", size = 33429, upload-time = "2025-02-04T22:05:59.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/fb/e4c0ced9893b84ac95b7181d69a9786ce5879aeb3bbbcbba80a164f85d6a/rlp-4.1.0-py3-none-any.whl", hash = "sha256:8eca394c579bad34ee0b937aecb96a57052ff3716e19c7a578883e767bc5da6f", size = 19973, upload-time = "2025-02-04T22:05:57.05Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, + { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, + { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, + { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, + { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, + { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, + { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "toolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, +] From 3d6186c1c02ca49162f8cd92f5b3e1e6205cfa01 Mon Sep 17 00:00:00 2001 From: Abhijeet Saharan Date: Tue, 7 Apr 2026 00:57:48 +0530 Subject: [PATCH 43/60] fix: ensure deterministic installs in codecov workflow Signed-off-by: Abhijeet Saharan --- .github/workflows/pr-check-codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-check-codecov.yml b/.github/workflows/pr-check-codecov.yml index b46e80a36..e9375a53e 100644 --- a/.github/workflows/pr-check-codecov.yml +++ b/.github/workflows/pr-check-codecov.yml @@ -33,7 +33,7 @@ jobs: - name: Install dependencies run: | - uv sync --all-extras --dev + uv sync --all-extras --dev --frozen - name: Generate Proto Files run: uv run python generate_proto.py From 3a0eaf751a2ced4c3efb6e290f5ee3200284f107 Mon Sep 17 00:00:00 2001 From: Abhijeet Saharan Date: Tue, 7 Apr 2026 01:19:29 +0530 Subject: [PATCH 44/60] fix: ensure deterministic installs in pr-check-examples workflow Signed-off-by: Abhijeet Saharan --- .github/workflows/pr-check-examples.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-check-examples.yml b/.github/workflows/pr-check-examples.yml index 2ffedf599..867982f8e 100644 --- a/.github/workflows/pr-check-examples.yml +++ b/.github/workflows/pr-check-examples.yml @@ -27,7 +27,7 @@ jobs: uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - name: Install dependencies - run: uv sync --all-extras + run: uv sync --all-extras --frozen - name: Generate Proto Files run: uv run python generate_proto.py From d1b666f1fb06074c786ade1a4be7ef8f30ad0948 Mon Sep 17 00:00:00 2001 From: Abhijeet Saharan Date: Tue, 7 Apr 2026 01:26:50 +0530 Subject: [PATCH 45/60] fix: ensure deterministic installs in pr-check-test workflow Signed-off-by: Abhijeet Saharan --- .../workflows/pr-check-secondary-unit-integration-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-check-secondary-unit-integration-test.yml b/.github/workflows/pr-check-secondary-unit-integration-test.yml index 8a2dc1dc5..77a73e253 100644 --- a/.github/workflows/pr-check-secondary-unit-integration-test.yml +++ b/.github/workflows/pr-check-secondary-unit-integration-test.yml @@ -59,7 +59,7 @@ jobs: enable-cache: true - name: Install dependencies - run: uv sync --all-extras --dev + run: uv sync --all-extras --dev --frozen - name: Generate Proto Files run: uv run generate_proto.py @@ -122,7 +122,7 @@ jobs: enable-cache: true - name: Install dependencies - run: uv sync --all-extras --dev + run: uv sync --all-extras --dev --frozen - name: Generate Proto Files run: uv run generate_proto.py From 9360ad4cc951ddec5fd74b7c2d37d009c4bbdc28 Mon Sep 17 00:00:00 2001 From: Manish Dait <90558243+manishdait@users.noreply.github.com> Date: Sun, 12 Apr 2026 00:12:35 +0530 Subject: [PATCH 46/60] chore: Refactor Sync Linked Issue Labels Worflow (#2095) Signed-off-by: Manish Dait --- .github/workflows/sync-issue-labels-add.yml | 161 ++++-------- .../workflows/sync-issue-labels-compute.yml | 230 +++++------------- CHANGELOG.md | 1 + 3 files changed, 110 insertions(+), 282 deletions(-) diff --git a/.github/workflows/sync-issue-labels-add.yml b/.github/workflows/sync-issue-labels-add.yml index 49bc8e796..3370ac6f1 100644 --- a/.github/workflows/sync-issue-labels-add.yml +++ b/.github/workflows/sync-issue-labels-add.yml @@ -1,128 +1,61 @@ -name: Add Linked Issue Labels to PR +name: Sync Linked Issue Labels - Apply on: - workflow_dispatch: - inputs: - upstream_run_id: - description: "Upstream compute workflow run ID" - required: true - type: string - pr_number: - description: "Pull request number" - required: true - type: string - dry_run: - description: "Dry run flag" - required: false - type: string - default: "true" - is_fork_pr: - description: "Fork PR flag" - required: false - type: string - default: "false" -defaults: - run: - shell: bash -permissions: - actions: read - issues: write + workflow_run: + workflows: ["Sync Linked Issue Labels - Compute"] + types: [completed] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.workflow_run.id }} + cancel-in-progress: true jobs: - add-labels: - concurrency: - group: sync-issue-labels-pr-${{ github.event.inputs.pr_number }} - cancel-in-progress: true + apply: + if: github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest + permissions: + pull-requests: write + issues: write + actions: read + steps: - - name: Harden the runner + - name: Harden Runner uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit - - - name: Download labels artifact - id: download - continue-on-error: true + + - name: Download Artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + continue-on-error: true with: - name: pr-labels-${{ github.event.inputs.pr_number }} - path: artifacts - run-id: ${{ github.event.inputs.upstream_run_id }} + name: pr-labels-data + run-id: ${{ github.event.workflow_run.id }} github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Read labels payload - id: read - env: - INPUT_PR_NUMBER: ${{ github.event.inputs.pr_number }} - INPUT_IS_FORK_PR: ${{ github.event.inputs.is_fork_pr }} - INPUT_DRY_RUN: ${{ github.event.inputs.dry_run }} - run: | - labels_file="artifacts/labels.json" - if [ ! -f "$labels_file" ]; then - echo "::error::Labels artifact not found. Cross-workflow handoff is broken." - echo "labels=[]" >> "$GITHUB_OUTPUT" - echo "labels_count=0" >> "$GITHUB_OUTPUT" - echo "labels_multiline=" >> "$GITHUB_OUTPUT" - echo "pr_number=$INPUT_PR_NUMBER" >> "$GITHUB_OUTPUT" - echo "is_fork_pr=$INPUT_IS_FORK_PR" >> "$GITHUB_OUTPUT" - echo "dry_run=$INPUT_DRY_RUN" >> "$GITHUB_OUTPUT" - echo "source_event=workflow_dispatch" >> "$GITHUB_OUTPUT" - exit 1 - fi - labels=$(jq -c '.labels // []' "$labels_file") - pr_number=$(jq -r '.pr_number // 0' "$labels_file") - is_fork_pr=$(jq -r '.is_fork_pr // false' "$labels_file") - dry_run=$(jq -r '.dry_run // "true"' "$labels_file") - source_event=$(jq -r '.source_event // ""' "$labels_file") - labels_multiline=$(jq -r '.labels // [] | .[]' "$labels_file") - labels_count=$(echo "$labels" | jq 'length') - echo "labels=$labels" >> "$GITHUB_OUTPUT" - echo "labels_count=$labels_count" >> "$GITHUB_OUTPUT" - { - echo "labels_multiline<> "$GITHUB_OUTPUT" - echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" - echo "is_fork_pr=$is_fork_pr" >> "$GITHUB_OUTPUT" - echo "dry_run=$dry_run" >> "$GITHUB_OUTPUT" - echo "source_event=$source_event" >> "$GITHUB_OUTPUT" - - - name: Validate labels payload - id: validate - run: | - if [ "$PR_NUMBER" = "0" ] || [ "$(echo "$LABELS" | jq -r '. | length')" = "0" ]; then - echo "Invalid payload: pr_number=$PR_NUMBER or labels empty. Skipping label addition." - echo "valid_payload=false" >> "$GITHUB_OUTPUT" - else - echo "valid_payload=true" >> "$GITHUB_OUTPUT" - fi - env: - PR_NUMBER: ${{ steps.read.outputs.pr_number }} - LABELS: ${{ steps.read.outputs.labels }} - - - name: Determine if labels should be applied - id: should_apply - run: | - if [ "${{ steps.read.outputs.is_fork_pr }}" = "true" ]; then - echo "apply=false" >> "$GITHUB_OUTPUT" - echo "reason=fork PR" >> "$GITHUB_OUTPUT" - elif [ "${{ steps.validate.outputs.valid_payload }}" != "true" ]; then - echo "apply=false" >> "$GITHUB_OUTPUT" - echo "reason=invalid payload" >> "$GITHUB_OUTPUT" - elif [ "${{ steps.read.outputs.source_event }}" = "workflow_dispatch" ] && [ "${{ steps.read.outputs.dry_run }}" = "true" ]; then - echo "apply=false" >> "$GITHUB_OUTPUT" - echo "reason=dry run" >> "$GITHUB_OUTPUT" - else - echo "apply=true" >> "$GITHUB_OUTPUT" - echo "reason=" >> "$GITHUB_OUTPUT" - fi - - - name: Add labels to PR - if: ${{ steps.should_apply.outputs.apply == 'true' }} - uses: actions-ecosystem/action-add-labels@1a9c3715c0037e96b97bb38cb4c4b56a1f1d4871 # main + - name: Apply Labels + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github_token: ${{ secrets.GITHUB_TOKEN }} - labels: ${{ steps.read.outputs.labels_multiline }} - number: ${{ steps.read.outputs.pr_number }} - + script: | + const fs = require('fs'); + + if (!fs.existsSync('labels.json')) { + console.log("No payload file found. Nothing to apply."); + return; + } + + const data = JSON.parse(fs.readFileSync('labels.json', 'utf8')); + + if (!data.labels || data.labels.length === 0) { + console.log(`SKIPPING: PR #${data.pr_number} already has all labels or no labels were found.`); + return; + } + + console.log(`Applying labels to PR #${data.pr_number}: ${data.labels.join(', ')}`); + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: data.pr_number, + labels: data.labels + }); + console.log("Successfully applied labels!"); diff --git a/.github/workflows/sync-issue-labels-compute.yml b/.github/workflows/sync-issue-labels-compute.yml index a979b4add..0f282e1a0 100644 --- a/.github/workflows/sync-issue-labels-compute.yml +++ b/.github/workflows/sync-issue-labels-compute.yml @@ -1,212 +1,106 @@ -name: Compute Linked Issue Labels +name: Sync Linked Issue Labels - Compute on: pull_request: - types: [opened, edited, reopened, synchronize, ready_for_review] - workflow_dispatch: - inputs: - pr_number: - description: "PR number to sync labels for" - required: true - type: number - dry-run-enabled: - description: "Dry run (log only, do not apply labels)" - required: false - type: boolean - default: true + types: [opened, edited, reopened, synchronize] permissions: - actions: write - pull-requests: read issues: read contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + jobs: - compute-labels: - concurrency: - group: sync-issue-labels-compute-pr-${{ github.event.pull_request.number || github.event.inputs.pr_number }} - cancel-in-progress: true + sync: runs-on: ubuntu-latest - outputs: - pr_number: ${{ steps.compute.outputs.pr_number }} - dry_run: ${{ steps.compute.outputs.dry_run }} - is_fork_pr: ${{ steps.compute.outputs.is_fork_pr }} steps: - - name: Harden the runner + - name: Harden Runner uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit - - name: Compute linked issue labels + - name: Compute Labels id: compute - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} - DRY_RUN: 'true' - REQUESTED_DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && format('{0}', github.event.inputs['dry-run-enabled']) || 'true' }} - IS_FORK_PR: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork || 'false' }} - MAX_LINKED_ISSUES: '20' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - result-encoding: json script: | - const MAX_LINKED_ISSUES = Number(process.env.MAX_LINKED_ISSUES || "20"); - - function extractLabels(labelData) { - const result = []; - for (const item of labelData) { - const name = typeof item === "string" ? item : item && item.name; - if (name && name.trim()) result.push(name.trim()); - } - return result; - } - - function extractLinkedIssueNumbers(prBody, owner, repo) { - const numbers = new Set(); - const closingRefRegex = /(?:fix|fixes|fixed|close|closes|closed|resolve|resolves|resolved)\s+(?:([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+))?#(\d+)\b/gi; - const lines = String(prBody || "").split(/\r?\n/); - for (const line of lines) { - let m; - while ((m = closingRefRegex.exec(line)) !== null) { - const refOwner = (m[1] || "").toLowerCase(); - const refRepo = (m[2] || "").toLowerCase(); - if (refOwner && refRepo && (refOwner !== owner.toLowerCase() || refRepo !== repo.toLowerCase())) continue; - numbers.add(Number(m[3])); - } - } - const all = Array.from(numbers); - if (all.length > MAX_LINKED_ISSUES) { - console.log(`[sync] Limiting linked issue refs from ${all.length} to ${MAX_LINKED_ISSUES}.`); - } - return all.slice(0, MAX_LINKED_ISSUES); - } - - const prNumber = Number(process.env.PR_NUMBER); - if (!prNumber) { - core.setOutput('has_labels', 'false'); - core.setOutput('labels', '[]'); - core.setOutput('pr_number', ''); - core.setOutput('dry_run', 'true'); - core.setOutput('is_fork_pr', String(process.env.IS_FORK_PR || 'false')); - core.setOutput('source_event', context.eventName); - return; - } + const prNumber = context.payload.pull_request.number; + console.log(`--- Processing PR #${prNumber} ---`); const { data: prData } = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber }); - const prAuthor = (prData.user && prData.user.login) || ""; + const prAuthor = prData.user.login; if (/\[bot\]$/i.test(prAuthor) || /dependabot/i.test(prAuthor)) { - console.log(`[sync] Skipping bot-authored PR from ${prAuthor}.`); - core.setOutput('has_labels', 'false'); - core.setOutput('labels', '[]'); - core.setOutput('pr_number', String(prNumber)); - core.setOutput('dry_run', 'true'); - core.setOutput('is_fork_pr', String(process.env.IS_FORK_PR || 'false')); - core.setOutput('source_event', context.eventName); + console.log(`Skipping bot-authored PR (Author: ${prAuthor})`); return; } - const linkedIssues = extractLinkedIssueNumbers(prData.body || "", context.repo.owner, context.repo.repo); - if (!linkedIssues.length) { - console.log("[sync] No linked issue references found in PR body."); - core.setOutput('has_labels', 'false'); - core.setOutput('labels', '[]'); - core.setOutput('pr_number', String(prNumber)); - core.setOutput('dry_run', 'true'); - core.setOutput('is_fork_pr', String(process.env.IS_FORK_PR || 'false')); - core.setOutput('source_event', context.eventName); + const regex = /(?:fix|fixes|fixed|close|closes|closed|resolve|resolves|resolved)[:\s]+\s*#(\d+)\b/gi; + const issueNumbers = new Set(); + let match; + while ((match = regex.exec(prData.body || "")) !== null) { + issueNumbers.add(Number(match[1])); + } + + if (issueNumbers.size === 0) { + console.log("No linked issues found in the PR description."); return; } - console.log(`[sync] Linked issues: ${linkedIssues.map(n => '#' + n).join(', ')}`); + console.log(`Detected linked issues: #${Array.from(issueNumbers).join(', #')}`); - const allLabels = []; - for (const num of linkedIssues) { + const discoveredLabels = new Set(); + for (const num of issueNumbers) { try { - const { data } = await github.rest.issues.get({ + const { data: issue } = await github.rest.issues.get({ owner: context.repo.owner, repo: context.repo.repo, issue_number: num }); - if (data.pull_request) { console.log(`[sync] Skipping #${num}: is a PR reference.`); continue; } - const labels = extractLabels(data.labels || []); - console.log(`[sync] Issue #${num} labels: ${labels.length ? labels.join(', ') : '(none)'}`); - allLabels.push(...labels); - } catch (err) { - if (err && err.status === 404) { console.log(`[sync] Issue #${num} not found. Skipping.`); continue; } - throw err; + if (!issue.pull_request) { + const names = (issue.labels || []).map(l => typeof l === 'string' ? l : l.name); + console.log(`Found labels on issue #${num}: [${names.join(', ')}]`); + names.forEach(l => discoveredLabels.add(l)); + } else { + console.log(`Skipping #${num} because it is a Pull Request, not an Issue.`); + } + } catch (e) { + console.log(`Error fetching labels for issue #${num}: ${e.message}`); } } - const existing = extractLabels(prData.labels || []); - const existingSet = new Set(existing); - const deduped = Array.from(new Set(allLabels)); - const toAdd = deduped.filter(l => !existingSet.has(l)); - - console.log(`[sync] Existing: ${existing.length ? existing.join(', ') : '(none)'}`); - console.log(`[sync] To add: ${toAdd.length ? toAdd.join(', ') : '(none)'}`); + const currentLabels = (prData.labels || []).map(l => typeof l === 'string' ? l : l.name); + console.log(`Current PR labels: [${currentLabels.join(', ')}]`); - const labels = toAdd; - const hasLabels = labels.length > 0; - core.setOutput('has_labels', String(hasLabels)); - core.setOutput('labels', JSON.stringify(labels)); - core.setOutput('pr_number', String(prNumber)); - core.setOutput('dry_run', String(process.env.REQUESTED_DRY_RUN || 'true')); - core.setOutput('is_fork_pr', String(process.env.IS_FORK_PR || 'false')); - core.setOutput('source_event', context.eventName); - return { has_labels: hasLabels, labels, pr_number: String(prNumber), dry_run: process.env.REQUESTED_DRY_RUN, is_fork_pr: process.env.IS_FORK_PR, source_event: context.eventName }; + const newLabels = Array.from(discoveredLabels).filter(label => !currentLabels.includes(label)); + + if (newLabels.length > 0) { + console.log(`New labels to be added: [${newLabels.join(', ')}]`); + } else { + console.log("No new labels to add. Skipping artifact creation"); + return; + } - - name: Write labels artifact payload - env: - LABELS_JSON: ${{ steps.compute.outputs.labels }} - PR_NUMBER: ${{ steps.compute.outputs.pr_number }} - IS_FORK_PR: ${{ steps.compute.outputs.is_fork_pr }} - DRY_RUN: ${{ steps.compute.outputs.dry_run }} - SOURCE_EVENT: ${{ steps.compute.outputs.source_event }} - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | const fs = require('fs'); - const parsed = JSON.parse(process.env.LABELS_JSON || '[]'); - const payload = { - pr_number: Number(process.env.PR_NUMBER || 0), - labels: Array.isArray(parsed) ? parsed : [], - is_fork_pr: /^true$/i.test(process.env.IS_FORK_PR || ''), - dry_run: /^true$/i.test(process.env.DRY_RUN || ''), - source_event: process.env.SOURCE_EVENT || '', - }; - fs.writeFileSync('labels.json', JSON.stringify(payload)); - console.log(`Wrote labels artifact payload for PR #${payload.pr_number}: ${payload.labels.length} labels`); - - - name: Upload labels artifact + const result = { pr_number: prNumber, labels: newLabels }; + fs.writeFileSync('labels.json', JSON.stringify(result)); + console.log(`Calculated labels: ${newLabels.join(', ')}`); + + - name: Check for Labels File + id: check_file + run: | + if [ -f labels.json ]; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + + - name: Upload Artifact + if: steps.check_file.outputs.exists == 'true' uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: - name: pr-labels-${{ steps.compute.outputs.pr_number }} + name: pr-labels-data path: labels.json - retention-days: 1 - - dispatch-add: - needs: compute-labels - if: ${{ needs.compute-labels.outputs.is_fork_pr != 'true' }} - runs-on: ubuntu-latest - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 - with: - egress-policy: audit - - - name: Trigger add workflow - uses: step-security/workflow-dispatch@acca1a315af3bf7f33dd116d3cb405cb83f5cbdc # v1.2.8 - with: - workflow: .github/workflows/sync-issue-labels-add.yml - repo: ${{ github.repository }} - ref: main - token: ${{ secrets.GH_ACCESS_TOKEN }} - inputs: >- - { - "upstream_run_id":"${{ github.run_id }}", - "pr_number":"${{ needs.compute-labels.outputs.pr_number }}", - "dry_run":"${{ needs.compute-labels.outputs.dry_run }}", - "is_fork_pr":"${{ needs.compute-labels.outputs.is_fork_pr }}" - } - + retention-days: 1 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index fcaa881b2..adeabaf06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Add automated label sync workflow to propagate labels from linked issues to pull requests (#1716) - chore: update spam list (#2035) - Add CodeRabbit release gate workflow and prompt for PR audits `release-pr-prompt.md`, `release-pr-coderabbit-gate.yml` and `release-pr-coderabbit-gate.js` +- Refactor `sync-issue-labels-compute.yml` and `sync-issue-labels-add.yml` to properly sync issue labels and skip bot PRs. ## [0.2.3] - 2026-03-26 From 4ff23eac386d5eb6cb6b2d0a14d8d2ce5c9852d0 Mon Sep 17 00:00:00 2001 From: Abhijeet Saharan Date: Tue, 7 Apr 2026 02:06:58 +0530 Subject: [PATCH 47/60] fix: pin suitable dependencies versions in publish workflow for reproducibility Signed-off-by: Abhijeet Saharan --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4a83a1198..828fb45bf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,7 +34,7 @@ jobs: run: pip install "pip==26.0.1" - name: Install build, pdm-backend, and grpcio-tools - run: pip install build pdm-backend "grpcio-tools==1.76.0" + run: pip install build==1.4.2 pdm-backend==2.4.8 grpcio-tools==1.76.0 - name: Generate Protobuf run: python generate_proto.py From f6aae74dd0616fd46a4414dba9985cc3ed3e6adc Mon Sep 17 00:00:00 2001 From: Abhijeet Saharan Date: Wed, 8 Apr 2026 14:47:45 +0530 Subject: [PATCH 48/60] fix: ensure consistent dependency installation in tck-test workflow Signed-off-by: Abhijeet Saharan --- .github/workflows/tck-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tck-test.yml b/.github/workflows/tck-test.yml index d5717b155..7e9a627a6 100644 --- a/.github/workflows/tck-test.yml +++ b/.github/workflows/tck-test.yml @@ -42,7 +42,7 @@ jobs: enable-cache: true - name: Install dependencies - run: uv sync --all-extras --dev + run: uv sync --all-extras --dev --frozen - name: Generate Proto Files run: uv run generate_proto.py From 0dd0453357e689424f0d772ce554902dcf8ecfc2 Mon Sep 17 00:00:00 2001 From: Abhijeet Saharan Date: Wed, 8 Apr 2026 14:59:18 +0530 Subject: [PATCH 49/60] docs: add comment for pinned dependencies in publish workflow Signed-off-by: Abhijeet Saharan --- .github/workflows/publish.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 828fb45bf..2ebfb84d1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -33,6 +33,8 @@ jobs: - name: Upgrade pip run: pip install "pip==26.0.1" + # Dependency versions are pinned to ensure reproducible builds. + # These should be periodically reviewed and updated for ensuring smooth execution. - name: Install build, pdm-backend, and grpcio-tools run: pip install build==1.4.2 pdm-backend==2.4.8 grpcio-tools==1.76.0 From 51352be72b1d79bf0c2265c6f6b751e9d628bfa4 Mon Sep 17 00:00:00 2001 From: Abhijeet Saharan Date: Wed, 8 Apr 2026 19:00:35 +0530 Subject: [PATCH 50/60] fix: add dependency version ranges in pyproject.toml for compatibility Signed-off-by: Abhijeet Saharan --- pyproject.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9b11b7f45..0a2d72446 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,12 @@ lint = [ default-groups = ["dev", "lint"] [build-system] -requires = ["pdm-backend", "setuptools", "setuptools_scm", "grpcio-tools"] +requires = [ + "pdm-backend>=2.4.8,<3", + "setuptools>=82,<83", + "setuptools_scm>=7,<9", + "grpcio-tools>=1.80.0,<2" +] build-backend = "pdm.backend" [tool.setuptools_scm] From 0f1b153928e410b6380f69c17fdc6bc813e798f3 Mon Sep 17 00:00:00 2001 From: Abhijeet Saharan Date: Wed, 8 Apr 2026 19:35:32 +0530 Subject: [PATCH 51/60] docs: add changelog entry Signed-off-by: Abhijeet Saharan --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82ff18cbb..f0c1a9fb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ### Src - Exposed all missing `TransactionRecord` protobuf fields `consensusTimestamp`, `scheduleRef`, `assessed_custom_fees`, `automatic_token_associations`, `parent_consensus_timestamp`, `alias`, `ethereum_hash`, `paid_staking_rewards`, `evm_address`, `contractCreateResult` with proper `None` handling, PRNG oneof handling with unset values return `None` instead of default values 0 / b"" (#1636) +- Enforce reproducible and consistent builds using `uv.lock` and frozen installs across workflows. (#2057) ### Tests - Refactor `mock_server` setup for network level TLS handling and added thread safety From 8016438deb93e01665c4c49e203451947254175f Mon Sep 17 00:00:00 2001 From: Abhijeet Saharan Date: Wed, 8 Apr 2026 21:11:58 +0530 Subject: [PATCH 52/60] trigger workflows Signed-off-by: Abhijeet Saharan From 7a444b0e24a3211b6131be423568a8fafdf9acde Mon Sep 17 00:00:00 2001 From: Abhijeet Saharan Date: Sat, 11 Apr 2026 15:04:26 +0530 Subject: [PATCH 53/60] fix: use full git history for SCM versioning in deps-check Signed-off-by: Abhijeet Saharan --- .github/workflows/deps-check.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deps-check.yml b/.github/workflows/deps-check.yml index aa90413b5..2d18afc18 100644 --- a/.github/workflows/deps-check.yml +++ b/.github/workflows/deps-check.yml @@ -26,10 +26,13 @@ jobs: uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit - + - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - + with: + fetch-depth: 0 # ensure full git history for SCM-based versioning + + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: From cbaf492b7214ccb8785a9d462071267b91285b21 Mon Sep 17 00:00:00 2001 From: Abhijeet Date: Sat, 11 Apr 2026 15:17:14 +0530 Subject: [PATCH 54/60] fix: normalize SCM version format Signed-off-by: Abhijeet --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 0a2d72446..0c2603b71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,6 +75,7 @@ local_scheme = "dirty-tag" [tool.pdm] version = { source = "scm" } +version_format = "{version}" # normalize SCM version [tool.pdm.build] package-dir = "src" From a4dae26b10f64536aff238cf2ea09baeccf235c6 Mon Sep 17 00:00:00 2001 From: Abhijeet Date: Sat, 11 Apr 2026 15:24:46 +0530 Subject: [PATCH 55/60] fix: configure SCM versioning to avoid metadata mismatch Signed-off-by: Abhijeet --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0c2603b71..57beb993f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,8 +73,8 @@ build-backend = "pdm.backend" version_scheme = "python-simplified-semver" local_scheme = "dirty-tag" -[tool.pdm] -version = { source = "scm" } +[tool.pdm.version] +source = "scm" version_format = "{version}" # normalize SCM version [tool.pdm.build] From 0363aba6e8990b2cb6c692bd550fd206be150cd7 Mon Sep 17 00:00:00 2001 From: Abhijeet Date: Sat, 11 Apr 2026 15:33:40 +0530 Subject: [PATCH 56/60] fix: align SCM versioning to avoid metadata mismatch Signed-off-by: Abhijeet --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 57beb993f..ebb6c1be6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,11 +71,10 @@ build-backend = "pdm.backend" [tool.setuptools_scm] version_scheme = "python-simplified-semver" -local_scheme = "dirty-tag" +local_scheme = "no-local-version" [tool.pdm.version] source = "scm" -version_format = "{version}" # normalize SCM version [tool.pdm.build] package-dir = "src" From 595016e7f3632b78f1439f41e9d6961f892e1a34 Mon Sep 17 00:00:00 2001 From: Abhijeet Date: Sat, 11 Apr 2026 15:45:31 +0530 Subject: [PATCH 57/60] fix: disable editable install in deps-check to avoid SCM version mismatch Signed-off-by: Abhijeet --- .github/workflows/deps-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deps-check.yml b/.github/workflows/deps-check.yml index 2d18afc18..83868262b 100644 --- a/.github/workflows/deps-check.yml +++ b/.github/workflows/deps-check.yml @@ -47,7 +47,7 @@ jobs: - name: Install minimum dependencies and tooling run: | - uv sync --all-extras --resolution=lowest-direct + uv sync --all-extras --resolution=lowest-direct --no-editable - name: Show resolved core dependency versions run: | From 348127c5bc44e44d3e7fbb6f962201057e013895 Mon Sep 17 00:00:00 2001 From: Abhijeet Date: Sun, 12 Apr 2026 00:54:48 +0530 Subject: [PATCH 58/60] docs: fix indentation Signed-off-by: Abhijeet --- .github/workflows/deps-check.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/deps-check.yml b/.github/workflows/deps-check.yml index c27ffe9bc..9db2bc7bc 100644 --- a/.github/workflows/deps-check.yml +++ b/.github/workflows/deps-check.yml @@ -30,8 +30,7 @@ jobs: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - fetch-depth: 0 # ensure full git history for SCM-based versioning - + fetch-depth: 0 # ensure full git history for SCM-based versioning - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 From c8d12cd86092f30b28d1f075732b2d96243c550a Mon Sep 17 00:00:00 2001 From: Abhijeet Date: Sun, 12 Apr 2026 01:06:53 +0530 Subject: [PATCH 59/60] fix: align grpcio-tools version range with publish setup Signed-off-by: Abhijeet --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ebb6c1be6..255ab7433 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ requires = [ "pdm-backend>=2.4.8,<3", "setuptools>=82,<83", "setuptools_scm>=7,<9", - "grpcio-tools>=1.80.0,<2" + "grpcio-tools>=1.76.0,<2" ] build-backend = "pdm.backend" From 0274282e17875b282b6a6676573d81e7f4dd9c5e Mon Sep 17 00:00:00 2001 From: Abhijeet Date: Tue, 14 Apr 2026 00:33:53 +0530 Subject: [PATCH 60/60] revert: remove local_scheme change from setuptools_scm Signed-off-by: Abhijeet --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 255ab7433..c7b87bde4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ build-backend = "pdm.backend" [tool.setuptools_scm] version_scheme = "python-simplified-semver" -local_scheme = "no-local-version" +local_scheme = "dirty-tag" [tool.pdm.version] source = "scm"