From 35b47a61c2a1c51f894aa79f9f2997257313aa24 Mon Sep 17 00:00:00 2001 From: mhh Date: Mon, 4 Dec 2023 09:33:24 +0100 Subject: [PATCH 1/7] Handle rejected create_instance response and raise InsufficientFundsError if error_code is 5 This allows better exception handling when creating instances --- src/aleph/sdk/client/authenticated_http.py | 80 ++++++++++++++++------ src/aleph/sdk/client/http.py | 23 +++++++ src/aleph/sdk/exceptions.py | 10 ++- 3 files changed, 90 insertions(+), 23 deletions(-) diff --git a/src/aleph/sdk/client/authenticated_http.py b/src/aleph/sdk/client/authenticated_http.py index 4c923f83..42f9d1b4 100644 --- a/src/aleph/sdk/client/authenticated_http.py +++ b/src/aleph/sdk/client/authenticated_http.py @@ -35,7 +35,7 @@ from aleph_message.status import MessageStatus from ..conf import settings -from ..exceptions import BroadcastError, InvalidMessageError +from ..exceptions import BroadcastError, InvalidMessageError, InsufficientFundsError from ..types import Account, StorageEnum from ..utils import extended_json_encoder from .abstract import AuthenticatedAlephClient @@ -219,8 +219,8 @@ async def _broadcast_deprecated(self, message_dict: Mapping[str, Any]) -> None: await self._handle_broadcast_deprecated_response(response) async def _handle_broadcast_response( - self, response: aiohttp.ClientResponse, sync: bool - ) -> MessageStatus: + self, response: aiohttp.ClientResponse, sync: bool, raise_on_rejected: bool + ) -> Tuple[Dict[str, Any], MessageStatus]: if response.status in (200, 202): status = await response.json() self._log_publication_status(status["publication_status"]) @@ -230,10 +230,11 @@ async def _handle_broadcast_response( logger.warning( "Timed out while waiting for processing of sync message" ) - return MessageStatus.PENDING - - return MessageStatus.PROCESSED + return status, MessageStatus.PENDING + return status, MessageStatus.PROCESSED + elif response.status == 422 and not raise_on_rejected: + return await response.json(), MessageStatus.REJECTED else: await self._handle_broadcast_error(response) @@ -241,7 +242,8 @@ async def _broadcast( self, message: AlephMessage, sync: bool, - ) -> MessageStatus: + raise_on_rejected: bool = True, + ) -> Tuple[Dict[str, Any], MessageStatus]: """ Broadcast a message on the aleph.im network. @@ -266,12 +268,11 @@ async def _broadcast( "POST /messages/ not found. Defaulting to legacy endpoint..." ) await self._broadcast_deprecated(message_dict=message_dict) - return MessageStatus.PENDING + return await response.json(), MessageStatus.PENDING else: - message_status = await self._handle_broadcast_response( - response=response, sync=sync + return await self._handle_broadcast_response( + response=response, sync=sync, raise_on_rejected=raise_on_rejected ) - return message_status async def create_post( self, @@ -294,7 +295,7 @@ async def create_post( ref=ref, ) - return await self.submit( + message, status, _ = await self.submit( content=content.dict(exclude_none=True), message_type=MessageType.post, channel=channel, @@ -302,6 +303,7 @@ async def create_post( storage_engine=storage_engine, sync=sync, ) + return message, status async def create_aggregate( self, @@ -321,13 +323,14 @@ async def create_aggregate( time=time.time(), ) - return await self.submit( + message, status, _ = await self.submit( content=content_.dict(exclude_none=True), message_type=MessageType.aggregate, channel=channel, allow_inlining=inline, sync=sync, ) + return message, status async def create_store( self, @@ -394,7 +397,7 @@ async def create_store( content = StoreContent(**values) - return await self.submit( + message, status, _ = await self.submit( content=content.dict(exclude_none=True), message_type=MessageType.store, channel=channel, @@ -486,13 +489,14 @@ async def create_program( # Ensure that the version of aleph-message used supports the field. assert content.on.persistent == persistent - return await self.submit( + message, status, _ = await self.submit( content=content.dict(exclude_none=True), message_type=MessageType.program, channel=channel, storage_engine=storage_engine, sync=sync, ) + return message, status async def create_instance( self, @@ -555,14 +559,44 @@ async def create_instance( authorized_keys=ssh_keys, metadata=metadata, ) - - return await self.submit( + message, status, response = await self.submit( content=content.dict(exclude_none=True), message_type=MessageType.instance, channel=channel, storage_engine=storage_engine, sync=sync, + raise_on_rejected=False, ) + if status in (MessageStatus.PROCESSED, MessageStatus.PENDING): + return message, status + + # get the reason for rejection + rejected_message = await self.get_message_error(message.item_hash) + assert rejected_message, "No rejected message found" + """ + "error_code": 5, + "details": { + "errors": [ + { + "account_balance": "0", + "required_balance": "4213.265726725260407192763523" + } + ] + } + """ + error_code = rejected_message["error_code"] + if error_code == 5: + # not enough balance + details = rejected_message["details"] + errors = details["errors"] + error = errors[0] + account_balance = float(error["account_balance"]) + required_balance = float(error["required_balance"]) + raise InsufficientFundsError( + f"Account balance {account_balance} is not enough to create instance, required {required_balance}" + ) + else: + raise ValueError(f"Unknown error code {error_code}: {rejected_message}") async def forget( self, @@ -582,7 +616,7 @@ async def forget( time=time.time(), ) - return await self.submit( + message, status, _ = await self.submit( content=content.dict(exclude_none=True), message_type=MessageType.forget, channel=channel, @@ -590,6 +624,7 @@ async def forget( allow_inlining=True, sync=sync, ) + return message, status @staticmethod def compute_sha256(s: str) -> str: @@ -647,7 +682,8 @@ async def submit( storage_engine: StorageEnum = StorageEnum.storage, allow_inlining: bool = True, sync: bool = False, - ) -> Tuple[AlephMessage, MessageStatus]: + raise_on_rejected: bool = True, + ) -> Tuple[AlephMessage, MessageStatus, Optional[Dict[str, Any]]]: message = await self._prepare_aleph_message( message_type=message_type, content=content, @@ -655,8 +691,8 @@ async def submit( allow_inlining=allow_inlining, storage_engine=storage_engine, ) - message_status = await self._broadcast(message=message, sync=sync) - return message, message_status + response, message_status = await self._broadcast(message=message, sync=sync, raise_on_rejected=raise_on_rejected) + return message, message_status, response async def _storage_push_file_with_message( self, @@ -731,5 +767,5 @@ async def _upload_file_native( # Some nodes may not implement authenticated file upload yet. As we cannot detect # this easily, broadcast the message a second time to ensure publication on older # nodes. - status = await self._broadcast(message=message, sync=sync) + _, status = await self._broadcast(message=message, sync=sync) return message, status diff --git a/src/aleph/sdk/client/http.py b/src/aleph/sdk/client/http.py index a1f77255..0a46896b 100644 --- a/src/aleph/sdk/client/http.py +++ b/src/aleph/sdk/client/http.py @@ -320,6 +320,29 @@ async def get_message( ) return message + async def get_message_error( + self, + item_hash: str, + ) -> Optional[Dict[str, Any]]: + async with self.http_session.get(f"/api/v0/messages/{item_hash}") as resp: + try: + resp.raise_for_status() + except aiohttp.ClientResponseError as e: + if e.status == 404: + raise MessageNotFoundError(f"No such hash {item_hash}") + raise e + message_raw = await resp.json() + if message_raw["status"] == "forgotten": + raise ForgottenMessageError( + f"The requested message {message_raw['item_hash']} has been forgotten by {', '.join(message_raw['forgotten_by'])}" + ) + if message_raw["status"] != "rejected": + return None + return { + "error_code": message_raw["error_code"], + "details": message_raw["details"], + } + async def watch_messages( self, message_filter: Optional[MessageFilter] = None, diff --git a/src/aleph/sdk/exceptions.py b/src/aleph/sdk/exceptions.py index f2cd96d6..158d6911 100644 --- a/src/aleph/sdk/exceptions.py +++ b/src/aleph/sdk/exceptions.py @@ -24,7 +24,9 @@ class BroadcastError(Exception): Data could not be broadcast to the aleph.im network. """ - pass + def __init__(self, errors): + self.errors = errors + super().__init__(errors) class InvalidMessageError(BroadcastError): @@ -62,3 +64,9 @@ class ForgottenMessageError(QueryError): """The requested message was forgotten""" pass + + +class InsufficientFundsError(Exception): + """Raised when the account does not have enough funds to perform an action""" + + pass From 868d72f3cc5098684873dc44340093463b87ba81 Mon Sep 17 00:00:00 2001 From: mhh Date: Mon, 4 Dec 2023 09:33:43 +0100 Subject: [PATCH 2/7] Add test for handling an InsufficientFundsError while creating an instance. --- tests/unit/conftest.py | 27 ++++++++++ tests/unit/rejected_message.json | 53 +++++++++++++++++++ tests/unit/test_asynchronous.py | 79 +++++++++++++++++++++++++++++ tests/unit/test_asynchronous_get.py | 12 ++++- 4 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 tests/unit/rejected_message.json diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 552cb548..d59fbd46 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -57,6 +57,13 @@ def json_messages(): return json.load(f) +@pytest.fixture +def rejected_message(): + message_path = Path(__file__).parent / "rejected_message.json" + with open(message_path) as f: + return json.load(f) + + @pytest.fixture def aleph_messages() -> List[AlephMessage]: return [ @@ -213,3 +220,23 @@ def get(self, *_args, **_kwargs): client.http_session = http_session return client + + +@pytest.fixture +def mock_session_with_rejected_message(ethereum_account, rejected_message) -> AuthenticatedAlephHttpClient: + class MockHttpSession(AsyncMock): + def get(self, *_args, **_kwargs): + return make_custom_mock_response(rejected_message) + + def post(self, *_args, **_kwargs): + return make_custom_mock_response({ + "message_status": "rejected", + "publication_status": {"status": "success", "failed": []}, + }, status=422) + + http_session = MockHttpSession() + + client = AuthenticatedAlephHttpClient(account=ethereum_account, api_server="http://localhost") + client.http_session = http_session + + return client diff --git a/tests/unit/rejected_message.json b/tests/unit/rejected_message.json new file mode 100644 index 00000000..0ec2cd4e --- /dev/null +++ b/tests/unit/rejected_message.json @@ -0,0 +1,53 @@ +{ + "status": "rejected", + "item_hash": "7091b61632e0385adb6978dccbec1da40e9c400ffac7233076a3ad0219f49de0", + "reception_time": "2023-12-03T16:00:53.432607+00:00", + "message": { + "time": 1701619253.309467, + "type": "INSTANCE", + "chain": "ETH", + "sender": "0x48A87924135892bEE2F264547a0D69909b3bA5C6", + "channel": null, + "content": { + "time": 1701619253.3092096, + "rootfs": { + "parent": { + "ref": "549ec451d9b099cad112d4aaa2c00ac40fb6729a92ff252ff22eef0b5c3cb613", + "use_latest": true + }, + "size_mib": 2000, + "persistence": "host" + }, + "address": "0x48A87924135892bEE2F264547a0D69909b3bA5C6", + "volumes": [], + "resources": { + "vcpus": 1, + "memory": 256, + "seconds": 30 + }, + "allow_amend": false, + "environment": { + "internet": true, + "aleph_api": true, + "reproducible": false, + "shared_cache": false + }, + "authorized_keys": [ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCdU/B01GaUQ3l8aIt0XR6WajhM/5oTdxsON3b90quwQjH38Ba+Z3z74gf4+f56vxRw7XqtC8pg8zuh2L8+Fo3IHctULJD24YbLr2CagwrPsd9fsK+DsJSrcnTKaBBjvgloQi96hl4hPtlR9jA5bRYked0bFxdbFUMCmF17tpU0AC/dLeH3FwVDfh5dF11SX6ezg8brKd9SHYDRkOBHyOLIOWMtOgIgfEy9OsNdAS9OlOHyKxz4MEl5xkq5lRk9nS1n45A3rrIwi91qamjphHNrEutKaN/ramuW+davCP4xfnEVyu2DmoteDM+lmmf4skN90CwxhlrYpc5xpie5vTTYxgUCSnN0PyYiD0m7YHpZwMg1yHKB+aZghOIYUyKVJ8fIH/uU1DKwqcZ5qmH8sPvB4tCy3QOn7m7xTYVd/R3CECdG/R/55IUfGbg8KoAHBgmQ13/hSJ3IshMYOTWwMenma29D8UqKsyTfiMfof6Yf6NYOeDkAsUSIOEetxcv3MaE= mhh@mhh-TUXEDO" + ] + }, + "item_hash": "7091b61632e0385adb6978dccbec1da40e9c400ffac7233076a3ad0219f49de0", + "item_type": "inline", + "signature": "0xa8e549c5228b379bede875016ed25215c6cd7c3e9131ec1b3bdb49af4859c8840251362c3c421995f520648026621245b4896c0a88d6a5735e5e2620812f22ff1b", + "item_content": "{\"address\":\"0x48A87924135892bEE2F264547a0D69909b3bA5C6\",\"time\":1701619253.3092096,\"allow_amend\":false,\"authorized_keys\":[\"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCdU/B01GaUQ3l8aIt0XR6WajhM/5oTdxsON3b90quwQjH38Ba+Z3z74gf4+f56vxRw7XqtC8pg8zuh2L8+Fo3IHctULJD24YbLr2CagwrPsd9fsK+DsJSrcnTKaBBjvgloQi96hl4hPtlR9jA5bRYked0bFxdbFUMCmF17tpU0AC/dLeH3FwVDfh5dF11SX6ezg8brKd9SHYDRkOBHyOLIOWMtOgIgfEy9OsNdAS9OlOHyKxz4MEl5xkq5lRk9nS1n45A3rrIwi91qamjphHNrEutKaN/ramuW+davCP4xfnEVyu2DmoteDM+lmmf4skN90CwxhlrYpc5xpie5vTTYxgUCSnN0PyYiD0m7YHpZwMg1yHKB+aZghOIYUyKVJ8fIH/uU1DKwqcZ5qmH8sPvB4tCy3QOn7m7xTYVd/R3CECdG/R/55IUfGbg8KoAHBgmQ13/hSJ3IshMYOTWwMenma29D8UqKsyTfiMfof6Yf6NYOeDkAsUSIOEetxcv3MaE= mhh@mhh-TUXEDO\"],\"environment\":{\"reproducible\":false,\"internet\":true,\"aleph_api\":true,\"shared_cache\":false},\"resources\":{\"vcpus\":1,\"memory\":256,\"seconds\":30},\"volumes\":[],\"rootfs\":{\"parent\":{\"ref\":\"549ec451d9b099cad112d4aaa2c00ac40fb6729a92ff252ff22eef0b5c3cb613\",\"use_latest\":true},\"persistence\":\"host\",\"size_mib\":2000}}" + }, + "error_code": 5, + "details": { + "errors": [ + { + "account_balance": "0", + "required_balance": "4213.265726725260407192763523" + } + ] + } +} \ No newline at end of file diff --git a/tests/unit/test_asynchronous.py b/tests/unit/test_asynchronous.py index 1d3f4339..d47b7438 100644 --- a/tests/unit/test_asynchronous.py +++ b/tests/unit/test_asynchronous.py @@ -1,3 +1,4 @@ +import datetime from unittest.mock import AsyncMock import pytest as pytest @@ -5,12 +6,15 @@ AggregateMessage, ForgetMessage, InstanceMessage, + MessageType, PostMessage, ProgramMessage, StoreMessage, ) +from aleph_message.models.execution.environment import MachineResources from aleph_message.status import MessageStatus +from aleph.sdk.exceptions import InsufficientFundsError from aleph.sdk.types import StorageEnum @@ -121,3 +125,78 @@ async def test_forget(mock_session_with_post_success): assert mock_session_with_post_success.http_session.post.called_once assert isinstance(forget_message, ForgetMessage) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "message_type, content", + [ + ( + MessageType.aggregate, + { + "content": {"Hello": datetime.datetime.now()}, + "key": "test", + "address": "0x1", + "time": 1.0, + }, + ), + ( + MessageType.aggregate, + { + "content": {"Hello": datetime.date.today()}, + "key": "test", + "address": "0x1", + "time": 1.0, + }, + ), + ( + MessageType.aggregate, + { + "content": {"Hello": datetime.time()}, + "key": "test", + "address": "0x1", + "time": 1.0, + }, + ), + ( + MessageType.aggregate, + { + "content": { + "Hello": MachineResources( + vcpus=1, + memory=1024, + seconds=1, + ) + }, + "key": "test", + "address": "0x1", + "time": 1.0, + }, + ), + ], +) +async def test_prepare_aleph_message( + mock_session_with_post_success, message_type, content +): + # Call the function under test + async with mock_session_with_post_success as session: + await session._prepare_aleph_message( + message_type=message_type, + content=content, + channel="TEST", + ) + + +@pytest.mark.asyncio +async def test_create_instance_insufficient_funds_error( + mock_session_with_rejected_message +): + async with mock_session_with_rejected_message as session: + with pytest.raises(InsufficientFundsError): + await session.create_instance( + rootfs="cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe", + rootfs_size=1, + rootfs_name="rootfs", + channel="TEST", + metadata={"tags": ["test"]}, + ) \ No newline at end of file diff --git a/tests/unit/test_asynchronous_get.py b/tests/unit/test_asynchronous_get.py index cef01a5c..10aa6ac4 100644 --- a/tests/unit/test_asynchronous_get.py +++ b/tests/unit/test_asynchronous_get.py @@ -4,7 +4,7 @@ import pytest from aleph_message.models import MessagesResponse, MessageType -from aleph.sdk.exceptions import ForgottenMessageError +from aleph.sdk.exceptions import ForgottenMessageError, InvalidMessageError from aleph.sdk.query.filters import MessageFilter, PostFilter from aleph.sdk.query.responses import PostsResponse from tests.unit.conftest import make_mock_get_session @@ -85,5 +85,15 @@ async def test_get_forgotten_message(): await session.get_message("cafebabe") +@pytest.mark.asyncio +async def test_get_message_error(rejected_message): + mock_session = make_mock_get_session( + rejected_message + ) + async with mock_session as session: + error = await session.get_message_error(rejected_message["item_hash"]) + assert error["error_code"] == rejected_message["error_code"] + assert error["details"] == rejected_message["details"] + if __name__ == "__main __": unittest.main() From 5069fa64251e7f0381c15a621fc712c5188a3957 Mon Sep 17 00:00:00 2001 From: mhh Date: Mon, 4 Dec 2023 09:39:19 +0100 Subject: [PATCH 3/7] Fix formatting --- src/aleph/sdk/client/authenticated_http.py | 6 ++++-- tests/unit/conftest.py | 19 +++++++++++++------ tests/unit/test_asynchronous.py | 4 ++-- tests/unit/test_asynchronous_get.py | 8 ++++---- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/aleph/sdk/client/authenticated_http.py b/src/aleph/sdk/client/authenticated_http.py index 42f9d1b4..eb9d7f76 100644 --- a/src/aleph/sdk/client/authenticated_http.py +++ b/src/aleph/sdk/client/authenticated_http.py @@ -35,7 +35,7 @@ from aleph_message.status import MessageStatus from ..conf import settings -from ..exceptions import BroadcastError, InvalidMessageError, InsufficientFundsError +from ..exceptions import BroadcastError, InsufficientFundsError, InvalidMessageError from ..types import Account, StorageEnum from ..utils import extended_json_encoder from .abstract import AuthenticatedAlephClient @@ -691,7 +691,9 @@ async def submit( allow_inlining=allow_inlining, storage_engine=storage_engine, ) - response, message_status = await self._broadcast(message=message, sync=sync, raise_on_rejected=raise_on_rejected) + response, message_status = await self._broadcast( + message=message, sync=sync, raise_on_rejected=raise_on_rejected + ) return message, message_status, response async def _storage_push_file_with_message( diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index d59fbd46..95cc7851 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -223,20 +223,27 @@ def get(self, *_args, **_kwargs): @pytest.fixture -def mock_session_with_rejected_message(ethereum_account, rejected_message) -> AuthenticatedAlephHttpClient: +def mock_session_with_rejected_message( + ethereum_account, rejected_message +) -> AuthenticatedAlephHttpClient: class MockHttpSession(AsyncMock): def get(self, *_args, **_kwargs): return make_custom_mock_response(rejected_message) def post(self, *_args, **_kwargs): - return make_custom_mock_response({ - "message_status": "rejected", - "publication_status": {"status": "success", "failed": []}, - }, status=422) + return make_custom_mock_response( + { + "message_status": "rejected", + "publication_status": {"status": "success", "failed": []}, + }, + status=422, + ) http_session = MockHttpSession() - client = AuthenticatedAlephHttpClient(account=ethereum_account, api_server="http://localhost") + client = AuthenticatedAlephHttpClient( + account=ethereum_account, api_server="http://localhost" + ) client.http_session = http_session return client diff --git a/tests/unit/test_asynchronous.py b/tests/unit/test_asynchronous.py index d47b7438..c13df757 100644 --- a/tests/unit/test_asynchronous.py +++ b/tests/unit/test_asynchronous.py @@ -189,7 +189,7 @@ async def test_prepare_aleph_message( @pytest.mark.asyncio async def test_create_instance_insufficient_funds_error( - mock_session_with_rejected_message + mock_session_with_rejected_message, ): async with mock_session_with_rejected_message as session: with pytest.raises(InsufficientFundsError): @@ -199,4 +199,4 @@ async def test_create_instance_insufficient_funds_error( rootfs_name="rootfs", channel="TEST", metadata={"tags": ["test"]}, - ) \ No newline at end of file + ) diff --git a/tests/unit/test_asynchronous_get.py b/tests/unit/test_asynchronous_get.py index 10aa6ac4..7cfb38f3 100644 --- a/tests/unit/test_asynchronous_get.py +++ b/tests/unit/test_asynchronous_get.py @@ -4,7 +4,7 @@ import pytest from aleph_message.models import MessagesResponse, MessageType -from aleph.sdk.exceptions import ForgottenMessageError, InvalidMessageError +from aleph.sdk.exceptions import ForgottenMessageError from aleph.sdk.query.filters import MessageFilter, PostFilter from aleph.sdk.query.responses import PostsResponse from tests.unit.conftest import make_mock_get_session @@ -87,13 +87,13 @@ async def test_get_forgotten_message(): @pytest.mark.asyncio async def test_get_message_error(rejected_message): - mock_session = make_mock_get_session( - rejected_message - ) + mock_session = make_mock_get_session(rejected_message) async with mock_session as session: error = await session.get_message_error(rejected_message["item_hash"]) + assert error assert error["error_code"] == rejected_message["error_code"] assert error["details"] == rejected_message["details"] + if __name__ == "__main __": unittest.main() From a033bfeae273bcd07f70524514d1f0dc453a2ee2 Mon Sep 17 00:00:00 2001 From: mhh Date: Mon, 4 Dec 2023 09:44:30 +0100 Subject: [PATCH 4/7] Update `submit()` abstract interface --- src/aleph/sdk/client/abstract.py | 4 +++- src/aleph/sdk/client/authenticated_http.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/aleph/sdk/client/abstract.py b/src/aleph/sdk/client/abstract.py index e27f7d77..20a04e43 100644 --- a/src/aleph/sdk/client/abstract.py +++ b/src/aleph/sdk/client/abstract.py @@ -428,7 +428,8 @@ async def submit( storage_engine: StorageEnum = StorageEnum.storage, allow_inlining: bool = True, sync: bool = False, - ) -> Tuple[AlephMessage, MessageStatus]: + raise_on_rejected: bool = True, + ) -> Tuple[AlephMessage, MessageStatus, Optional[Dict[str, Any]]]: """ Submit a message to the network. This is a generic method that can be used to submit any type of message. Prefer using the more specific methods to submit messages. @@ -439,6 +440,7 @@ async def submit( :param storage_engine: Storage engine to use (Default: "storage") :param allow_inlining: Whether to allow inlining the content of the message (Default: True) :param sync: If true, waits for the message to be processed by the API server (Default: False) + :param raise_on_rejected: Whether to raise an exception if the message is rejected (Default: True) """ pass diff --git a/src/aleph/sdk/client/authenticated_http.py b/src/aleph/sdk/client/authenticated_http.py index eb9d7f76..e39bfe37 100644 --- a/src/aleph/sdk/client/authenticated_http.py +++ b/src/aleph/sdk/client/authenticated_http.py @@ -404,6 +404,7 @@ async def create_store( allow_inlining=True, sync=sync, ) + return message, status async def create_program( self, From 984dece0864cd0d6d7f8a38dbe5aa333251fb4ee Mon Sep 17 00:00:00 2001 From: mhh Date: Mon, 4 Dec 2023 09:49:10 +0100 Subject: [PATCH 5/7] Remove "Test" from "PR Difficulty Rating Action" name --- .github/workflows/pr-rating.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-rating.yml b/.github/workflows/pr-rating.yml index 2bbcd27d..8f42647d 100644 --- a/.github/workflows/pr-rating.yml +++ b/.github/workflows/pr-rating.yml @@ -1,4 +1,4 @@ -name: Test PR Difficulty Rating Action +name: PR Difficulty Rating Action permissions: pull-requests: write From eadd0026cdbd91ff2531a811d9cedeab17780031 Mon Sep 17 00:00:00 2001 From: mhh Date: Mon, 4 Dec 2023 10:29:42 +0100 Subject: [PATCH 6/7] Remove unnecessary doc string --- src/aleph/sdk/client/authenticated_http.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/aleph/sdk/client/authenticated_http.py b/src/aleph/sdk/client/authenticated_http.py index e39bfe37..4b0c9443 100644 --- a/src/aleph/sdk/client/authenticated_http.py +++ b/src/aleph/sdk/client/authenticated_http.py @@ -574,17 +574,6 @@ async def create_instance( # get the reason for rejection rejected_message = await self.get_message_error(message.item_hash) assert rejected_message, "No rejected message found" - """ - "error_code": 5, - "details": { - "errors": [ - { - "account_balance": "0", - "required_balance": "4213.265726725260407192763523" - } - ] - } - """ error_code = rejected_message["error_code"] if error_code == 5: # not enough balance From 07011ddc1163e4e1c5faa7b33ae33589b3ba7a31 Mon Sep 17 00:00:00 2001 From: mhh Date: Mon, 4 Dec 2023 10:34:42 +0100 Subject: [PATCH 7/7] Update InsufficientFundsError to have `required_balance` and `account_balance` fields --- src/aleph/sdk/client/authenticated_http.py | 2 +- src/aleph/sdk/exceptions.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/aleph/sdk/client/authenticated_http.py b/src/aleph/sdk/client/authenticated_http.py index 4b0c9443..78b006fd 100644 --- a/src/aleph/sdk/client/authenticated_http.py +++ b/src/aleph/sdk/client/authenticated_http.py @@ -583,7 +583,7 @@ async def create_instance( account_balance = float(error["account_balance"]) required_balance = float(error["required_balance"]) raise InsufficientFundsError( - f"Account balance {account_balance} is not enough to create instance, required {required_balance}" + required_funds=required_balance, available_funds=account_balance ) else: raise ValueError(f"Unknown error code {error_code}: {rejected_message}") diff --git a/src/aleph/sdk/exceptions.py b/src/aleph/sdk/exceptions.py index 158d6911..39972f7f 100644 --- a/src/aleph/sdk/exceptions.py +++ b/src/aleph/sdk/exceptions.py @@ -69,4 +69,12 @@ class ForgottenMessageError(QueryError): class InsufficientFundsError(Exception): """Raised when the account does not have enough funds to perform an action""" - pass + required_funds: float + available_funds: float + + def __init__(self, required_funds: float, available_funds: float): + self.required_funds = required_funds + self.available_funds = available_funds + super().__init__( + f"Insufficient funds: required {required_funds}, available {available_funds}" + )