From a409433759df55c74960600e64d77f7787c37ee8 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Fri, 4 Oct 2024 10:59:02 +0900 Subject: [PATCH 01/37] Migrate to Pydantic v2, update model validation and fix async issues - Migrated to Pydantic v2: - Replaced deprecated `parse_obj()` and `parse_raw()` with `model_validate()` and `model_validate_json()`. - Replaced `.dict()` with `.model_dump()` for serializing models to dictionaries. - Updated `validator` to `field_validator` and `root_validator` to `model_validator` to comply with Pydantic v2 syntax changes. - Fixed asyncio issues: - Added `await` for asynchronous methods like `raise_for_status()` in `RemoteAccount` and other HTTP operations to avoid `RuntimeWarning`. - Updated config handling: - Used `ClassVar` for constants in `Settings` and other configuration classes. - Replaced `Config` with `ConfigDict` in Pydantic models to follow v2 conventions. - Added default values for missing fields in chain configurations (`CHAINS_SEPOLIA_ACTIVE`, etc.). - Adjusted signature handling: - Updated the signing logic to prepend `0x` in the `BaseAccount` signature generation to ensure correct Ethereum address formatting. - Minor fixes: - Resolved issue with extra fields not being allowed by default by specifying `extra="allow"` or `extra="forbid"` where necessary. - Fixed tests to account for changes in model validation and serialization behavior. - Added `pydantic-settings` as a new dependency for configuration management. --- pyproject.toml | 1 + src/aleph/sdk/chains/common.py | 2 +- src/aleph/sdk/chains/remote.py | 4 +- src/aleph/sdk/client/authenticated_http.py | 18 +++--- src/aleph/sdk/client/http.py | 2 +- .../sdk/client/vm_confidential_client.py | 2 +- src/aleph/sdk/conf.py | 62 ++++++++++--------- src/aleph/sdk/query/responses.py | 10 +-- src/aleph/sdk/utils.py | 8 ++- tests/unit/aleph_vm_authentication.py | 40 ++++++------ tests/unit/conftest.py | 6 +- tests/unit/test_remote_account.py | 2 +- tests/unit/test_utils.py | 23 +++---- tests/unit/test_vm_client.py | 4 +- 14 files changed, 95 insertions(+), 89 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f533bfe2..8a7bbc09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ "eth_abi>=4.0.0; python_version>=\"3.11\"", "jwcrypto==1.5.6", "python-magic", + "pydantic-settings", "typing_extensions", "aioresponses>=0.7.6", "aleph-superfluid>=0.2.1", diff --git a/src/aleph/sdk/chains/common.py b/src/aleph/sdk/chains/common.py index 0a90183c..8f57f9b4 100644 --- a/src/aleph/sdk/chains/common.py +++ b/src/aleph/sdk/chains/common.py @@ -72,7 +72,7 @@ async def sign_message(self, message: Dict) -> Dict: """ message = self._setup_sender(message) signature = await self.sign_raw(get_verification_buffer(message)) - message["signature"] = signature.hex() + message["signature"] = "0x" + signature.hex() return message @abstractmethod diff --git a/src/aleph/sdk/chains/remote.py b/src/aleph/sdk/chains/remote.py index 931b68f3..917cf39b 100644 --- a/src/aleph/sdk/chains/remote.py +++ b/src/aleph/sdk/chains/remote.py @@ -52,7 +52,7 @@ async def from_crypto_host( session = aiohttp.ClientSession(connector=connector) async with session.get(f"{host}/properties") as response: - response.raise_for_status() + await response.raise_for_status() data = await response.json() properties = AccountProperties(**data) @@ -75,7 +75,7 @@ def private_key(self): async def sign_message(self, message: Dict) -> Dict: """Sign a message inplace.""" async with self._session.post(f"{self._host}/sign", json=message) as response: - response.raise_for_status() + await response.raise_for_status() return await response.json() async def sign_raw(self, buffer: bytes) -> bytes: diff --git a/src/aleph/sdk/client/authenticated_http.py b/src/aleph/sdk/client/authenticated_http.py index f84b97ca..2f30e534 100644 --- a/src/aleph/sdk/client/authenticated_http.py +++ b/src/aleph/sdk/client/authenticated_http.py @@ -259,7 +259,7 @@ async def _broadcast( url = "/api/v0/messages" logger.debug(f"Posting message on {url}") - message_dict = message.dict(include=self.BROADCAST_MESSAGE_FIELDS) + message_dict = message.model_dump(include=self.BROADCAST_MESSAGE_FIELDS) async with self.http_session.post( url, json={ @@ -301,7 +301,7 @@ async def create_post( ) message, status, _ = await self.submit( - content=content.dict(exclude_none=True), + content=content.model_dump(exclude_none=True), message_type=MessageType.post, channel=channel, allow_inlining=inline, @@ -329,7 +329,7 @@ async def create_aggregate( ) message, status, _ = await self.submit( - content=content_.dict(exclude_none=True), + content=content_.model_dump(exclude_none=True), message_type=MessageType.aggregate, channel=channel, allow_inlining=inline, @@ -403,7 +403,7 @@ async def create_store( content = StoreContent(**values) message, status, _ = await self.submit( - content=content.dict(exclude_none=True), + content=content.model_dump(exclude_none=True), message_type=MessageType.store, channel=channel, allow_inlining=True, @@ -496,7 +496,7 @@ async def create_program( assert content.on.persistent == persistent message, status, _ = await self.submit( - content=content.dict(exclude_none=True), + content=content.model_dump(exclude_none=True), message_type=MessageType.program, channel=channel, storage_engine=storage_engine, @@ -572,7 +572,7 @@ async def create_instance( payment=payment, ) message, status, response = await self.submit( - content=content.dict(exclude_none=True), + content=content.model_dump(exclude_none=True), message_type=MessageType.instance, channel=channel, storage_engine=storage_engine, @@ -618,7 +618,7 @@ async def forget( ) message, status, _ = await self.submit( - content=content.dict(exclude_none=True), + content=content.model_dump(exclude_none=True), message_type=MessageType.forget, channel=channel, storage_engine=storage_engine, @@ -662,11 +662,11 @@ async def _storage_push_file_with_message( # Prepare the STORE message message = await self.generate_signed_message( message_type=MessageType.store, - content=store_content.dict(exclude_none=True), + content=store_content.model_dump(exclude_none=True), channel=channel, ) metadata = { - "message": message.dict(exclude_none=True), + "message": message.model_dump(exclude_none=True), "sync": sync, } data.add_field( diff --git a/src/aleph/sdk/client/http.py b/src/aleph/sdk/client/http.py index 2c953d4e..60e65bb2 100644 --- a/src/aleph/sdk/client/http.py +++ b/src/aleph/sdk/client/http.py @@ -180,7 +180,7 @@ async def get_posts( posts: List[Post] = [] for post_raw in posts_raw: try: - posts.append(Post.parse_obj(post_raw)) + posts.append(Post.model_validate(post_raw)) except ValidationError as e: if not ignore_invalid_messages: raise e diff --git a/src/aleph/sdk/client/vm_confidential_client.py b/src/aleph/sdk/client/vm_confidential_client.py index e027b384..0d9d6e18 100644 --- a/src/aleph/sdk/client/vm_confidential_client.py +++ b/src/aleph/sdk/client/vm_confidential_client.py @@ -105,7 +105,7 @@ async def measurement(self, vm_id: ItemHash) -> SEVMeasurement: status, text = await self.perform_operation( vm_id, "confidential/measurement", method="GET" ) - sev_measurement = SEVMeasurement.parse_raw(text) + sev_measurement = SEVMeasurement.model_validate_json(text) return sev_measurement async def validate_measure( diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index 114652b7..e9a46293 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -3,11 +3,13 @@ import os from pathlib import Path from shutil import which -from typing import Dict, Optional, Union +from typing import ClassVar, Dict, Optional, Union + +from pydantic_settings import BaseSettings from aleph_message.models import Chain from aleph_message.models.execution.environment import HypervisorType -from pydantic import BaseModel, BaseSettings, Field +from pydantic import BaseModel, ConfigDict, Field from aleph.sdk.types import ChainInfo @@ -41,7 +43,7 @@ class Settings(BaseSettings): REMOTE_CRYPTO_HOST: Optional[str] = None REMOTE_CRYPTO_UNIX_SOCKET: Optional[str] = None ADDRESS_TO_USE: Optional[str] = None - HTTP_REQUEST_TIMEOUT = 10.0 + HTTP_REQUEST_TIMEOUT: ClassVar[float] = 10.0 DEFAULT_CHANNEL: str = "ALEPH-CLOUDSOLUTIONS" DEFAULT_RUNTIME_ID: str = ( @@ -83,12 +85,12 @@ class Settings(BaseSettings): CODE_USES_SQUASHFS: bool = which("mksquashfs") is not None # True if command exists - VM_URL_PATH = "https://aleph.sh/vm/{hash}" - VM_URL_HOST = "https://{hash_base32}.aleph.sh" + VM_URL_PATH: ClassVar[str] = "https://aleph.sh/vm/{hash}" + VM_URL_HOST: ClassVar[str] = "https://{hash_base32}.aleph.sh" # Web3Provider settings - TOKEN_DECIMALS = 18 - TX_TIMEOUT = 60 * 3 + TOKEN_DECIMALS: ClassVar[int] = 18 + TX_TIMEOUT: ClassVar[int] = 60 * 3 CHAINS: Dict[Union[Chain, str], ChainInfo] = { # TESTNETS "SEPOLIA": ChainInfo( @@ -124,28 +126,29 @@ class Settings(BaseSettings): ), } # Add all placeholders to allow easy dynamic setup of CHAINS - CHAINS_SEPOLIA_ACTIVE: Optional[bool] - CHAINS_ETH_ACTIVE: Optional[bool] - CHAINS_AVAX_ACTIVE: Optional[bool] - CHAINS_BASE_ACTIVE: Optional[bool] - CHAINS_BSC_ACTIVE: Optional[bool] - CHAINS_SEPOLIA_RPC: Optional[str] - CHAINS_ETH_RPC: Optional[str] - CHAINS_AVAX_RPC: Optional[str] - CHAINS_BASE_RPC: Optional[str] - CHAINS_BSC_RPC: Optional[str] + CHAINS_SEPOLIA_ACTIVE: Optional[bool] = None + CHAINS_ETH_ACTIVE: Optional[bool] = None + CHAINS_AVAX_ACTIVE: Optional[bool] = None + CHAINS_BASE_ACTIVE: Optional[bool] = None + CHAINS_BSC_ACTIVE: Optional[bool] = None + CHAINS_SEPOLIA_RPC: Optional[str] = None + CHAINS_ETH_RPC: Optional[str] = None + CHAINS_AVAX_RPC: Optional[str] = None + CHAINS_BASE_RPC: Optional[str] = None + CHAINS_BSC_RPC: Optional[str] = None # Dns resolver - DNS_IPFS_DOMAIN = "ipfs.public.aleph.sh" - DNS_PROGRAM_DOMAIN = "program.public.aleph.sh" - DNS_INSTANCE_DOMAIN = "instance.public.aleph.sh" - DNS_STATIC_DOMAIN = "static.public.aleph.sh" - DNS_RESOLVERS = ["9.9.9.9", "1.1.1.1"] - - class Config: - env_prefix = "ALEPH_" - case_sensitive = False - env_file = ".env" + DNS_IPFS_DOMAIN: ClassVar[str] = "ipfs.public.aleph.sh" + DNS_PROGRAM_DOMAIN: ClassVar[str] = "program.public.aleph.sh" + DNS_INSTANCE_DOMAIN: ClassVar[str] = "instance.public.aleph.sh" + DNS_STATIC_DOMAIN: ClassVar[str] = "static.public.aleph.sh" + DNS_RESOLVERS: ClassVar[str] = ["9.9.9.9", "1.1.1.1"] + + model_config = ConfigDict( + env_prefix="ALEPH_", + case_sensitive=False, + env_file=".env" + ) class MainConfiguration(BaseModel): @@ -156,8 +159,7 @@ class MainConfiguration(BaseModel): path: Path chain: Chain - class Config: - use_enum_values = True + model_config = ConfigDict(use_enum_values = True) # Settings singleton @@ -213,7 +215,7 @@ def save_main_configuration(file_path: Path, data: MainConfiguration): Synchronously save a single ChainAccount object as JSON to a file. """ with file_path.open("w") as file: - data_serializable = data.dict() + data_serializable = data.model_dump() data_serializable["path"] = str(data_serializable["path"]) json.dump(data_serializable, file, indent=4) diff --git a/src/aleph/sdk/query/responses.py b/src/aleph/sdk/query/responses.py index 4b598f12..277a1bea 100644 --- a/src/aleph/sdk/query/responses.py +++ b/src/aleph/sdk/query/responses.py @@ -9,7 +9,7 @@ ItemType, MessageConfirmation, ) -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field class Post(BaseModel): @@ -48,9 +48,9 @@ class Post(BaseModel): ref: Optional[Union[str, Any]] = Field( description="Other message referenced by this one" ) + address: Optional[str] = Field(description="Address of the sender") - class Config: - allow_extra = False + model_config = ConfigDict(extra="forbid") class PaginationResponse(BaseModel): @@ -64,14 +64,14 @@ class PostsResponse(PaginationResponse): """Response from an aleph.im node API on the path /api/v0/posts.json""" posts: List[Post] - pagination_item = "posts" + pagination_item: str = "posts" class MessagesResponse(PaginationResponse): """Response from an aleph.im node API on the path /api/v0/messages.json""" messages: List[AlephMessage] - pagination_item = "messages" + pagination_item: str = "messages" class PriceResponse(BaseModel): diff --git a/src/aleph/sdk/utils.py b/src/aleph/sdk/utils.py index 116c7b42..1cdb6608 100644 --- a/src/aleph/sdk/utils.py +++ b/src/aleph/sdk/utils.py @@ -28,13 +28,15 @@ from uuid import UUID from zipfile import BadZipFile, ZipFile +from pydantic import BaseModel + from aleph_message.models import ItemHash, MessageType from aleph_message.models.execution.program import Encoding from aleph_message.models.execution.volume import MachineVolume from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from jwcrypto.jwa import JWA -from pydantic.json import pydantic_encoder +import pydantic_core from aleph.sdk.conf import settings from aleph.sdk.types import GenericMessage, SEVInfo, SEVMeasurement @@ -173,7 +175,7 @@ def extended_json_encoder(obj: Any) -> Any: elif isinstance(obj, time): return obj.hour * 3600 + obj.minute * 60 + obj.second + obj.microsecond / 1e6 else: - return pydantic_encoder(obj) + return pydantic_core.to_jsonable_python(obj) def parse_volume(volume_dict: Union[Mapping, MachineVolume]) -> MachineVolume: @@ -185,7 +187,7 @@ def parse_volume(volume_dict: Union[Mapping, MachineVolume]) -> MachineVolume: return volume_dict for volume_type in get_args(MachineVolume): try: - return volume_type.parse_obj(volume_dict) + return volume_type.model_validate(volume_dict) except ValueError: continue else: diff --git a/tests/unit/aleph_vm_authentication.py b/tests/unit/aleph_vm_authentication.py index 491da51a..86a4cd20 100644 --- a/tests/unit/aleph_vm_authentication.py +++ b/tests/unit/aleph_vm_authentication.py @@ -13,7 +13,7 @@ from eth_account.messages import encode_defunct from jwcrypto import jwk from jwcrypto.jwa import JWA -from pydantic import BaseModel, ValidationError, root_validator, validator +from pydantic import BaseModel, ValidationError, model_validator, field_validator from aleph.sdk.utils import bytes_from_hex @@ -63,23 +63,23 @@ class SignedPubKeyHeader(BaseModel): signature: bytes payload: bytes - @validator("signature") + @field_validator("signature") def signature_must_be_hex(cls, value: bytes) -> bytes: """Convert the signature from hexadecimal to bytes""" return bytes_from_hex(value.decode()) - @validator("payload") + @field_validator("payload") def payload_must_be_hex(cls, value: bytes) -> bytes: """Convert the payload from hexadecimal to bytes""" return bytes_from_hex(value.decode()) - @root_validator(pre=False, skip_on_failure=True) + @model_validator(mode="after") def check_expiry(cls, values) -> Dict[str, bytes]: """Check that the token has not expired""" - payload: bytes = values["payload"] - content = SignedPubKeyPayload.parse_raw(payload) + payload: bytes = values.payload + content = SignedPubKeyPayload.model_validate_json(payload) if not is_token_still_valid(content.expires): msg = "Token expired" @@ -87,12 +87,12 @@ def check_expiry(cls, values) -> Dict[str, bytes]: return values - @root_validator(pre=False, skip_on_failure=True) + @model_validator(mode="after") def check_signature(cls, values: Dict[str, bytes]) -> Dict[str, bytes]: """Check that the signature is valid""" - signature: bytes = values["signature"] - payload: bytes = values["payload"] - content = SignedPubKeyPayload.parse_raw(payload) + signature: bytes = values.signature + payload: bytes = values.payload + content = SignedPubKeyPayload.model_validate_json(payload) if not verify_wallet_signature(signature, payload.hex(), content.address): msg = "Invalid signature" @@ -103,7 +103,7 @@ def check_signature(cls, values: Dict[str, bytes]) -> Dict[str, bytes]: @property def content(self) -> SignedPubKeyPayload: """Return the content of the header""" - return SignedPubKeyPayload.parse_raw(self.payload) + return SignedPubKeyPayload.model_validate_json(self.payload) class SignedOperationPayload(BaseModel): @@ -113,7 +113,7 @@ class SignedOperationPayload(BaseModel): path: str # body_sha256: str # disabled since there is no body - @validator("time") + @field_validator("time") def time_is_current(cls, v: datetime.datetime) -> datetime.datetime: """Check that the time is current and the payload is not a replay attack.""" max_past = datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta( @@ -135,7 +135,7 @@ class SignedOperation(BaseModel): signature: bytes payload: bytes - @validator("signature") + @field_validator("signature") def signature_must_be_hex(cls, value: str) -> bytes: """Convert the signature from hexadecimal to bytes""" @@ -147,17 +147,17 @@ def signature_must_be_hex(cls, value: str) -> bytes: logger.warning(value) raise error - @validator("payload") + @field_validator("payload") def payload_must_be_hex(cls, v) -> bytes: """Convert the payload from hexadecimal to bytes""" v = bytes.fromhex(v.decode()) - _ = SignedOperationPayload.parse_raw(v) + _ = SignedOperationPayload.model_validate_json(v) return v @property def content(self) -> SignedOperationPayload: """Return the content of the header""" - return SignedOperationPayload.parse_raw(self.payload) + return SignedOperationPayload.model_validate_json(self.payload) def get_signed_pubkey(request: web.Request) -> SignedPubKeyHeader: @@ -168,7 +168,7 @@ def get_signed_pubkey(request: web.Request) -> SignedPubKeyHeader: raise web.HTTPBadRequest(reason="Missing X-SignedPubKey header") try: - return SignedPubKeyHeader.parse_raw(signed_pubkey_header) + return SignedPubKeyHeader.model_validate_json(signed_pubkey_header) except KeyError as error: logger.debug(f"Missing X-SignedPubKey header: {error}") @@ -199,7 +199,7 @@ def get_signed_operation(request: web.Request) -> SignedOperation: """Get the signed operation public key that is signed by the ephemeral key from the request headers.""" try: signed_operation = request.headers["X-SignedOperation"] - return SignedOperation.parse_raw(signed_operation) + return SignedOperation.model_validate_json(signed_operation) except KeyError as error: raise web.HTTPBadRequest(reason="Missing X-SignedOperation header") from error except json.JSONDecodeError as error: @@ -259,8 +259,8 @@ async def authenticate_websocket_message( message, domain_name: Optional[str] = DOMAIN_NAME ) -> str: """Authenticate a websocket message since JS cannot configure headers on WebSockets.""" - signed_pubkey = SignedPubKeyHeader.parse_obj(message["X-SignedPubKey"]) - signed_operation = SignedOperation.parse_obj(message["X-SignedOperation"]) + signed_pubkey = SignedPubKeyHeader.model_validate(message["X-SignedPubKey"]) + signed_operation = SignedOperation.model_validate(message["X-SignedOperation"]) if signed_operation.content.domain != domain_name: logger.debug( f"Invalid domain '{signed_pubkey.content.domain}' != '{domain_name}'" diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index c1c56fcd..bea0d886 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -71,7 +71,7 @@ def rejected_message(): @pytest.fixture def aleph_messages() -> List[AlephMessage]: return [ - AggregateMessage.parse_obj( + AggregateMessage.model_validate( { "item_hash": "5b26d949fe05e38f535ef990a89da0473f9d700077cced228f2d36e73fca1fd6", "type": "AGGREGATE", @@ -95,7 +95,7 @@ def aleph_messages() -> List[AlephMessage]: "confirmed": False, } ), - PostMessage.parse_obj( + PostMessage.model_validate( { "item_hash": "70f3798fdc68ce0ee03715a5547ee24e2c3e259bf02e3f5d1e4bf5a6f6a5e99f", "type": "POST", @@ -135,7 +135,7 @@ def json_post() -> dict: def raw_messages_response(aleph_messages) -> Callable[[int], Dict[str, Any]]: return lambda page: { "messages": ( - [message.dict() for message in aleph_messages] if int(page) == 1 else [] + [message.model_dump() for message in aleph_messages] if int(page) == 1 else [] ), "pagination_item": "messages", "pagination_page": int(page), diff --git a/tests/unit/test_remote_account.py b/tests/unit/test_remote_account.py index cb4a2af5..3abe979e 100644 --- a/tests/unit/test_remote_account.py +++ b/tests/unit/test_remote_account.py @@ -22,7 +22,7 @@ async def test_remote_storage(): curve="secp256k1", address=local_account.get_address(), public_key=local_account.get_public_key(), - ).dict() + ).model_dump() ) remote_account = await RemoteAccount.from_crypto_host( diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index bfca23a5..df27ee2f 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -115,16 +115,17 @@ def test_enum_as_str(): ( MessageType.aggregate, { - "content": { - "Hello": MachineResources( - vcpus=1, - memory=1024, - seconds=1, - ) + 'address': '0x1', + 'content': { + 'Hello': { + 'vcpus': 1, + 'memory': 1024, + 'seconds': 1, + 'published_ports': None, + }, }, - "key": "test", - "address": "0x1", - "time": 1.0, + 'key': 'test', + 'time': 1.0, }, ), ], @@ -140,7 +141,7 @@ async def test_prepare_aleph_message( channel="TEST", ) - assert message.content.dict() == content + assert message.content.model_dump() == content def test_parse_immutable_volume(): @@ -215,7 +216,7 @@ def test_compute_confidential_measure(): assert base64.b64encode(tik) == b"npOTEc4mtRGfXfB+G6EBdw==" expected_hash = "d06471f485c0a61aba5a431ec136b947be56907acf6ed96afb11788ae4525aeb" nonce = base64.b64decode("URQNqJAqh/2ep4drjx/XvA==") - sev_info = SEVInfo.parse_obj( + sev_info = SEVInfo.model_validate( { "enabled": True, "api_major": 1, diff --git a/tests/unit/test_vm_client.py b/tests/unit/test_vm_client.py index 7cc9a2c3..d9a9a36b 100644 --- a/tests/unit/test_vm_client.py +++ b/tests/unit/test_vm_client.py @@ -290,8 +290,8 @@ async def test_vm_client_generate_correct_authentication_headers(): ) path, headers = await vm_client._generate_header(vm_id, "reboot", method="post") - signed_pubkey = SignedPubKeyHeader.parse_raw(headers["X-SignedPubKey"]) - signed_operation = SignedOperation.parse_raw(headers["X-SignedOperation"]) + signed_pubkey = SignedPubKeyHeader.model_validate_json(headers["X-SignedPubKey"]) + signed_operation = SignedOperation.model_validate_json(headers["X-SignedOperation"]) address = verify_signed_operation(signed_operation, signed_pubkey) assert vm_client.account.get_address() == address From 660762f1ac72c7386310aae339b5a207c7e5e56a Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Fri, 4 Oct 2024 11:46:22 +0900 Subject: [PATCH 02/37] fix: lint tests were failing - Updated all instances of **extra_fields to ensure proper handling of Optional dictionaries using `(extra_fields or {})` pattern. - Added proper return statements in `AlephHttpClient.get_message_status` to return parsed JSON data as a `MessageStatus` object. - Updated `Settings` class in `conf.py` to correct DNS resolvers type and simplify the `model_config` definition. - Refactored `parse_volume` to ensure correct handling of Mapping types and MachineVolume types, avoiding TypeErrors. - Improved field validation and model validation in `SignedPubKeyHeader` by using correct Pydantic v2 validation decorators and ensuring compatibility with the new model behavior. - Applied formatting and consistency fixes for `model_dump` usage and indentation improvements in test files. --- src/aleph/sdk/client/authenticated_http.py | 2 +- src/aleph/sdk/client/http.py | 3 +++ src/aleph/sdk/conf.py | 11 ++++------ src/aleph/sdk/utils.py | 25 +++++++++++----------- tests/unit/aleph_vm_authentication.py | 8 +++---- tests/unit/conftest.py | 4 +++- tests/unit/test_utils.py | 19 ++++++++-------- 7 files changed, 35 insertions(+), 37 deletions(-) diff --git a/src/aleph/sdk/client/authenticated_http.py b/src/aleph/sdk/client/authenticated_http.py index 2f30e534..ad15ca09 100644 --- a/src/aleph/sdk/client/authenticated_http.py +++ b/src/aleph/sdk/client/authenticated_http.py @@ -710,7 +710,7 @@ async def _upload_file_native( item_hash=file_hash, mime_type=mime_type, time=time.time(), - **extra_fields, + **(extra_fields or {}), ) message, _ = await self._storage_push_file_with_message( file_content=file_content, diff --git a/src/aleph/sdk/client/http.py b/src/aleph/sdk/client/http.py index 60e65bb2..a59531c6 100644 --- a/src/aleph/sdk/client/http.py +++ b/src/aleph/sdk/client/http.py @@ -467,3 +467,6 @@ async def get_message_status(self, item_hash: str) -> MessageStatus: if resp.status == HTTPNotFound.status_code: raise MessageNotFoundError(f"No such hash {item_hash}") resp.raise_for_status() + + data = await resp.json() + return MessageStatus(**data) diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index e9a46293..fbf77e53 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -5,11 +5,10 @@ from shutil import which from typing import ClassVar, Dict, Optional, Union -from pydantic_settings import BaseSettings - from aleph_message.models import Chain from aleph_message.models.execution.environment import HypervisorType from pydantic import BaseModel, ConfigDict, Field +from pydantic_settings import BaseSettings from aleph.sdk.types import ChainInfo @@ -142,12 +141,10 @@ class Settings(BaseSettings): DNS_PROGRAM_DOMAIN: ClassVar[str] = "program.public.aleph.sh" DNS_INSTANCE_DOMAIN: ClassVar[str] = "instance.public.aleph.sh" DNS_STATIC_DOMAIN: ClassVar[str] = "static.public.aleph.sh" - DNS_RESOLVERS: ClassVar[str] = ["9.9.9.9", "1.1.1.1"] + DNS_RESOLVERS: ClassVar[list[str]] = ["9.9.9.9", "1.1.1.1"] model_config = ConfigDict( - env_prefix="ALEPH_", - case_sensitive=False, - env_file=".env" + env_prefix="ALEPH_", case_sensitive=False, env_file=".env" ) @@ -159,7 +156,7 @@ class MainConfiguration(BaseModel): path: Path chain: Chain - model_config = ConfigDict(use_enum_values = True) + model_config = ConfigDict(use_enum_values=True) # Settings singleton diff --git a/src/aleph/sdk/utils.py b/src/aleph/sdk/utils.py index 1cdb6608..9f69d2d7 100644 --- a/src/aleph/sdk/utils.py +++ b/src/aleph/sdk/utils.py @@ -28,15 +28,13 @@ from uuid import UUID from zipfile import BadZipFile, ZipFile -from pydantic import BaseModel - +import pydantic_core from aleph_message.models import ItemHash, MessageType from aleph_message.models.execution.program import Encoding from aleph_message.models.execution.volume import MachineVolume from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from jwcrypto.jwa import JWA -import pydantic_core from aleph.sdk.conf import settings from aleph.sdk.types import GenericMessage, SEVInfo, SEVMeasurement @@ -181,17 +179,18 @@ def extended_json_encoder(obj: Any) -> Any: def parse_volume(volume_dict: Union[Mapping, MachineVolume]) -> MachineVolume: # Python 3.9 does not support `isinstance(volume_dict, MachineVolume)`, # so we need to iterate over all types. - if any( - isinstance(volume_dict, volume_type) for volume_type in get_args(MachineVolume) - ): - return volume_dict for volume_type in get_args(MachineVolume): - try: - return volume_type.model_validate(volume_dict) - except ValueError: - continue - else: - raise ValueError(f"Could not parse volume: {volume_dict}") + if isinstance(volume_dict, volume_type): + return volume_dict + + if isinstance(volume_dict, Mapping): + for volume_type in get_args(MachineVolume): + try: + return volume_type(**volume_dict) + except (TypeError, ValueError): + continue + + raise ValueError("Invalid volume data, could not be parsed into a MachineVolume") def compute_sha256(s: str) -> str: diff --git a/tests/unit/aleph_vm_authentication.py b/tests/unit/aleph_vm_authentication.py index 86a4cd20..4a048b50 100644 --- a/tests/unit/aleph_vm_authentication.py +++ b/tests/unit/aleph_vm_authentication.py @@ -13,7 +13,7 @@ from eth_account.messages import encode_defunct from jwcrypto import jwk from jwcrypto.jwa import JWA -from pydantic import BaseModel, ValidationError, model_validator, field_validator +from pydantic import BaseModel, ValidationError, field_validator, model_validator from aleph.sdk.utils import bytes_from_hex @@ -66,17 +66,15 @@ class SignedPubKeyHeader(BaseModel): @field_validator("signature") def signature_must_be_hex(cls, value: bytes) -> bytes: """Convert the signature from hexadecimal to bytes""" - return bytes_from_hex(value.decode()) @field_validator("payload") def payload_must_be_hex(cls, value: bytes) -> bytes: """Convert the payload from hexadecimal to bytes""" - return bytes_from_hex(value.decode()) @model_validator(mode="after") - def check_expiry(cls, values) -> Dict[str, bytes]: + def check_expiry(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": """Check that the token has not expired""" payload: bytes = values.payload content = SignedPubKeyPayload.model_validate_json(payload) @@ -88,7 +86,7 @@ def check_expiry(cls, values) -> Dict[str, bytes]: return values @model_validator(mode="after") - def check_signature(cls, values: Dict[str, bytes]) -> Dict[str, bytes]: + def check_signature(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": """Check that the signature is valid""" signature: bytes = values.signature payload: bytes = values.payload diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index bea0d886..385d2836 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -135,7 +135,9 @@ def json_post() -> dict: def raw_messages_response(aleph_messages) -> Callable[[int], Dict[str, Any]]: return lambda page: { "messages": ( - [message.model_dump() for message in aleph_messages] if int(page) == 1 else [] + [message.model_dump() for message in aleph_messages] + if int(page) == 1 + else [] ), "pagination_item": "messages", "pagination_page": int(page), diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index df27ee2f..8e4083c0 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -12,7 +12,6 @@ ProgramMessage, StoreMessage, ) -from aleph_message.models.execution.environment import MachineResources from aleph_message.models.execution.volume import ( EphemeralVolume, ImmutableVolume, @@ -115,17 +114,17 @@ def test_enum_as_str(): ( MessageType.aggregate, { - 'address': '0x1', - 'content': { - 'Hello': { - 'vcpus': 1, - 'memory': 1024, - 'seconds': 1, - 'published_ports': None, + "address": "0x1", + "content": { + "Hello": { + "vcpus": 1, + "memory": 1024, + "seconds": 1, + "published_ports": None, }, }, - 'key': 'test', - 'time': 1.0, + "key": "test", + "time": 1.0, }, ), ], From 4908fbedc5af2640ccc869a27445145fd1797b1d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 25 Sep 2024 23:18:08 +0200 Subject: [PATCH 03/37] feat: add pyproject-fmt --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 8a7bbc09..1c7ae2da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -154,6 +154,7 @@ dependencies = [ "mypy-extensions==1.0.0", "ruff==0.4.8", "isort==5.13.2", + "pyproject-fmt==2.2.1", ] [tool.hatch.envs.linting.scripts] typing = "mypy --config-file=pyproject.toml {args:} ./src/ ./tests/ ./examples/" @@ -161,11 +162,13 @@ style = [ "ruff check {args:.} ./src/ ./tests/ ./examples/", "black --check --diff {args:} ./src/ ./tests/ ./examples/", "isort --check-only --profile black {args:} ./src/ ./tests/ ./examples/", + "pyproject-fmt --check pyproject.toml", ] fmt = [ "black {args:} ./src/ ./tests/ ./examples/", "ruff check --fix {args:.} ./src/ ./tests/ ./examples/", "isort --profile black {args:} ./src/ ./tests/ ./examples/", + "pyproject-fmt pyproject.toml", "style", ] all = [ From e50fabbec67e37e7cdb65b69ab5adf963d9edb63 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 25 Sep 2024 23:19:03 +0200 Subject: [PATCH 04/37] fix: run pyproject-fmt --- pyproject.toml | 321 +++++++++++++++++++++++++------------------------ 1 file changed, 161 insertions(+), 160 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1c7ae2da..409694cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,138 +1,137 @@ [build-system] -requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" +requires = [ "hatch-vcs", "hatchling" ] + [project] name = "aleph-sdk-python" -dynamic = ["version"] description = "Lightweight Python Client library for the Aleph.im network" readme = "README.md" license = { file = "LICENSE.txt" } authors = [ - { name = "Aleph.im Team", email = "hello@aleph.im" }, + { name = "Aleph.im Team", email = "hello@aleph.im" }, ] classifiers = [ - "Programming Language :: Python :: 3", - "Development Status :: 4 - Beta", - "Framework :: aiohttp", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: POSIX :: Linux", - "Operating System :: MacOS :: MacOS X", - "Topic :: Software Development :: Libraries", -] + "Development Status :: 4 - Beta", + "Framework :: aiohttp", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries", +] +dynamic = [ "version" ] dependencies = [ - "aiohttp>=3.8.3", - "aleph-message>=0.4.9", - "coincurve; python_version<\"3.11\"", - "coincurve>=19.0.0; python_version>=\"3.11\"", - "eth_abi>=4.0.0; python_version>=\"3.11\"", - "jwcrypto==1.5.6", - "python-magic", - "pydantic-settings", - "typing_extensions", - "aioresponses>=0.7.6", - "aleph-superfluid>=0.2.1", - "eth_typing==4.3.1", - "web3==6.3.0", - "base58==2.1.1", # Needed now as default with _load_account changement - "pynacl==1.5.0" # Needed now as default with _load_account changement -] - -[project.optional-dependencies] -cosmos = [ - "cosmospy", -] -dns = [ - "aiodns", -] -docs = [ - "sphinxcontrib-plantuml", -] -ledger = [ - "ledgereth==0.9.1", -] -mqtt = [ - "aiomqtt<=0.1.3", - "certifi", - "Click", -] -nuls2 = [ - "aleph-nuls2", -] -substrate = [ - "py-sr25519-bindings", - "substrate-interface", -] -solana = [ - "base58", - "pynacl", -] -tezos = [ - "aleph-pytezos==0.1.1", - "pynacl", -] -encryption = [ - "eciespy; python_version<\"3.11\"", - "eciespy>=0.3.13; python_version>=\"3.11\"", + "aiohttp>=3.8.3", + "aioresponses>=0.7.6", + "aleph-message>=0.4.9", + "aleph-superfluid>=0.2.1", + "base58==2.1.1", # Needed now as default with _load_account changement + "coincurve; python_version<'3.11'", + "coincurve>=19; python_version>='3.11'", + "eth-abi>=4; python_version>='3.11'", + "eth-typing==4.3.1", + "jwcrypto==1.5.6", + "pynacl==1.5", # Needed now as default with _load_account changement + "python-magic", + "typing-extensions", + "web3==6.3", ] -all = [ - "aleph-sdk-python[cosmos,dns,docs,ledger,mqtt,nuls2,substrate,solana,tezos,encryption]", + +optional-dependencies.all = [ + "aleph-sdk-python[cosmos,dns,docs,ledger,mqtt,nuls2,substrate,solana,tezos,encryption]", +] +optional-dependencies.cosmos = [ + "cosmospy", +] +optional-dependencies.dns = [ + "aiodns", +] +optional-dependencies.docs = [ + "sphinxcontrib-plantuml", +] +optional-dependencies.encryption = [ + "eciespy; python_version<'3.11'", + "eciespy>=0.3.13; python_version>='3.11'", ] +optional-dependencies.ledger = [ + "ledgereth==0.9.1", +] +optional-dependencies.mqtt = [ + "aiomqtt<=0.1.3", + "certifi", + "click", +] +optional-dependencies.nuls2 = [ + "aleph-nuls2", +] +optional-dependencies.solana = [ + "base58", + "pynacl", +] +optional-dependencies.substrate = [ + "py-sr25519-bindings", + "substrate-interface", +] +optional-dependencies.tezos = [ + "aleph-pytezos==0.1.1", + "pynacl", +] +urls.Documentation = "https://aleph.im/" +urls.Homepage = "https://github.com/aleph-im/aleph-sdk-python" [tool.hatch.metadata] allow-direct-references = true -[project.urls] -Documentation = "https://aleph.im/" -Homepage = "https://github.com/aleph-im/aleph-sdk-python" - [tool.hatch.version] source = "vcs" [tool.hatch.build.targets.wheel] packages = [ - "src/aleph", - "pyproject.toml", - "README.md", - "LICENSE.txt", + "src/aleph", + "pyproject.toml", + "README.md", + "LICENSE.txt", ] [tool.hatch.build.targets.sdist] include = [ - "src/aleph", - "pyproject.toml", - "README.md", - "LICENSE.txt", + "src/aleph", + "pyproject.toml", + "README.md", + "LICENSE.txt", ] -[tool.isort] -profile = "black" - [[tool.hatch.envs.all.matrix]] -python = ["3.9", "3.10", "3.11"] +python = [ "3.9", "3.10", "3.11" ] [tool.hatch.envs.testing] features = [ - "cosmos", - "dns", - "ledger", - "nuls2", - "substrate", - "solana", - "tezos", - "encryption", + "cosmos", + "dns", + "ledger", + "nuls2", + "substrate", + "solana", + "tezos", + "encryption", ] dependencies = [ - "pytest==8.0.1", - "pytest-cov==4.1.0", - "pytest-mock==3.12.0", - "pytest-asyncio==0.23.5", - "pytest-aiohttp==1.0.5", - "aioresponses==0.7.6", - "fastapi", - "httpx", - "secp256k1", + "pytest==8.0.1", + "pytest-cov==4.1.0", + "pytest-mock==3.12.0", + "pytest-asyncio==0.23.5", + "pytest-aiohttp==1.0.5", + "aioresponses==0.7.6", + "fastapi", + "httpx", + "secp256k1", ] [tool.hatch.envs.testing.scripts] test = "pytest {args:} ./src/ ./tests/ ./examples/" @@ -149,101 +148,103 @@ cov = [ [tool.hatch.envs.linting] detached = true dependencies = [ - "black==24.2.0", - "mypy==1.9.0", - "mypy-extensions==1.0.0", - "ruff==0.4.8", - "isort==5.13.2", - "pyproject-fmt==2.2.1", + "black==24.2.0", + "mypy==1.9.0", + "mypy-extensions==1.0.0", + "ruff==0.4.8", + "isort==5.13.2", + "pyproject-fmt==2.2.1", ] [tool.hatch.envs.linting.scripts] typing = "mypy --config-file=pyproject.toml {args:} ./src/ ./tests/ ./examples/" style = [ - "ruff check {args:.} ./src/ ./tests/ ./examples/", - "black --check --diff {args:} ./src/ ./tests/ ./examples/", - "isort --check-only --profile black {args:} ./src/ ./tests/ ./examples/", - "pyproject-fmt --check pyproject.toml", + "ruff check {args:.} ./src/ ./tests/ ./examples/", + "black --check --diff {args:} ./src/ ./tests/ ./examples/", + "isort --check-only --profile black {args:} ./src/ ./tests/ ./examples/", + "pyproject-fmt --check pyproject.toml", ] fmt = [ - "black {args:} ./src/ ./tests/ ./examples/", - "ruff check --fix {args:.} ./src/ ./tests/ ./examples/", - "isort --profile black {args:} ./src/ ./tests/ ./examples/", - "pyproject-fmt pyproject.toml", - "style", + "black {args:} ./src/ ./tests/ ./examples/", + "ruff check --fix {args:.} ./src/ ./tests/ ./examples/", + "isort --profile black {args:} ./src/ ./tests/ ./examples/", + "pyproject-fmt pyproject.toml", + "style", ] all = [ - "style", - "typing", -] - -[tool.mypy] -python_version = 3.9 -mypy_path = "src" -exclude = [ - "conftest.py" + "style", + "typing", ] -show_column_numbers = true -check_untyped_defs = true - -# Import discovery -# Install types for third-party library stubs (e.g. from typeshed repository) -install_types = true -non_interactive = true -# Suppresses error messages about imports that cannot be resolved (no py.typed file, no stub file, etc). -ignore_missing_imports = true -# Don't follow imports -follow_imports = "silent" - -# Miscellaneous strictness flags -# Allows variables to be redefined with an arbitrary type, as long as the redefinition is in the same block and nesting level as the original definition. -allow_redefinition = true +[tool.isort] +profile = "black" [tool.pytest.ini_options] minversion = "6.0" -pythonpath = ["src"] +pythonpath = [ "src" ] addopts = "-vv -m \"not ledger_hardware\"" -norecursedirs = ["*.egg", "dist", "build", ".tox", ".venv", "*/site-packages/*"] -testpaths = ["tests/unit"] -markers = {ledger_hardware = "marks tests as requiring ledger hardware"} +norecursedirs = [ "*.egg", "dist", "build", ".tox", ".venv", "*/site-packages/*" ] +testpaths = [ "tests/unit" ] +markers = { ledger_hardware = "marks tests as requiring ledger hardware" } [tool.coverage.run] branch = true parallel = true source = [ - "src/", + "src/", ] omit = [ - "*/site-packages/*", + "*/site-packages/*", ] [tool.coverage.paths] source = [ - "src/", + "src/", ] omit = [ - "*/site-packages/*", + "*/site-packages/*", ] [tool.coverage.report] show_missing = true skip_empty = true exclude_lines = [ - # Have to re-enable the standard pragma - "pragma: no cover", + # Have to re-enable the standard pragma + "pragma: no cover", - # Don't complain about missing debug-only code: - "def __repr__", - "if self\\.debug", + # Don't complain about missing debug-only code: + "def __repr__", + "if self\\.debug", - # Don't complain if tests don't hit defensive assertion code: - "raise AssertionError", - "raise NotImplementedError", + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", - # Don't complain if non-runnable code isn't run: - "if 0:", - "if __name__ == .__main__.:", + # Don't complain if non-runnable code isn't run: + "if 0:", + "if __name__ == .__main__.:", + + # Don't complain about ineffective code: + "pass", +] - # Don't complain about ineffective code: - "pass", +[tool.mypy] +python_version = 3.9 +mypy_path = "src" +exclude = [ + "conftest.py", ] +show_column_numbers = true +check_untyped_defs = true + +# Import discovery +# Install types for third-party library stubs (e.g. from typeshed repository) +install_types = true +non_interactive = true +# Suppresses error messages about imports that cannot be resolved (no py.typed file, no stub file, etc). +ignore_missing_imports = true +# Don't follow imports +follow_imports = "silent" + +# Miscellaneous strictness flags +# Allows variables to be redefined with an arbitrary type, as long as the redefinition is in the same block and nesting level as the original definition. +allow_redefinition = true From 29b3a1bb7cbe0f38d07e88ba9780a4009a835eb7 Mon Sep 17 00:00:00 2001 From: philogicae Date: Fri, 11 Oct 2024 20:01:12 +0300 Subject: [PATCH 05/37] Post-SOL fixes (#178) * Missing chain field on auth * Fix Signature of Solana operation for CRN * Add export_private_key func for accounts * Improve _load_account * Add chain arg to _load_account * Increase default HTTP_REQUEST_TIMEOUT * Typing --------- Co-authored-by: Olivier Le Thanh Duong --- src/aleph/sdk/account.py | 108 +++++++++++++++++++----------- src/aleph/sdk/chains/ethereum.py | 5 ++ src/aleph/sdk/chains/solana.py | 6 ++ src/aleph/sdk/client/vm_client.py | 17 +++-- src/aleph/sdk/conf.py | 2 +- src/aleph/sdk/types.py | 4 ++ 6 files changed, 97 insertions(+), 45 deletions(-) diff --git a/src/aleph/sdk/account.py b/src/aleph/sdk/account.py index 8c067283..9bfafcd3 100644 --- a/src/aleph/sdk/account.py +++ b/src/aleph/sdk/account.py @@ -10,67 +10,95 @@ from aleph.sdk.chains.remote import RemoteAccount from aleph.sdk.chains.solana import SOLAccount from aleph.sdk.conf import load_main_configuration, settings +from aleph.sdk.evm_utils import get_chains_with_super_token from aleph.sdk.types import AccountFromPrivateKey logger = logging.getLogger(__name__) T = TypeVar("T", bound=AccountFromPrivateKey) +chain_account_map: Dict[Chain, Type[T]] = { # type: ignore + Chain.ETH: ETHAccount, + Chain.AVAX: ETHAccount, + Chain.BASE: ETHAccount, + Chain.SOL: SOLAccount, +} + def load_chain_account_type(chain: Chain) -> Type[AccountFromPrivateKey]: - chain_account_map: Dict[Chain, Type[AccountFromPrivateKey]] = { - Chain.ETH: ETHAccount, - Chain.AVAX: ETHAccount, - Chain.SOL: SOLAccount, - Chain.BASE: ETHAccount, - } - return chain_account_map.get(chain) or ETHAccount + return chain_account_map.get(chain) or ETHAccount # type: ignore -def account_from_hex_string(private_key_str: str, account_type: Type[T]) -> T: +def account_from_hex_string( + private_key_str: str, + account_type: Optional[Type[T]], + chain: Optional[Chain] = None, +) -> AccountFromPrivateKey: if private_key_str.startswith("0x"): private_key_str = private_key_str[2:] - return account_type(bytes.fromhex(private_key_str)) + if not chain: + if not account_type: + account_type = load_chain_account_type(Chain.ETH) # type: ignore + return account_type(bytes.fromhex(private_key_str)) # type: ignore + + account_type = load_chain_account_type(chain) + account = account_type(bytes.fromhex(private_key_str)) + if chain in get_chains_with_super_token(): + account.switch_chain(chain) + return account # type: ignore -def account_from_file(private_key_path: Path, account_type: Type[T]) -> T: + +def account_from_file( + private_key_path: Path, + account_type: Optional[Type[T]], + chain: Optional[Chain] = None, +) -> AccountFromPrivateKey: private_key = private_key_path.read_bytes() - return account_type(private_key) + + if not chain: + if not account_type: + account_type = load_chain_account_type(Chain.ETH) # type: ignore + return account_type(private_key) # type: ignore + + account_type = load_chain_account_type(chain) + account = account_type(private_key) + if chain in get_chains_with_super_token(): + account.switch_chain(chain) + return account def _load_account( private_key_str: Optional[str] = None, private_key_path: Optional[Path] = None, account_type: Optional[Type[AccountFromPrivateKey]] = None, + chain: Optional[Chain] = None, ) -> AccountFromPrivateKey: - """Load private key from a string or a file. takes the string argument in priority""" - if private_key_str or (private_key_path and private_key_path.is_file()): - if account_type: - if private_key_path and private_key_path.is_file(): - return account_from_file(private_key_path, account_type) - elif private_key_str: - return account_from_hex_string(private_key_str, account_type) - else: - raise ValueError("Any private key specified") + """Load an account from a private key string or file, or from the configuration file.""" + + # Loads configuration if no account_type is specified + if not account_type: + config = load_main_configuration(settings.CONFIG_FILE) + if config and hasattr(config, "chain"): + account_type = load_chain_account_type(config.chain) + logger.debug( + f"Detected {config.chain} account for path {settings.CONFIG_FILE}" + ) else: - main_configuration = load_main_configuration(settings.CONFIG_FILE) - if main_configuration: - account_type = load_chain_account_type(main_configuration.chain) - logger.debug( - f"Detected {main_configuration.chain} account for path {settings.CONFIG_FILE}" - ) - else: - account_type = ETHAccount # Defaults to ETHAccount - logger.warning( - f"No main configuration data found in {settings.CONFIG_FILE}, defaulting to {account_type.__name__}" - ) - if private_key_path and private_key_path.is_file(): - return account_from_file(private_key_path, account_type) - elif private_key_str: - return account_from_hex_string(private_key_str, account_type) - else: - raise ValueError("Any private key specified") + account_type = account_type = load_chain_account_type( + Chain.ETH + ) # Defaults to ETHAccount + logger.warning( + f"No main configuration data found in {settings.CONFIG_FILE}, defaulting to {account_type and account_type.__name__}" + ) + # Loads private key from a string + if private_key_str: + return account_from_hex_string(private_key_str, account_type, chain) + # Loads private key from a file + elif private_key_path and private_key_path.is_file(): + return account_from_file(private_key_path, account_type, chain) + # For ledger keys elif settings.REMOTE_CRYPTO_HOST: logger.debug("Using remote account") loop = asyncio.get_event_loop() @@ -80,10 +108,12 @@ def _load_account( unix_socket=settings.REMOTE_CRYPTO_UNIX_SOCKET, ) ) + # Fallback: config.path if set, else generate a new private key else: - account_type = ETHAccount # Defaults to ETHAccount new_private_key = get_fallback_private_key() - account = account_type(private_key=new_private_key) + account = account_from_hex_string( + bytes.hex(new_private_key), account_type, chain + ) logger.info( f"Generated fallback private key with address {account.get_address()}" ) diff --git a/src/aleph/sdk/chains/ethereum.py b/src/aleph/sdk/chains/ethereum.py index 32f459b7..ab93df56 100644 --- a/src/aleph/sdk/chains/ethereum.py +++ b/src/aleph/sdk/chains/ethereum.py @@ -1,4 +1,5 @@ import asyncio +import base64 from decimal import Decimal from pathlib import Path from typing import Awaitable, Optional, Union @@ -61,6 +62,10 @@ def from_mnemonic(mnemonic: str, chain: Optional[Chain] = None) -> "ETHAccount": private_key=Account.from_mnemonic(mnemonic=mnemonic).key, chain=chain ) + def export_private_key(self) -> str: + """Export the private key using standard format.""" + return f"0x{base64.b16encode(self.private_key).decode().lower()}" + def get_address(self) -> str: return self._account.address diff --git a/src/aleph/sdk/chains/solana.py b/src/aleph/sdk/chains/solana.py index a9352489..920ca8a0 100644 --- a/src/aleph/sdk/chains/solana.py +++ b/src/aleph/sdk/chains/solana.py @@ -43,6 +43,12 @@ async def sign_raw(self, buffer: bytes) -> bytes: sig = self._signing_key.sign(buffer) return sig.signature + def export_private_key(self) -> str: + """Export the private key using Phantom format.""" + return base58.b58encode( + self.private_key + self._signing_key.verify_key.encode() + ).decode() + def get_address(self) -> str: return encode(self._signing_key.verify_key) diff --git a/src/aleph/sdk/client/vm_client.py b/src/aleph/sdk/client/vm_client.py index 18d280cc..83b00dc9 100644 --- a/src/aleph/sdk/client/vm_client.py +++ b/src/aleph/sdk/client/vm_client.py @@ -5,10 +5,11 @@ from urllib.parse import urlparse import aiohttp -from aleph_message.models import ItemHash +from aleph_message.models import Chain, ItemHash from eth_account.messages import encode_defunct from jwcrypto import jwk +from aleph.sdk.chains.solana import SOLAccount from aleph.sdk.types import Account from aleph.sdk.utils import ( create_vm_control_payload, @@ -36,11 +37,13 @@ def __init__( self.account = account self.ephemeral_key = jwk.JWK.generate(kty="EC", crv="P-256") self.node_url = node_url.rstrip("/") - self.pubkey_payload = self._generate_pubkey_payload() + self.pubkey_payload = self._generate_pubkey_payload( + Chain.SOL if isinstance(account, SOLAccount) else Chain.ETH + ) self.pubkey_signature_header = "" self.session = session or aiohttp.ClientSession() - def _generate_pubkey_payload(self) -> Dict[str, Any]: + def _generate_pubkey_payload(self, chain: Chain = Chain.ETH) -> Dict[str, Any]: return { "pubkey": json.loads(self.ephemeral_key.export_public()), "alg": "ECDSA", @@ -50,12 +53,16 @@ def _generate_pubkey_payload(self) -> Dict[str, Any]: datetime.datetime.utcnow() + datetime.timedelta(days=1) ).isoformat() + "Z", + "chain": chain.value, } async def _generate_pubkey_signature_header(self) -> str: pubkey_payload = json.dumps(self.pubkey_payload).encode("utf-8").hex() - signable_message = encode_defunct(hexstr=pubkey_payload) - buffer_to_sign = signable_message.body + if isinstance(self.account, SOLAccount): + buffer_to_sign = bytes(pubkey_payload, encoding="utf-8") + else: + signable_message = encode_defunct(hexstr=pubkey_payload) + buffer_to_sign = signable_message.body signed_message = await self.account.sign_raw(buffer_to_sign) pubkey_signature = to_0x_hex(signed_message) diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index fbf77e53..090c290b 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -42,7 +42,7 @@ class Settings(BaseSettings): REMOTE_CRYPTO_HOST: Optional[str] = None REMOTE_CRYPTO_UNIX_SOCKET: Optional[str] = None ADDRESS_TO_USE: Optional[str] = None - HTTP_REQUEST_TIMEOUT: ClassVar[float] = 10.0 + HTTP_REQUEST_TIMEOUT = 15.0 DEFAULT_CHANNEL: str = "ALEPH-CLOUDSOLUTIONS" DEFAULT_RUNTIME_ID: str = ( diff --git a/src/aleph/sdk/types.py b/src/aleph/sdk/types.py index 081a3465..dab90379 100644 --- a/src/aleph/sdk/types.py +++ b/src/aleph/sdk/types.py @@ -39,6 +39,10 @@ def __init__(self, private_key: bytes): ... async def sign_raw(self, buffer: bytes) -> bytes: ... + def export_private_key(self) -> str: ... + + def switch_chain(self, chain: Optional[str] = None) -> None: ... + GenericMessage = TypeVar("GenericMessage", bound=AlephMessage) From cd2f8e465dcdd6f57fdbbfa06411b2a025f64908 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Fri, 4 Oct 2024 10:59:02 +0900 Subject: [PATCH 06/37] Migrate to Pydantic v2, update model validation and fix async issues - Migrated to Pydantic v2: - Replaced deprecated `parse_obj()` and `parse_raw()` with `model_validate()` and `model_validate_json()`. - Replaced `.dict()` with `.model_dump()` for serializing models to dictionaries. - Updated `validator` to `field_validator` and `root_validator` to `model_validator` to comply with Pydantic v2 syntax changes. - Fixed asyncio issues: - Added `await` for asynchronous methods like `raise_for_status()` in `RemoteAccount` and other HTTP operations to avoid `RuntimeWarning`. - Updated config handling: - Used `ClassVar` for constants in `Settings` and other configuration classes. - Replaced `Config` with `ConfigDict` in Pydantic models to follow v2 conventions. - Added default values for missing fields in chain configurations (`CHAINS_SEPOLIA_ACTIVE`, etc.). - Adjusted signature handling: - Updated the signing logic to prepend `0x` in the `BaseAccount` signature generation to ensure correct Ethereum address formatting. - Minor fixes: - Resolved issue with extra fields not being allowed by default by specifying `extra="allow"` or `extra="forbid"` where necessary. - Fixed tests to account for changes in model validation and serialization behavior. - Added `pydantic-settings` as a new dependency for configuration management. --- src/aleph/sdk/conf.py | 9 ++++++--- src/aleph/sdk/utils.py | 21 +++++++++------------ tests/unit/aleph_vm_authentication.py | 6 +++--- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index 090c290b..8f158aef 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -5,10 +5,11 @@ from shutil import which from typing import ClassVar, Dict, Optional, Union +from pydantic_settings import BaseSettings + from aleph_message.models import Chain from aleph_message.models.execution.environment import HypervisorType from pydantic import BaseModel, ConfigDict, Field -from pydantic_settings import BaseSettings from aleph.sdk.types import ChainInfo @@ -141,10 +142,12 @@ class Settings(BaseSettings): DNS_PROGRAM_DOMAIN: ClassVar[str] = "program.public.aleph.sh" DNS_INSTANCE_DOMAIN: ClassVar[str] = "instance.public.aleph.sh" DNS_STATIC_DOMAIN: ClassVar[str] = "static.public.aleph.sh" - DNS_RESOLVERS: ClassVar[list[str]] = ["9.9.9.9", "1.1.1.1"] + DNS_RESOLVERS: ClassVar[str] = ["9.9.9.9", "1.1.1.1"] model_config = ConfigDict( - env_prefix="ALEPH_", case_sensitive=False, env_file=".env" + env_prefix="ALEPH_", + case_sensitive=False, + env_file=".env" ) diff --git a/src/aleph/sdk/utils.py b/src/aleph/sdk/utils.py index 9f69d2d7..f9380c1a 100644 --- a/src/aleph/sdk/utils.py +++ b/src/aleph/sdk/utils.py @@ -28,13 +28,15 @@ from uuid import UUID from zipfile import BadZipFile, ZipFile -import pydantic_core +from pydantic import BaseModel + from aleph_message.models import ItemHash, MessageType from aleph_message.models.execution.program import Encoding from aleph_message.models.execution.volume import MachineVolume from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from jwcrypto.jwa import JWA +import pydantic_core from aleph.sdk.conf import settings from aleph.sdk.types import GenericMessage, SEVInfo, SEVMeasurement @@ -180,17 +182,12 @@ def parse_volume(volume_dict: Union[Mapping, MachineVolume]) -> MachineVolume: # Python 3.9 does not support `isinstance(volume_dict, MachineVolume)`, # so we need to iterate over all types. for volume_type in get_args(MachineVolume): - if isinstance(volume_dict, volume_type): - return volume_dict - - if isinstance(volume_dict, Mapping): - for volume_type in get_args(MachineVolume): - try: - return volume_type(**volume_dict) - except (TypeError, ValueError): - continue - - raise ValueError("Invalid volume data, could not be parsed into a MachineVolume") + try: + return volume_type.model_validate(volume_dict) + except ValueError: + continue + else: + raise ValueError(f"Could not parse volume: {volume_dict}") def compute_sha256(s: str) -> str: diff --git a/tests/unit/aleph_vm_authentication.py b/tests/unit/aleph_vm_authentication.py index 4a048b50..4c435482 100644 --- a/tests/unit/aleph_vm_authentication.py +++ b/tests/unit/aleph_vm_authentication.py @@ -13,7 +13,7 @@ from eth_account.messages import encode_defunct from jwcrypto import jwk from jwcrypto.jwa import JWA -from pydantic import BaseModel, ValidationError, field_validator, model_validator +from pydantic import BaseModel, ValidationError, model_validator, field_validator from aleph.sdk.utils import bytes_from_hex @@ -74,7 +74,7 @@ def payload_must_be_hex(cls, value: bytes) -> bytes: return bytes_from_hex(value.decode()) @model_validator(mode="after") - def check_expiry(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": + def check_expiry(cls, values) -> Dict[str, bytes]: """Check that the token has not expired""" payload: bytes = values.payload content = SignedPubKeyPayload.model_validate_json(payload) @@ -86,7 +86,7 @@ def check_expiry(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": return values @model_validator(mode="after") - def check_signature(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": + def check_signature(cls, values: Dict[str, bytes]) -> Dict[str, bytes]: """Check that the signature is valid""" signature: bytes = values.signature payload: bytes = values.payload From a86da5fcf38638682063d661406cba2d476ee40a Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Tue, 15 Oct 2024 10:14:50 +0900 Subject: [PATCH 07/37] fix: add explicit float type for HTTP_REQUEST_TIMEOUT to comply with Pydantic v2 requirements Pydantic v2 requires explicit type annotations for fields, so added `float` to ensure proper validation of HTTP_REQUEST_TIMEOUT. --- src/aleph/sdk/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index 8f158aef..56686c42 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -43,7 +43,7 @@ class Settings(BaseSettings): REMOTE_CRYPTO_HOST: Optional[str] = None REMOTE_CRYPTO_UNIX_SOCKET: Optional[str] = None ADDRESS_TO_USE: Optional[str] = None - HTTP_REQUEST_TIMEOUT = 15.0 + HTTP_REQUEST_TIMEOUT: float = 15.0 DEFAULT_CHANNEL: str = "ALEPH-CLOUDSOLUTIONS" DEFAULT_RUNTIME_ID: str = ( From fa8e7c84f85dc1ab2c40b4849669f57cfad3b198 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Tue, 15 Oct 2024 12:02:00 +0900 Subject: [PATCH 08/37] Fix: Linting tests did not pass: --- src/aleph/sdk/conf.py | 9 +++------ src/aleph/sdk/utils.py | 4 +--- tests/unit/aleph_vm_authentication.py | 7 +++---- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index 56686c42..b81668f0 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -5,11 +5,10 @@ from shutil import which from typing import ClassVar, Dict, Optional, Union -from pydantic_settings import BaseSettings - from aleph_message.models import Chain from aleph_message.models.execution.environment import HypervisorType from pydantic import BaseModel, ConfigDict, Field +from pydantic_settings import BaseSettings from aleph.sdk.types import ChainInfo @@ -142,12 +141,10 @@ class Settings(BaseSettings): DNS_PROGRAM_DOMAIN: ClassVar[str] = "program.public.aleph.sh" DNS_INSTANCE_DOMAIN: ClassVar[str] = "instance.public.aleph.sh" DNS_STATIC_DOMAIN: ClassVar[str] = "static.public.aleph.sh" - DNS_RESOLVERS: ClassVar[str] = ["9.9.9.9", "1.1.1.1"] + DNS_RESOLVERS: ClassVar[list[str]] = ["9.9.9.9", "1.1.1.1"] model_config = ConfigDict( - env_prefix="ALEPH_", - case_sensitive=False, - env_file=".env" + env_prefix="ALEPH_", case_sensitive=False, env_file=".env" ) diff --git a/src/aleph/sdk/utils.py b/src/aleph/sdk/utils.py index f9380c1a..f9ec05c5 100644 --- a/src/aleph/sdk/utils.py +++ b/src/aleph/sdk/utils.py @@ -28,15 +28,13 @@ from uuid import UUID from zipfile import BadZipFile, ZipFile -from pydantic import BaseModel - +import pydantic_core from aleph_message.models import ItemHash, MessageType from aleph_message.models.execution.program import Encoding from aleph_message.models.execution.volume import MachineVolume from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from jwcrypto.jwa import JWA -import pydantic_core from aleph.sdk.conf import settings from aleph.sdk.types import GenericMessage, SEVInfo, SEVMeasurement diff --git a/tests/unit/aleph_vm_authentication.py b/tests/unit/aleph_vm_authentication.py index 4c435482..a562a96d 100644 --- a/tests/unit/aleph_vm_authentication.py +++ b/tests/unit/aleph_vm_authentication.py @@ -13,7 +13,7 @@ from eth_account.messages import encode_defunct from jwcrypto import jwk from jwcrypto.jwa import JWA -from pydantic import BaseModel, ValidationError, model_validator, field_validator +from pydantic import BaseModel, ValidationError, field_validator, model_validator from aleph.sdk.utils import bytes_from_hex @@ -74,7 +74,7 @@ def payload_must_be_hex(cls, value: bytes) -> bytes: return bytes_from_hex(value.decode()) @model_validator(mode="after") - def check_expiry(cls, values) -> Dict[str, bytes]: + def check_expiry(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": """Check that the token has not expired""" payload: bytes = values.payload content = SignedPubKeyPayload.model_validate_json(payload) @@ -86,8 +86,7 @@ def check_expiry(cls, values) -> Dict[str, bytes]: return values @model_validator(mode="after") - def check_signature(cls, values: Dict[str, bytes]) -> Dict[str, bytes]: - """Check that the signature is valid""" + def check_signature(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": signature: bytes = values.signature payload: bytes = values.payload content = SignedPubKeyPayload.model_validate_json(payload) From 213982bd17cfc032e2e10e3dc15451bc0b62eaef Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Mon, 21 Oct 2024 23:55:12 +0900 Subject: [PATCH 09/37] Fix: Project don't use the good version of aleph-message There were changes made on aleph-message on the main branch about pydantic version. Using the version by the url and then change it later after the release. --- pyproject.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 409694cf..165e1779 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,15 +30,16 @@ dynamic = [ "version" ] dependencies = [ "aiohttp>=3.8.3", "aioresponses>=0.7.6", - "aleph-message>=0.4.9", + "aleph-message @ git+https://github.com/aleph-im/aleph-message#egg=main", "aleph-superfluid>=0.2.1", - "base58==2.1.1", # Needed now as default with _load_account changement + "base58==2.1.1", # Needed now as default with _load_account changement "coincurve; python_version<'3.11'", "coincurve>=19; python_version>='3.11'", "eth-abi>=4; python_version>='3.11'", "eth-typing==4.3.1", "jwcrypto==1.5.6", - "pynacl==1.5", # Needed now as default with _load_account changement + "pydantic-settings>=2", + "pynacl==1.5", # Needed now as default with _load_account changement "python-magic", "typing-extensions", "web3==6.3", From 8894347629d72d16245cde5aa72e03fa2066e862 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Tue, 5 Nov 2024 23:00:54 +0900 Subject: [PATCH 10/37] fix: Wrong aleph-message version --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 165e1779..b313cac7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,16 +30,16 @@ dynamic = [ "version" ] dependencies = [ "aiohttp>=3.8.3", "aioresponses>=0.7.6", - "aleph-message @ git+https://github.com/aleph-im/aleph-message#egg=main", + "aleph-message @ git+https://github.com/aleph-im/aleph-message@108-upgrade-pydantic-version#egg=aleph-message", "aleph-superfluid>=0.2.1", - "base58==2.1.1", # Needed now as default with _load_account changement + "base58==2.1.1", # Needed now as default with _load_account changement "coincurve; python_version<'3.11'", "coincurve>=19; python_version>='3.11'", "eth-abi>=4; python_version>='3.11'", "eth-typing==4.3.1", "jwcrypto==1.5.6", "pydantic-settings>=2", - "pynacl==1.5", # Needed now as default with _load_account changement + "pynacl==1.5", # Needed now as default with _load_account changement "python-magic", "typing-extensions", "web3==6.3", From 56743560a2e8f6db91d873771d66d5cf75cdfdc6 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Tue, 5 Nov 2024 23:18:34 +0900 Subject: [PATCH 11/37] Fix: list[str] rise an error in ubuntu 20.04 Using List from typing instead to assure the compatibility between python3.8 and above --- src/aleph/sdk/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index b81668f0..36818cb5 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -3,7 +3,7 @@ import os from pathlib import Path from shutil import which -from typing import ClassVar, Dict, Optional, Union +from typing import ClassVar, Dict, Optional, Union, List from aleph_message.models import Chain from aleph_message.models.execution.environment import HypervisorType @@ -141,7 +141,7 @@ class Settings(BaseSettings): DNS_PROGRAM_DOMAIN: ClassVar[str] = "program.public.aleph.sh" DNS_INSTANCE_DOMAIN: ClassVar[str] = "instance.public.aleph.sh" DNS_STATIC_DOMAIN: ClassVar[str] = "static.public.aleph.sh" - DNS_RESOLVERS: ClassVar[list[str]] = ["9.9.9.9", "1.1.1.1"] + DNS_RESOLVERS: ClassVar[List[str]] = ["9.9.9.9", "1.1.1.1"] model_config = ConfigDict( env_prefix="ALEPH_", case_sensitive=False, env_file=".env" From 00177ae32327a126752cc8b91dcb1efd537e9fa7 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Tue, 5 Nov 2024 23:21:59 +0900 Subject: [PATCH 12/37] style: isort --- src/aleph/sdk/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index 36818cb5..bb40b3d8 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -3,7 +3,7 @@ import os from pathlib import Path from shutil import which -from typing import ClassVar, Dict, Optional, Union, List +from typing import ClassVar, Dict, List, Optional, Union from aleph_message.models import Chain from aleph_message.models.execution.environment import HypervisorType From afe055d4e960d7a1f510facf826174df98ca7618 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Fri, 29 Nov 2024 22:08:27 +0900 Subject: [PATCH 13/37] fix: Hugo comments --- src/aleph/sdk/client/http.py | 3 --- tests/unit/aleph_vm_authentication.py | 6 ++++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/aleph/sdk/client/http.py b/src/aleph/sdk/client/http.py index a59531c6..60e65bb2 100644 --- a/src/aleph/sdk/client/http.py +++ b/src/aleph/sdk/client/http.py @@ -467,6 +467,3 @@ async def get_message_status(self, item_hash: str) -> MessageStatus: if resp.status == HTTPNotFound.status_code: raise MessageNotFoundError(f"No such hash {item_hash}") resp.raise_for_status() - - data = await resp.json() - return MessageStatus(**data) diff --git a/tests/unit/aleph_vm_authentication.py b/tests/unit/aleph_vm_authentication.py index a562a96d..ad796903 100644 --- a/tests/unit/aleph_vm_authentication.py +++ b/tests/unit/aleph_vm_authentication.py @@ -1,4 +1,6 @@ # Keep datetime import as is as it allow patching in test +from __future__ import annotations + import datetime import functools import json @@ -74,7 +76,7 @@ def payload_must_be_hex(cls, value: bytes) -> bytes: return bytes_from_hex(value.decode()) @model_validator(mode="after") - def check_expiry(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": + def check_expiry(cls, values: SignedPubKeyHeader) -> SignedPubKeyHeader: """Check that the token has not expired""" payload: bytes = values.payload content = SignedPubKeyPayload.model_validate_json(payload) @@ -86,7 +88,7 @@ def check_expiry(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": return values @model_validator(mode="after") - def check_signature(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": + def check_signature(cls, values: SignedPubKeyHeader) -> SignedPubKeyHeader: signature: bytes = values.signature payload: bytes = values.payload content = SignedPubKeyPayload.model_validate_json(payload) From 3e88f28cdebf0f2c3a173653ab0a803174693ab2 Mon Sep 17 00:00:00 2001 From: philogicae <38438271+philogicae@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:09:46 +0200 Subject: [PATCH 14/37] Add pydantic for better mypy tests + Fixes --- pyproject.toml | 1 + src/aleph/sdk/conf.py | 8 ++++---- src/aleph/sdk/domain.py | 4 ++-- src/aleph/sdk/vm/cache.py | 2 +- tests/unit/aleph_vm_authentication.py | 6 +++--- tests/unit/test_price.py | 2 +- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b313cac7..dc0873d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -155,6 +155,7 @@ dependencies = [ "ruff==0.4.8", "isort==5.13.2", "pyproject-fmt==2.2.1", + "pydantic-settings>=2", ] [tool.hatch.envs.linting.scripts] typing = "mypy --config-file=pyproject.toml {args:} ./src/ ./tests/ ./examples/" diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index 5035e29c..5a8fcc6c 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -7,8 +7,8 @@ from aleph_message.models import Chain from aleph_message.models.execution.environment import HypervisorType -from pydantic import BaseModel, ConfigDict, Field -from pydantic_settings import BaseSettings +from pydantic import BaseModel, Field +from pydantic_settings import BaseSettings, SettingsConfigDict from aleph.sdk.types import ChainInfo @@ -224,7 +224,7 @@ class Settings(BaseSettings): DNS_STATIC_DOMAIN: ClassVar[str] = "static.public.aleph.sh" DNS_RESOLVERS: ClassVar[List[str]] = ["9.9.9.9", "1.1.1.1"] - model_config = ConfigDict( + model_config = SettingsConfigDict( env_prefix="ALEPH_", case_sensitive=False, env_file=".env" ) @@ -237,7 +237,7 @@ class MainConfiguration(BaseModel): path: Path chain: Chain - model_config = ConfigDict(use_enum_values=True) + model_config = SettingsConfigDict(use_enum_values=True) # Settings singleton diff --git a/src/aleph/sdk/domain.py b/src/aleph/sdk/domain.py index a8f3fd82..525e6cef 100644 --- a/src/aleph/sdk/domain.py +++ b/src/aleph/sdk/domain.py @@ -52,11 +52,11 @@ def raise_error(self, status: Dict[str, bool]): def hostname_from_url(url: Union[HttpUrl, str]) -> Hostname: """Extract FQDN from url""" - parsed = urlparse(url) + parsed = urlparse(str(url)) if all([parsed.scheme, parsed.netloc]) is True: url = parsed.netloc - return Hostname(url) + return Hostname(str(url)) async def get_target_type(fqdn: Hostname) -> Optional[TargetType]: diff --git a/src/aleph/sdk/vm/cache.py b/src/aleph/sdk/vm/cache.py index ff5ca7c8..a7ac6acc 100644 --- a/src/aleph/sdk/vm/cache.py +++ b/src/aleph/sdk/vm/cache.py @@ -70,7 +70,7 @@ def __init__( ) self.cache = {} - self.api_host = connector_url if connector_url else settings.API_HOST + self.api_host = str(connector_url) if connector_url else settings.API_HOST async def get(self, key: str) -> Optional[bytes]: sanitized_key = sanitize_cache_key(key) diff --git a/tests/unit/aleph_vm_authentication.py b/tests/unit/aleph_vm_authentication.py index ad796903..c1710c16 100644 --- a/tests/unit/aleph_vm_authentication.py +++ b/tests/unit/aleph_vm_authentication.py @@ -75,7 +75,7 @@ def payload_must_be_hex(cls, value: bytes) -> bytes: """Convert the payload from hexadecimal to bytes""" return bytes_from_hex(value.decode()) - @model_validator(mode="after") + @model_validator(mode="after") # type: ignore def check_expiry(cls, values: SignedPubKeyHeader) -> SignedPubKeyHeader: """Check that the token has not expired""" payload: bytes = values.payload @@ -87,7 +87,7 @@ def check_expiry(cls, values: SignedPubKeyHeader) -> SignedPubKeyHeader: return values - @model_validator(mode="after") + @model_validator(mode="after") # type: ignore def check_signature(cls, values: SignedPubKeyHeader) -> SignedPubKeyHeader: signature: bytes = values.signature payload: bytes = values.payload @@ -262,7 +262,7 @@ async def authenticate_websocket_message( signed_operation = SignedOperation.model_validate(message["X-SignedOperation"]) if signed_operation.content.domain != domain_name: logger.debug( - f"Invalid domain '{signed_pubkey.content.domain}' != '{domain_name}'" + f"Invalid domain '{signed_operation.content.domain}' != '{domain_name}'" ) raise web.HTTPUnauthorized(reason="Invalid domain") return verify_signed_operation(signed_operation, signed_pubkey) diff --git a/tests/unit/test_price.py b/tests/unit/test_price.py index bed9304a..b596c33b 100644 --- a/tests/unit/test_price.py +++ b/tests/unit/test_price.py @@ -18,7 +18,7 @@ async def test_get_program_price_valid(): mock_session = make_mock_get_session(expected_response) async with mock_session: response = await mock_session.get_program_price("cacacacacacaca") - assert response == PriceResponse(**expected_response) + assert response == PriceResponse(**expected_response) # type: ignore @pytest.mark.asyncio From c7d3a88f96782b405dfbd4bfc5dd4832881574a5 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Wed, 15 Jan 2025 11:24:32 +0900 Subject: [PATCH 15/37] fix: Changing version of aleph-message --- pyproject.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 635d3aa1..95ed9d4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,16 +30,16 @@ dynamic = [ "version" ] dependencies = [ "aiohttp>=3.8.3", "aioresponses>=0.7.6", - "aleph-message>=0.6", + "aleph-message @ git+https://github.com/aleph-im/aleph-message@108-upgrade-pydantic-version#egg=aleph-message", "aleph-superfluid>=0.2.1", - "base58==2.1.1", # Needed now as default with _load_account changement + "base58==2.1.1", # Needed now as default with _load_account changement "coincurve; python_version<'3.11'", "coincurve>=19; python_version>='3.11'", "eth-abi>=4; python_version>='3.11'", "eth-typing==4.3.1", "jwcrypto==1.5.6", "pydantic-settings>=2", - "pynacl==1.5", # Needed now as default with _load_account changement + "pynacl==1.5", # Needed now as default with _load_account changement "python-magic", "typing-extensions", "web3==6.3", @@ -134,6 +134,7 @@ dependencies = [ "httpx", "secp256k1", ] + [tool.hatch.envs.testing.scripts] test = "pytest {args:} ./src/ ./tests/ ./examples/" test-cov = "pytest --cov {args:} ./src/ ./tests/ ./examples/" From f9bdd3c2b98984150296dcf0fc11e563a4f31b27 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Wed, 15 Jan 2025 11:27:02 +0900 Subject: [PATCH 16/37] style: Missing type for URL --- src/aleph/sdk/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index b6154662..16ddc4a1 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -84,9 +84,9 @@ class Settings(BaseSettings): CODE_USES_SQUASHFS: bool = which("mksquashfs") is not None # True if command exists - VM_URL_PATH = "https://aleph.sh/vm/{hash}" - VM_URL_HOST = "https://{hash_base32}.aleph.sh" - IPFS_GATEWAY = "https://ipfs.aleph.cloud/ipfs/" + VM_URL_PATH: ClassVar[str] = "https://aleph.sh/vm/{hash}" + VM_URL_HOST: ClassVar[str] = "https://{hash_base32}.aleph.sh" + IPFS_GATEWAY: ClassVar[str] = "https://ipfs.aleph.cloud/ipfs/" # Web3Provider settings TOKEN_DECIMALS: ClassVar[int] = 18 From d1e4e518a8fc81027be2a4101bb3fd7aa4a82944 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Wed, 15 Jan 2025 11:27:22 +0900 Subject: [PATCH 17/37] style: Missing type for URL --- pyproject.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 95ed9d4c..f97fd0a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,14 +32,14 @@ dependencies = [ "aioresponses>=0.7.6", "aleph-message @ git+https://github.com/aleph-im/aleph-message@108-upgrade-pydantic-version#egg=aleph-message", "aleph-superfluid>=0.2.1", - "base58==2.1.1", # Needed now as default with _load_account changement + "base58==2.1.1", # Needed now as default with _load_account changement "coincurve; python_version<'3.11'", "coincurve>=19; python_version>='3.11'", "eth-abi>=4; python_version>='3.11'", "eth-typing==4.3.1", "jwcrypto==1.5.6", "pydantic-settings>=2", - "pynacl==1.5", # Needed now as default with _load_account changement + "pynacl==1.5", # Needed now as default with _load_account changement "python-magic", "typing-extensions", "web3==6.3", @@ -134,7 +134,6 @@ dependencies = [ "httpx", "secp256k1", ] - [tool.hatch.envs.testing.scripts] test = "pytest {args:} ./src/ ./tests/ ./examples/" test-cov = "pytest --cov {args:} ./src/ ./tests/ ./examples/" From 2c68cd347e298302b02eb46a43fc28e06bfae470 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Tue, 21 Jan 2025 20:11:24 +0900 Subject: [PATCH 18/37] fix: Changing version of aleph-message and fix mypy Changing the version from the branch to the main of aleph-message mypy rose some errors about missing name argument, so setting the as None because they are optional --- pyproject.toml | 6 +++--- src/aleph/sdk/types.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f97fd0a1..f7911982 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,16 +30,16 @@ dynamic = [ "version" ] dependencies = [ "aiohttp>=3.8.3", "aioresponses>=0.7.6", - "aleph-message @ git+https://github.com/aleph-im/aleph-message@108-upgrade-pydantic-version#egg=aleph-message", + "aleph-message>=0.6", "aleph-superfluid>=0.2.1", - "base58==2.1.1", # Needed now as default with _load_account changement + "base58==2.1.1", # Needed now as default with _load_account changement "coincurve; python_version<'3.11'", "coincurve>=19; python_version>='3.11'", "eth-abi>=4; python_version>='3.11'", "eth-typing==4.3.1", "jwcrypto==1.5.6", "pydantic-settings>=2", - "pynacl==1.5", # Needed now as default with _load_account changement + "pynacl==1.5", # Needed now as default with _load_account changement "python-magic", "typing-extensions", "web3==6.3", diff --git a/src/aleph/sdk/types.py b/src/aleph/sdk/types.py index c698da5d..e192335f 100644 --- a/src/aleph/sdk/types.py +++ b/src/aleph/sdk/types.py @@ -2,7 +2,7 @@ from enum import Enum from typing import Dict, Optional, Protocol, TypeVar -from pydantic import BaseModel +from pydantic import BaseModel, Field __all__ = ("StorageEnum", "Account", "AccountFromPrivateKey", "GenericMessage") @@ -83,7 +83,7 @@ class ChainInfo(BaseModel): class StoredContent(BaseModel): - filename: Optional[str] - hash: Optional[str] - url: Optional[str] - error: Optional[str] + filename: Optional[str] = Field(default=None) + hash: Optional[str] = Field(default=None) + url: Optional[str] = Field(default=None) + error: Optional[str] = Field(default=None) From ac1b48ffd92bde39a3976a6137220c3f9374152f Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Tue, 21 Jan 2025 22:13:24 +0900 Subject: [PATCH 19/37] fix: Changing version of aleph-message --- pyproject.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f7911982..dc0873d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,16 +30,16 @@ dynamic = [ "version" ] dependencies = [ "aiohttp>=3.8.3", "aioresponses>=0.7.6", - "aleph-message>=0.6", + "aleph-message @ git+https://github.com/aleph-im/aleph-message@108-upgrade-pydantic-version#egg=aleph-message", "aleph-superfluid>=0.2.1", - "base58==2.1.1", # Needed now as default with _load_account changement + "base58==2.1.1", # Needed now as default with _load_account changement "coincurve; python_version<'3.11'", "coincurve>=19; python_version>='3.11'", "eth-abi>=4; python_version>='3.11'", "eth-typing==4.3.1", "jwcrypto==1.5.6", "pydantic-settings>=2", - "pynacl==1.5", # Needed now as default with _load_account changement + "pynacl==1.5", # Needed now as default with _load_account changement "python-magic", "typing-extensions", "web3==6.3", @@ -62,7 +62,7 @@ optional-dependencies.encryption = [ "eciespy>=0.3.13; python_version>='3.11'", ] optional-dependencies.ledger = [ - "ledgereth==0.10", + "ledgereth==0.9.1", ] optional-dependencies.mqtt = [ "aiomqtt<=0.1.3", @@ -81,7 +81,7 @@ optional-dependencies.substrate = [ "substrate-interface", ] optional-dependencies.tezos = [ - "aleph-pytezos==3.13.4", + "aleph-pytezos==0.1.1", "pynacl", ] urls.Documentation = "https://aleph.im/" From 5ce940ef250cf5cb8ed367b045e1216e1d8e1ec5 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Tue, 21 Jan 2025 22:25:11 +0900 Subject: [PATCH 20/37] fix: Changing version of pytezos --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dc0873d8..bc28fb15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,7 @@ optional-dependencies.substrate = [ "substrate-interface", ] optional-dependencies.tezos = [ - "aleph-pytezos==0.1.1", + "aleph-pytezos==3.13.4", "pynacl", ] urls.Documentation = "https://aleph.im/" From 7c942c7002742c4c0190c62a90cd0ad574923e6e Mon Sep 17 00:00:00 2001 From: philogicae Date: Tue, 18 Feb 2025 19:05:00 +0200 Subject: [PATCH 21/37] Changes for new pricing system (#199) - Move/improve flow code parts from CLI to SDK - Add utils functions - Add `make_instance_content` and `make_program_content` - Refactor `create_instance` and `create_program` - Add `get_estimated_price` - Fixes for mypy/ruff/pytest - Minor improvements - Remove firecracker rootfs hashes for instances --- src/aleph/sdk/chains/ethereum.py | 26 ++- src/aleph/sdk/chains/evm.py | 9 + src/aleph/sdk/client/abstract.py | 80 ++++--- src/aleph/sdk/client/authenticated_http.py | 239 +++++++++------------ src/aleph/sdk/client/http.py | 77 ++++++- src/aleph/sdk/conf.py | 20 +- src/aleph/sdk/connectors/superfluid.py | 65 +++++- src/aleph/sdk/evm_utils.py | 23 +- src/aleph/sdk/exceptions.py | 11 +- src/aleph/sdk/types.py | 13 ++ src/aleph/sdk/utils.py | 213 +++++++++++++++++- tests/unit/test_asynchronous.py | 10 +- tests/unit/test_price.py | 10 +- tests/unit/test_superfluid.py | 13 ++ tests/unit/test_utils.py | 10 +- 15 files changed, 588 insertions(+), 231 deletions(-) diff --git a/src/aleph/sdk/chains/ethereum.py b/src/aleph/sdk/chains/ethereum.py index ab93df56..c185d174 100644 --- a/src/aleph/sdk/chains/ethereum.py +++ b/src/aleph/sdk/chains/ethereum.py @@ -15,6 +15,7 @@ from web3.types import TxParams, TxReceipt from aleph.sdk.exceptions import InsufficientFundsError +from aleph.sdk.types import TokenType from ..conf import settings from ..connectors.superfluid import Superfluid @@ -22,12 +23,13 @@ BALANCEOF_ABI, MIN_ETH_BALANCE, MIN_ETH_BALANCE_WEI, + FlowUpdate, + from_wei_token, get_chain_id, get_chains_with_super_token, get_rpc, get_super_token_address, get_token_address, - to_human_readable_token, ) from ..exceptions import BadSignatureError from ..utils import bytes_from_hex @@ -106,8 +108,9 @@ def can_transact(self, block=True) -> bool: valid = balance > MIN_ETH_BALANCE_WEI if self.chain else False if not valid and block: raise InsufficientFundsError( + token_type=TokenType.GAS, required_funds=MIN_ETH_BALANCE, - available_funds=to_human_readable_token(balance), + available_funds=float(from_wei_token(balance)), ) return valid @@ -162,6 +165,12 @@ def get_super_token_balance(self) -> Decimal: return Decimal(contract.functions.balanceOf(self.get_address()).call()) return Decimal(0) + def can_start_flow(self, flow: Decimal) -> bool: + """Check if the account has enough funds to start a Superfluid flow of the given size.""" + if not self.superfluid_connector: + raise ValueError("Superfluid connector is required to check a flow") + return self.superfluid_connector.can_start_flow(flow) + def create_flow(self, receiver: str, flow: Decimal) -> Awaitable[str]: """Creat a Superfluid flow between this account and the receiver address.""" if not self.superfluid_connector: @@ -188,6 +197,19 @@ def delete_flow(self, receiver: str) -> Awaitable[str]: raise ValueError("Superfluid connector is required to delete a flow") return self.superfluid_connector.delete_flow(receiver=receiver) + def manage_flow( + self, + receiver: str, + flow: Decimal, + update_type: FlowUpdate, + ) -> Awaitable[Optional[str]]: + """Manage the Superfluid flow between this account and the receiver address.""" + if not self.superfluid_connector: + raise ValueError("Superfluid connector is required to manage a flow") + return self.superfluid_connector.manage_flow( + receiver=receiver, flow=flow, update_type=update_type + ) + def get_fallback_account( path: Optional[Path] = None, chain: Optional[Chain] = None diff --git a/src/aleph/sdk/chains/evm.py b/src/aleph/sdk/chains/evm.py index 5bf66ef1..a5eeed84 100644 --- a/src/aleph/sdk/chains/evm.py +++ b/src/aleph/sdk/chains/evm.py @@ -5,6 +5,7 @@ from aleph_message.models import Chain from eth_account import Account # type: ignore +from ..evm_utils import FlowUpdate from .common import get_fallback_private_key from .ethereum import ETHAccount @@ -29,6 +30,9 @@ def get_token_balance(self) -> Decimal: def get_super_token_balance(self) -> Decimal: raise ValueError(f"Super token not implemented for this chain {self.CHAIN}") + def can_start_flow(self, flow: Decimal) -> bool: + raise ValueError(f"Flow checking not implemented for this chain {self.CHAIN}") + def create_flow(self, receiver: str, flow: Decimal) -> Awaitable[str]: raise ValueError(f"Flow creation not implemented for this chain {self.CHAIN}") @@ -41,6 +45,11 @@ def update_flow(self, receiver: str, flow: Decimal) -> Awaitable[str]: def delete_flow(self, receiver: str) -> Awaitable[str]: raise ValueError(f"Flow deletion not implemented for this chain {self.CHAIN}") + def manage_flow( + self, receiver: str, flow: Decimal, update_type: FlowUpdate + ) -> Awaitable[Optional[str]]: + raise ValueError(f"Flow management not implemented for this chain {self.CHAIN}") + def get_fallback_account( path: Optional[Path] = None, chain: Optional[Chain] = None diff --git a/src/aleph/sdk/client/abstract.py b/src/aleph/sdk/client/abstract.py index 025aae6a..7f9fed8e 100644 --- a/src/aleph/sdk/client/abstract.py +++ b/src/aleph/sdk/client/abstract.py @@ -20,9 +20,9 @@ from aleph_message.models import ( AlephMessage, + ExecutableContent, ItemHash, ItemType, - MessagesResponse, MessageType, Payment, PostMessage, @@ -41,7 +41,7 @@ from aleph.sdk.utils import extended_json_encoder from ..query.filters import MessageFilter, PostFilter -from ..query.responses import PostsResponse, PriceResponse +from ..query.responses import MessagesResponse, PostsResponse, PriceResponse from ..types import GenericMessage, StorageEnum from ..utils import Writable, compute_sha256 @@ -110,7 +110,7 @@ async def get_posts_iterator( ) page += 1 for post in resp.posts: - yield post + yield post # type: ignore @abstractmethod async def download_file(self, file_hash: str) -> bytes: @@ -242,6 +242,18 @@ def watch_messages( """ raise NotImplementedError("Did you mean to import `AlephHttpClient`?") + @abstractmethod + def get_estimated_price( + self, + content: ExecutableContent, + ) -> Coroutine[Any, Any, PriceResponse]: + """ + Get Instance/Program content estimated price + + :param content: Instance or Program content + """ + raise NotImplementedError("Did you mean to import `AlephHttpClient`?") + @abstractmethod def get_program_price( self, @@ -265,7 +277,7 @@ async def create_post( post_type: str, ref: Optional[str] = None, address: Optional[str] = None, - channel: Optional[str] = None, + channel: Optional[str] = settings.DEFAULT_CHANNEL, inline: bool = True, storage_engine: StorageEnum = StorageEnum.storage, sync: bool = False, @@ -290,9 +302,9 @@ async def create_post( async def create_aggregate( self, key: str, - content: Mapping[str, Any], + content: dict[str, Any], address: Optional[str] = None, - channel: Optional[str] = None, + channel: Optional[str] = settings.DEFAULT_CHANNEL, inline: bool = True, sync: bool = False, ) -> Tuple[AlephMessage, MessageStatus]: @@ -302,7 +314,7 @@ async def create_aggregate( :param key: Key to use to store the content :param content: Content to store :param address: Address to use to sign the message - :param channel: Channel to use (Default: "TEST") + :param channel: Channel to use (Default: "ALEPH-CLOUDSOLUTIONS") :param inline: Whether to write content inside the message (Default: True) :param sync: If true, waits for the message to be processed by the API server (Default: False) """ @@ -321,7 +333,7 @@ async def create_store( ref: Optional[str] = None, storage_engine: StorageEnum = StorageEnum.storage, extra_fields: Optional[dict] = None, - channel: Optional[str] = None, + channel: Optional[str] = settings.DEFAULT_CHANNEL, sync: bool = False, ) -> Tuple[AlephMessage, MessageStatus]: """ @@ -350,22 +362,22 @@ async def create_program( program_ref: str, entrypoint: str, runtime: str, - environment_variables: Optional[Mapping[str, str]] = None, - storage_engine: StorageEnum = StorageEnum.storage, - channel: Optional[str] = None, + metadata: Optional[dict[str, Any]] = None, address: Optional[str] = None, - sync: bool = False, - memory: Optional[int] = None, vcpus: Optional[int] = None, + memory: Optional[int] = None, timeout_seconds: Optional[float] = None, - persistent: bool = False, - allow_amend: bool = False, internet: bool = True, + allow_amend: bool = False, aleph_api: bool = True, encoding: Encoding = Encoding.zip, + persistent: bool = False, volumes: Optional[List[Mapping]] = None, - subscriptions: Optional[List[Mapping]] = None, - metadata: Optional[Mapping[str, Any]] = None, + environment_variables: Optional[dict[str, str]] = None, + subscriptions: Optional[List[dict]] = None, + sync: bool = False, + channel: Optional[str] = settings.DEFAULT_CHANNEL, + storage_engine: StorageEnum = StorageEnum.storage, ) -> Tuple[AlephMessage, MessageStatus]: """ Post a (create) PROGRAM message. @@ -373,22 +385,22 @@ async def create_program( :param program_ref: Reference to the program to run :param entrypoint: Entrypoint to run :param runtime: Runtime to use - :param environment_variables: Environment variables to pass to the program - :param storage_engine: Storage engine to use (Default: "storage") - :param channel: Channel to use (Default: "TEST") + :param metadata: Metadata to attach to the message :param address: Address to use (Default: account.get_address()) - :param sync: If true, waits for the message to be processed by the API server - :param memory: Memory in MB for the VM to be allocated (Default: 128) :param vcpus: Number of vCPUs to allocate (Default: 1) + :param memory: Memory in MB for the VM to be allocated (Default: 128) :param timeout_seconds: Timeout in seconds (Default: 30.0) - :param persistent: Whether the program should be persistent or not (Default: False) - :param allow_amend: Whether the deployed VM image may be changed (Default: False) :param internet: Whether the VM should have internet connectivity. (Default: True) + :param allow_amend: Whether the deployed VM image may be changed (Default: False) :param aleph_api: Whether the VM needs access to Aleph messages API (Default: True) :param encoding: Encoding to use (Default: Encoding.zip) + :param persistent: Whether the program should be persistent or not (Default: False) :param volumes: Volumes to mount + :param environment_variables: Environment variables to pass to the program :param subscriptions: Patterns of aleph.im messages to forward to the program's event receiver - :param metadata: Metadata to attach to the message + :param sync: If true, waits for the message to be processed by the API server + :param channel: Channel to use (Default: "ALEPH-CLOUDSOLUTIONS") + :param storage_engine: Storage engine to use (Default: "storage") """ raise NotImplementedError( "Did you mean to import `AuthenticatedAlephHttpClient`?" @@ -400,9 +412,9 @@ async def create_instance( rootfs: str, rootfs_size: int, payment: Optional[Payment] = None, - environment_variables: Optional[Mapping[str, str]] = None, + environment_variables: Optional[dict[str, str]] = None, storage_engine: StorageEnum = StorageEnum.storage, - channel: Optional[str] = None, + channel: Optional[str] = settings.DEFAULT_CHANNEL, address: Optional[str] = None, sync: bool = False, memory: Optional[int] = None, @@ -416,7 +428,7 @@ async def create_instance( volumes: Optional[List[Mapping]] = None, volume_persistence: str = "host", ssh_keys: Optional[List[str]] = None, - metadata: Optional[Mapping[str, Any]] = None, + metadata: Optional[dict[str, Any]] = None, requirements: Optional[HostRequirements] = None, ) -> Tuple[AlephMessage, MessageStatus]: """ @@ -427,7 +439,7 @@ async def create_instance( :param payment: Payment method used to pay for the instance :param environment_variables: Environment variables to pass to the program :param storage_engine: Storage engine to use (Default: "storage") - :param channel: Channel to use (Default: "TEST") + :param channel: Channel to use (Default: "ALEPH-CLOUDSOLUTIONS") :param address: Address to use (Default: account.get_address()) :param sync: If true, waits for the message to be processed by the API server :param memory: Memory in MB for the VM to be allocated (Default: 2048) @@ -455,7 +467,7 @@ async def forget( hashes: List[ItemHash], reason: Optional[str], storage_engine: StorageEnum = StorageEnum.storage, - channel: Optional[str] = None, + channel: Optional[str] = settings.DEFAULT_CHANNEL, address: Optional[str] = None, sync: bool = False, ) -> Tuple[AlephMessage, MessageStatus]: @@ -468,7 +480,7 @@ async def forget( :param hashes: Hashes of the messages to forget :param reason: Reason for forgetting the messages :param storage_engine: Storage engine to use (Default: "storage") - :param channel: Channel to use (Default: "TEST") + :param channel: Channel to use (Default: "ALEPH-CLOUDSOLUTIONS") :param address: Address to use (Default: account.get_address()) :param sync: If true, waits for the message to be processed by the API server (Default: False) """ @@ -490,7 +502,7 @@ async def generate_signed_message( :param message_type: Type of the message (PostMessage, ...) :param content: User-defined content of the message - :param channel: Channel to use (Default: "TEST") + :param channel: Channel to use (Default: "ALEPH-CLOUDSOLUTIONS") :param allow_inlining: Whether to allow inlining the content of the message (Default: True) :param storage_engine: Storage engine to use (Default: "storage") """ @@ -537,7 +549,7 @@ async def submit( self, content: Dict[str, Any], message_type: MessageType, - channel: Optional[str] = None, + channel: Optional[str] = settings.DEFAULT_CHANNEL, storage_engine: StorageEnum = StorageEnum.storage, allow_inlining: bool = True, sync: bool = False, @@ -549,7 +561,7 @@ async def submit( :param content: Content of the message :param message_type: Type of the message - :param channel: Channel to use (Default: "TEST") + :param channel: Channel to use (Default: "ALEPH-CLOUDSOLUTIONS") :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) diff --git a/src/aleph/sdk/client/authenticated_http.py b/src/aleph/sdk/client/authenticated_http.py index ad15ca09..f544a9c6 100644 --- a/src/aleph/sdk/client/authenticated_http.py +++ b/src/aleph/sdk/client/authenticated_http.py @@ -5,45 +5,37 @@ import time from io import BytesIO from pathlib import Path -from typing import Any, Dict, List, Mapping, NoReturn, Optional, Tuple, Union +from typing import Any, Dict, Mapping, NoReturn, Optional, Tuple, Union import aiohttp from aleph_message.models import ( AggregateContent, AggregateMessage, AlephMessage, - Chain, ForgetContent, ForgetMessage, - InstanceContent, InstanceMessage, ItemHash, + ItemType, MessageType, PostContent, PostMessage, - ProgramContent, ProgramMessage, StoreContent, StoreMessage, ) -from aleph_message.models.execution.base import Encoding, Payment, PaymentType +from aleph_message.models.execution.base import Encoding, Payment from aleph_message.models.execution.environment import ( - FunctionEnvironment, HostRequirements, HypervisorType, - InstanceEnvironment, - MachineResources, TrustedExecutionEnvironment, ) -from aleph_message.models.execution.instance import RootfsVolume -from aleph_message.models.execution.program import CodeContent, FunctionRuntime -from aleph_message.models.execution.volume import MachineVolume, ParentVolume from aleph_message.status import MessageStatus from ..conf import settings from ..exceptions import BroadcastError, InsufficientFundsError, InvalidMessageError -from ..types import Account, StorageEnum -from ..utils import extended_json_encoder, parse_volume +from ..types import Account, StorageEnum, TokenType +from ..utils import extended_json_encoder, make_instance_content, make_program_content from .abstract import AuthenticatedAlephClient from .http import AlephHttpClient @@ -285,7 +277,7 @@ async def create_post( post_type: str, ref: Optional[str] = None, address: Optional[str] = None, - channel: Optional[str] = None, + channel: Optional[str] = settings.DEFAULT_CHANNEL, inline: bool = True, storage_engine: StorageEnum = StorageEnum.storage, sync: bool = False, @@ -308,14 +300,14 @@ async def create_post( storage_engine=storage_engine, sync=sync, ) - return message, status + return message, status # type: ignore async def create_aggregate( self, key: str, - content: Mapping[str, Any], + content: dict[str, Any], address: Optional[str] = None, - channel: Optional[str] = None, + channel: Optional[str] = settings.DEFAULT_CHANNEL, inline: bool = True, sync: bool = False, ) -> Tuple[AggregateMessage, MessageStatus]: @@ -335,7 +327,7 @@ async def create_aggregate( allow_inlining=inline, sync=sync, ) - return message, status + return message, status # type: ignore async def create_store( self, @@ -347,7 +339,7 @@ async def create_store( ref: Optional[str] = None, storage_engine: StorageEnum = StorageEnum.storage, extra_fields: Optional[dict] = None, - channel: Optional[str] = None, + channel: Optional[str] = settings.DEFAULT_CHANNEL, sync: bool = False, ) -> Tuple[StoreMessage, MessageStatus]: address = address or settings.ADDRESS_TO_USE or self.account.get_address() @@ -400,7 +392,7 @@ async def create_store( if extra_fields is not None: values.update(extra_fields) - content = StoreContent(**values) + content = StoreContent.parse_obj(values) message, status, _ = await self.submit( content=content.model_dump(exclude_none=True), @@ -409,109 +401,89 @@ async def create_store( allow_inlining=True, sync=sync, ) - return message, status + return message, status # type: ignore async def create_program( self, program_ref: str, entrypoint: str, runtime: str, - environment_variables: Optional[Mapping[str, str]] = None, - storage_engine: StorageEnum = StorageEnum.storage, - channel: Optional[str] = None, + metadata: Optional[dict[str, Any]] = None, address: Optional[str] = None, - sync: bool = False, - memory: Optional[int] = None, vcpus: Optional[int] = None, + memory: Optional[int] = None, timeout_seconds: Optional[float] = None, - persistent: bool = False, - allow_amend: bool = False, internet: bool = True, + allow_amend: bool = False, aleph_api: bool = True, encoding: Encoding = Encoding.zip, - volumes: Optional[List[Mapping]] = None, - subscriptions: Optional[List[Mapping]] = None, - metadata: Optional[Mapping[str, Any]] = None, + persistent: bool = False, + volumes: Optional[list[Mapping]] = None, + environment_variables: Optional[dict[str, str]] = None, + subscriptions: Optional[list[dict]] = None, + sync: bool = False, + channel: Optional[str] = settings.DEFAULT_CHANNEL, + storage_engine: StorageEnum = StorageEnum.storage, ) -> Tuple[ProgramMessage, MessageStatus]: address = address or settings.ADDRESS_TO_USE or self.account.get_address() - volumes = volumes if volumes is not None else [] - memory = memory or settings.DEFAULT_VM_MEMORY - vcpus = vcpus or settings.DEFAULT_VM_VCPUS - timeout_seconds = timeout_seconds or settings.DEFAULT_VM_TIMEOUT - - # TODO: Check that program_ref, runtime and data_ref exist - - # Register the different ways to trigger a VM - if subscriptions: - # Trigger on HTTP calls and on aleph.im message subscriptions. - triggers = { - "http": True, - "persistent": persistent, - "message": subscriptions, - } - else: - # Trigger on HTTP calls. - triggers = {"http": True, "persistent": persistent} - - volumes: List[MachineVolume] = [parse_volume(volume) for volume in volumes] - - content = ProgramContent( - type="vm-function", + content = make_program_content( + program_ref=program_ref, + entrypoint=entrypoint, + runtime=runtime, + metadata=metadata, address=address, + vcpus=vcpus, + memory=memory, + timeout_seconds=timeout_seconds, + internet=internet, + aleph_api=aleph_api, allow_amend=allow_amend, - code=CodeContent( - encoding=encoding, - entrypoint=entrypoint, - ref=program_ref, - use_latest=True, - ), - on=triggers, - environment=FunctionEnvironment( - reproducible=False, - internet=internet, - aleph_api=aleph_api, - ), - variables=environment_variables, - resources=MachineResources( - vcpus=vcpus, - memory=memory, - seconds=timeout_seconds, - ), - runtime=FunctionRuntime( - ref=runtime, - use_latest=True, - comment=( - "Official aleph.im runtime" - if runtime == settings.DEFAULT_RUNTIME_ID - else "" - ), - ), - volumes=[parse_volume(volume) for volume in volumes], - time=time.time(), - metadata=metadata, + encoding=encoding, + persistent=persistent, + volumes=volumes, + environment_variables=environment_variables, + subscriptions=subscriptions, ) - # Ensure that the version of aleph-message used supports the field. - assert content.on.persistent == persistent - message, status, _ = await self.submit( content=content.model_dump(exclude_none=True), message_type=MessageType.program, channel=channel, storage_engine=storage_engine, sync=sync, + raise_on_rejected=False, ) - return message, status + if status in (MessageStatus.PROCESSED, MessageStatus.PENDING): + return message, status # type: ignore + + # get the reason for rejection + rejected_message = await self.get_message_error(message.item_hash) + assert rejected_message, "No rejected message found" + 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( + token_type=TokenType.ALEPH, + required_funds=required_balance, + available_funds=account_balance, + ) + else: + raise ValueError(f"Unknown error code {error_code}: {rejected_message}") async def create_instance( self, rootfs: str, rootfs_size: int, payment: Optional[Payment] = None, - environment_variables: Optional[Mapping[str, str]] = None, + environment_variables: Optional[dict[str, str]] = None, storage_engine: StorageEnum = StorageEnum.storage, - channel: Optional[str] = None, + channel: Optional[str] = settings.DEFAULT_CHANNEL, address: Optional[str] = None, sync: bool = False, memory: Optional[int] = None, @@ -522,57 +494,36 @@ async def create_instance( aleph_api: bool = True, hypervisor: Optional[HypervisorType] = None, trusted_execution: Optional[TrustedExecutionEnvironment] = None, - volumes: Optional[List[Mapping]] = None, + volumes: Optional[list[Mapping]] = None, volume_persistence: str = "host", - ssh_keys: Optional[List[str]] = None, - metadata: Optional[Mapping[str, Any]] = None, + ssh_keys: Optional[list[str]] = None, + metadata: Optional[dict[str, Any]] = None, requirements: Optional[HostRequirements] = None, ) -> Tuple[InstanceMessage, MessageStatus]: address = address or settings.ADDRESS_TO_USE or self.account.get_address() - volumes = volumes if volumes is not None else [] - memory = memory or settings.DEFAULT_VM_MEMORY - vcpus = vcpus or settings.DEFAULT_VM_VCPUS - timeout_seconds = timeout_seconds or settings.DEFAULT_VM_TIMEOUT - - payment = payment or Payment(chain=Chain.ETH, type=PaymentType.hold) - - # Default to the QEMU hypervisor for instances. - selected_hypervisor: HypervisorType = hypervisor or HypervisorType.qemu - - content = InstanceContent( + content = make_instance_content( + rootfs=rootfs, + rootfs_size=rootfs_size, + payment=payment, + environment_variables=environment_variables, address=address, + memory=memory, + vcpus=vcpus, + timeout_seconds=timeout_seconds, allow_amend=allow_amend, - environment=InstanceEnvironment( - internet=internet, - aleph_api=aleph_api, - hypervisor=selected_hypervisor, - trusted_execution=trusted_execution, - ), - variables=environment_variables, - resources=MachineResources( - vcpus=vcpus, - memory=memory, - seconds=timeout_seconds, - ), - rootfs=RootfsVolume( - parent=ParentVolume( - ref=rootfs, - use_latest=True, - ), - size_mib=rootfs_size, - persistence="host", - use_latest=True, - ), - volumes=[parse_volume(volume) for volume in volumes], - requirements=requirements, - time=time.time(), - authorized_keys=ssh_keys, + internet=internet, + aleph_api=aleph_api, + hypervisor=hypervisor, + trusted_execution=trusted_execution, + volumes=volumes, + ssh_keys=ssh_keys, metadata=metadata, - payment=payment, + requirements=requirements, ) + message, status, response = await self.submit( - content=content.model_dump(exclude_none=True), + content=content.dict(exclude_none=True), message_type=MessageType.instance, channel=channel, storage_engine=storage_engine, @@ -580,7 +531,7 @@ async def create_instance( raise_on_rejected=False, ) if status in (MessageStatus.PROCESSED, MessageStatus.PENDING): - return message, status + return message, status # type: ignore # get the reason for rejection rejected_message = await self.get_message_error(message.item_hash) @@ -594,17 +545,19 @@ async def create_instance( account_balance = float(error["account_balance"]) required_balance = float(error["required_balance"]) raise InsufficientFundsError( - required_funds=required_balance, available_funds=account_balance + token_type=TokenType.ALEPH, + required_funds=required_balance, + available_funds=account_balance, ) else: raise ValueError(f"Unknown error code {error_code}: {rejected_message}") async def forget( self, - hashes: List[ItemHash], + hashes: list[ItemHash], reason: Optional[str], storage_engine: StorageEnum = StorageEnum.storage, - channel: Optional[str] = None, + channel: Optional[str] = settings.DEFAULT_CHANNEL, address: Optional[str] = None, sync: bool = False, ) -> Tuple[ForgetMessage, MessageStatus]: @@ -625,13 +578,13 @@ async def forget( allow_inlining=True, sync=sync, ) - return message, status + return message, status # type: ignore async def submit( self, content: Dict[str, Any], message_type: MessageType, - channel: Optional[str] = None, + channel: Optional[str] = settings.DEFAULT_CHANNEL, storage_engine: StorageEnum = StorageEnum.storage, allow_inlining: bool = True, sync: bool = False, @@ -653,7 +606,7 @@ async def _storage_push_file_with_message( self, file_content: bytes, store_content: StoreContent, - channel: Optional[str] = None, + channel: Optional[str] = settings.DEFAULT_CHANNEL, sync: bool = False, ) -> Tuple[StoreMessage, MessageStatus]: """Push a file to the storage service.""" @@ -685,7 +638,7 @@ async def _storage_push_file_with_message( message_status = ( MessageStatus.PENDING if resp.status == 202 else MessageStatus.PROCESSED ) - return message, message_status + return message, message_status # type: ignore async def _upload_file_native( self, @@ -694,7 +647,7 @@ async def _upload_file_native( guess_mime_type: bool = False, ref: Optional[str] = None, extra_fields: Optional[dict] = None, - channel: Optional[str] = None, + channel: Optional[str] = settings.DEFAULT_CHANNEL, sync: bool = False, ) -> Tuple[StoreMessage, MessageStatus]: file_hash = hashlib.sha256(file_content).hexdigest() @@ -706,9 +659,9 @@ async def _upload_file_native( store_content = StoreContent( address=address, ref=ref, - item_type=StorageEnum.storage, - item_hash=file_hash, - mime_type=mime_type, + item_type=ItemType.storage, + item_hash=ItemHash(file_hash), + mime_type=mime_type, # type: ignore time=time.time(), **(extra_fields or {}), ) diff --git a/src/aleph/sdk/client/http.py b/src/aleph/sdk/client/http.py index ef57033c..6d2b42c7 100644 --- a/src/aleph/sdk/client/http.py +++ b/src/aleph/sdk/client/http.py @@ -2,6 +2,7 @@ import logging import os.path import ssl +import time from io import BytesIO from pathlib import Path from typing import ( @@ -20,7 +21,15 @@ import aiohttp from aiohttp.web import HTTPNotFound from aleph_message import parse_message -from aleph_message.models import AlephMessage, ItemHash, ItemType, MessageType +from aleph_message.models import ( + AlephMessage, + Chain, + ExecutableContent, + ItemHash, + ItemType, + MessageType, + ProgramContent, +) from aleph_message.status import MessageStatus from pydantic import ValidationError @@ -37,6 +46,7 @@ from ..utils import ( Writable, check_unix_socket_valid, + compute_sha256, copy_async_readable_to_buffer, extended_json_encoder, get_message_type_value, @@ -358,7 +368,7 @@ async def get_messages( ) @overload - async def get_message( + async def get_message( # type: ignore self, item_hash: str, message_type: Optional[Type[GenericMessage]] = None, @@ -383,7 +393,7 @@ async def get_message( resp.raise_for_status() except aiohttp.ClientResponseError as e: if e.status == 404: - raise MessageNotFoundError(f"No such hash {item_hash}") + raise MessageNotFoundError(f"No such hash {item_hash}") from e raise e message_raw = await resp.json() if message_raw["status"] == "forgotten": @@ -399,9 +409,9 @@ async def get_message( f"does not match the expected type '{expected_type}'" ) if with_status: - return message, message_raw["status"] + return message, message_raw["status"] # type: ignore else: - return message + return message # type: ignore async def get_message_error( self, @@ -448,6 +458,47 @@ async def watch_messages( elif msg.type == aiohttp.WSMsgType.ERROR: break + async def get_estimated_price( + self, + content: ExecutableContent, + ) -> PriceResponse: + cleaned_content = content.dict(exclude_none=True) + item_content: str = json.dumps( + cleaned_content, + separators=(",", ":"), + default=extended_json_encoder, + ) + message = parse_message( + dict( + sender=content.address, + chain=Chain.ETH, + type=( + MessageType.program + if isinstance(content, ProgramContent) + else MessageType.instance + ), + content=cleaned_content, + item_content=item_content, + time=time.time(), + channel=settings.DEFAULT_CHANNEL, + item_type=ItemType.inline, + item_hash=compute_sha256(item_content), + ) + ) + + async with self.http_session.post( + "/api/v0/price/estimate", json=dict(message=message) + ) as resp: + try: + resp.raise_for_status() + response_json = await resp.json() + return PriceResponse( + required_tokens=response_json["required_tokens"], + payment_type=response_json["payment_type"], + ) + except aiohttp.ClientResponseError as e: + raise e + async def get_program_price(self, item_hash: str) -> PriceResponse: async with self.http_session.get(f"/api/v0/price/{item_hash}") as resp: try: @@ -491,15 +542,21 @@ async def get_stored_content( resp = f"Invalid CID: {message.content.item_hash}" else: filename = safe_getattr(message.content, "metadata.name") - hash = message.content.item_hash + item_hash = message.content.item_hash url = ( f"{self.api_server}/api/v0/storage/raw/" - if len(hash) == 64 + if len(item_hash) == 64 else settings.IPFS_GATEWAY - ) + hash - result = StoredContent(filename=filename, hash=hash, url=url) + ) + item_hash + result = StoredContent( + filename=filename, hash=item_hash, url=url, error=None + ) except MessageNotFoundError: resp = f"Message not found: {item_hash}" except ForgottenMessageError: resp = f"Message forgotten: {item_hash}" - return result if result else StoredContent(error=resp) + return ( + result + if result + else StoredContent(error=resp, filename=None, hash=None, url=None) + ) diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index 16ddc4a1..c81c9367 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -45,27 +45,22 @@ class Settings(BaseSettings): HTTP_REQUEST_TIMEOUT: float = 15.0 DEFAULT_CHANNEL: str = "ALEPH-CLOUDSOLUTIONS" + + # Firecracker runtime for programs DEFAULT_RUNTIME_ID: str = ( "63f07193e6ee9d207b7d1fcf8286f9aee34e6f12f101d2ec77c1229f92964696" ) - DEBIAN_11_ROOTFS_ID: str = ( - "887957042bb0e360da3485ed33175882ce72a70d79f1ba599400ff4802b7cee7" - ) - DEBIAN_12_ROOTFS_ID: str = ( - "6e30de68c6cedfa6b45240c2b51e52495ac6fb1bd4b36457b3d5ca307594d595" - ) - UBUNTU_22_ROOTFS_ID: str = ( - "77fef271aa6ff9825efa3186ca2e715d19e7108279b817201c69c34cedc74c27" - ) - DEBIAN_11_QEMU_ROOTFS_ID: str = ( - "f7e68c568906b4ebcd3cd3c4bfdff96c489cd2a9ef73ba2d7503f244dfd578de" - ) + + # Qemu rootfs for instances DEBIAN_12_QEMU_ROOTFS_ID: str = ( "b6ff5c3a8205d1ca4c7c3369300eeafff498b558f71b851aa2114afd0a532717" ) UBUNTU_22_QEMU_ROOTFS_ID: str = ( "4a0f62da42f4478544616519e6f5d58adb1096e069b392b151d47c3609492d0c" ) + UBUNTU_24_QEMU_ROOTFS_ID: str = ( + "5330dcefe1857bcd97b7b7f24d1420a7d46232d53f27be280c8a7071d88bd84e" + ) DEFAULT_CONFIDENTIAL_FIRMWARE: str = ( "ba5bb13f3abca960b101a759be162b229e2b7e93ecad9d1307e54de887f177ff" @@ -87,6 +82,7 @@ class Settings(BaseSettings): VM_URL_PATH: ClassVar[str] = "https://aleph.sh/vm/{hash}" VM_URL_HOST: ClassVar[str] = "https://{hash_base32}.aleph.sh" IPFS_GATEWAY: ClassVar[str] = "https://ipfs.aleph.cloud/ipfs/" + CRN_URL_FOR_PROGRAMS: ClassVar[str] = "https://dchq.staging.aleph.sh/" # Web3Provider settings TOKEN_DECIMALS: ClassVar[int] = 18 diff --git a/src/aleph/sdk/connectors/superfluid.py b/src/aleph/sdk/connectors/superfluid.py index 4b7274f8..76bbf907 100644 --- a/src/aleph/sdk/connectors/superfluid.py +++ b/src/aleph/sdk/connectors/superfluid.py @@ -1,14 +1,19 @@ from __future__ import annotations from decimal import Decimal -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from eth_utils import to_normalized_address from superfluid import CFA_V1, Operation, Web3FlowInfo +from aleph.sdk.evm_utils import ( + FlowUpdate, + from_wei_token, + get_super_token_address, + to_wei_token, +) from aleph.sdk.exceptions import InsufficientFundsError - -from ..evm_utils import get_super_token_address, to_human_readable_token, to_wei_token +from aleph.sdk.types import TokenType if TYPE_CHECKING: from aleph.sdk.chains.ethereum import ETHAccount @@ -44,6 +49,7 @@ async def _execute_operation_with_account(self, operation: Operation) -> str: return await self.account._sign_and_send_transaction(populated_transaction) def can_start_flow(self, flow: Decimal, block=True) -> bool: + """Check if the account has enough funds to start a Superfluid flow of the given size.""" valid = False if self.account.can_transact(block=block): balance = self.account.get_super_token_balance() @@ -51,8 +57,9 @@ def can_start_flow(self, flow: Decimal, block=True) -> bool: valid = balance > MIN_FLOW_4H if not valid and block: raise InsufficientFundsError( - required_funds=float(MIN_FLOW_4H), - available_funds=to_human_readable_token(balance), + token_type=TokenType.ALEPH, + required_funds=float(from_wei_token(MIN_FLOW_4H)), + available_funds=float(from_wei_token(balance)), ) return valid @@ -96,3 +103,51 @@ async def update_flow(self, receiver: str, flow: Decimal) -> str: flow_rate=int(to_wei_token(flow)), ), ) + + async def manage_flow( + self, + receiver: str, + flow: Decimal, + update_type: FlowUpdate, + ) -> Optional[str]: + """ + Update the flow of a Superfluid stream between a sender and receiver. + This function either increases or decreases the flow rate between the sender and receiver, + based on the update_type. If no flow exists and the update type is augmentation, it creates a new flow + with the specified rate. If the update type is reduction and the reduction amount brings the flow to zero + or below, the flow is deleted. + + :param receiver: Address of the receiver in hexadecimal format. + :param flow: The flow rate to be added or removed (in ether). + :param update_type: The type of update to perform (augmentation or reduction). + :return: The transaction hash of the executed operation (create, update, or delete flow). + """ + + # Retrieve current flow info + flow_info: Web3FlowInfo = await self.account.get_flow(receiver) + + current_flow_rate_wei: Decimal = Decimal(flow_info["flowRate"] or 0) + flow_rate_wei: int = int(to_wei_token(flow)) + + if update_type == FlowUpdate.INCREASE: + if current_flow_rate_wei > 0: + # Update existing flow by increasing the rate + new_flow_rate_wei = current_flow_rate_wei + flow_rate_wei + new_flow_rate_ether = from_wei_token(new_flow_rate_wei) + return await self.account.update_flow(receiver, new_flow_rate_ether) + else: + # Create a new flow if none exists + return await self.account.create_flow(receiver, flow) + else: + if current_flow_rate_wei > 0: + # Reduce the existing flow + new_flow_rate_wei = current_flow_rate_wei - flow_rate_wei + # Ensure to not leave infinitesimal flows + # Often, there were 1-10 wei remaining in the flow rate, which prevented the flow from being deleted + if new_flow_rate_wei > 99: + new_flow_rate_ether = from_wei_token(new_flow_rate_wei) + return await self.account.update_flow(receiver, new_flow_rate_ether) + else: + # Delete the flow if the new flow rate is zero or negative + return await self.account.delete_flow(receiver) + return None diff --git a/src/aleph/sdk/evm_utils.py b/src/aleph/sdk/evm_utils.py index 4d2026ef..a425d580 100644 --- a/src/aleph/sdk/evm_utils.py +++ b/src/aleph/sdk/evm_utils.py @@ -1,4 +1,5 @@ -from decimal import Decimal +from decimal import ROUND_CEILING, Context, Decimal +from enum import Enum from typing import List, Optional, Union from aleph_message.models import Chain @@ -21,12 +22,26 @@ }]""" -def to_human_readable_token(amount: Decimal) -> float: - return float(amount / (Decimal(10) ** Decimal(settings.TOKEN_DECIMALS))) +class FlowUpdate(str, Enum): + REDUCE = "reduce" + INCREASE = "increase" + + +def ether_rounding(amount: Decimal) -> Decimal: + """Rounds the given value to 18 decimals.""" + return amount.quantize( + Decimal(1) / Decimal(10**18), rounding=ROUND_CEILING, context=Context(prec=36) + ) + + +def from_wei_token(amount: Decimal) -> Decimal: + """Converts the given wei value to ether.""" + return ether_rounding(amount / Decimal(10) ** Decimal(settings.TOKEN_DECIMALS)) def to_wei_token(amount: Decimal) -> Decimal: - return amount * Decimal(10) ** Decimal(settings.TOKEN_DECIMALS) + """Converts the given ether value to wei.""" + return Decimal(int(amount * Decimal(10) ** Decimal(settings.TOKEN_DECIMALS))) def get_chain_id(chain: Union[Chain, str, None]) -> Optional[int]: diff --git a/src/aleph/sdk/exceptions.py b/src/aleph/sdk/exceptions.py index a538a31c..05ed755f 100644 --- a/src/aleph/sdk/exceptions.py +++ b/src/aleph/sdk/exceptions.py @@ -1,5 +1,8 @@ from abc import ABC +from .types import TokenType +from .utils import displayable_amount + class QueryError(ABC, ValueError): """The result of an API query is inconsistent.""" @@ -69,14 +72,18 @@ class ForgottenMessageError(QueryError): class InsufficientFundsError(Exception): """Raised when the account does not have enough funds to perform an action""" + token_type: TokenType required_funds: float available_funds: float - def __init__(self, required_funds: float, available_funds: float): + def __init__( + self, token_type: TokenType, required_funds: float, available_funds: float + ): + self.token_type = token_type self.required_funds = required_funds self.available_funds = available_funds super().__init__( - f"Insufficient funds: required {required_funds}, available {available_funds}" + f"Insufficient funds ({self.token_type.value}): required {displayable_amount(self.required_funds, decimals=8)}, available {displayable_amount(self.available_funds, decimals=8)}" ) diff --git a/src/aleph/sdk/types.py b/src/aleph/sdk/types.py index e192335f..cf23f19d 100644 --- a/src/aleph/sdk/types.py +++ b/src/aleph/sdk/types.py @@ -83,7 +83,20 @@ class ChainInfo(BaseModel): class StoredContent(BaseModel): + """ + A stored content. + """ + filename: Optional[str] = Field(default=None) hash: Optional[str] = Field(default=None) url: Optional[str] = Field(default=None) error: Optional[str] = Field(default=None) + + +class TokenType(str, Enum): + """ + A token type. + """ + + GAS = "GAS" + ALEPH = "ALEPH" diff --git a/src/aleph/sdk/utils.py b/src/aleph/sdk/utils.py index a6ec40a8..e933440a 100644 --- a/src/aleph/sdk/utils.py +++ b/src/aleph/sdk/utils.py @@ -8,6 +8,7 @@ import os import subprocess from datetime import date, datetime, time +from decimal import Context, Decimal, InvalidOperation from enum import Enum from pathlib import Path from shutil import make_archive @@ -15,7 +16,6 @@ Any, Dict, Iterable, - List, Mapping, Optional, Protocol, @@ -28,10 +28,38 @@ from uuid import UUID from zipfile import BadZipFile, ZipFile -import pydantic_core -from aleph_message.models import ItemHash, MessageType -from aleph_message.models.execution.program import Encoding -from aleph_message.models.execution.volume import MachineVolume +from aleph_message.models import ( + Chain, + InstanceContent, + ItemHash, + MachineType, + MessageType, + ProgramContent, +) +from aleph_message.models.execution.base import Payment, PaymentType +from aleph_message.models.execution.environment import ( + FunctionEnvironment, + FunctionTriggers, + HostRequirements, + HypervisorType, + InstanceEnvironment, + MachineResources, + Subscription, + TrustedExecutionEnvironment, +) +from aleph_message.models.execution.instance import RootfsVolume +from aleph_message.models.execution.program import ( + CodeContent, + Encoding, + FunctionRuntime, +) +from aleph_message.models.execution.volume import ( + MachineVolume, + ParentVolume, + PersistentVolumeSizeMib, + VolumePersistence, +) +from aleph_message.utils import Mebibytes from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from jwcrypto.jwa import JWA @@ -179,13 +207,17 @@ def extended_json_encoder(obj: Any) -> Any: def parse_volume(volume_dict: Union[Mapping, MachineVolume]) -> MachineVolume: # Python 3.9 does not support `isinstance(volume_dict, MachineVolume)`, # so we need to iterate over all types. + if any( + isinstance(volume_dict, volume_type) for volume_type in get_args(MachineVolume) + ): + return volume_dict # type: ignore + for volume_type in get_args(MachineVolume): try: return volume_type.model_validate(volume_dict) except ValueError: - continue - else: - raise ValueError(f"Could not parse volume: {volume_dict}") + pass + raise ValueError(f"Could not parse volume: {volume_dict}") def compute_sha256(s: str) -> str: @@ -230,7 +262,7 @@ def sign_vm_control_payload(payload: Dict[str, str], ephemeral_key) -> str: async def run_in_subprocess( - command: List[str], check: bool = True, stdin_input: Optional[bytes] = None + command: list[str], check: bool = True, stdin_input: Optional[bytes] = None ) -> bytes: """Run the specified command in a subprocess, returns the stdout of the process.""" logger.debug(f"command: {' '.join(command)}") @@ -397,3 +429,166 @@ def safe_getattr(obj, attr, default=None): if obj is default: break return obj + + +def displayable_amount( + amount: Union[str, int, float, Decimal], decimals: int = 18 +) -> str: + """Returns the amount as a string without unnecessary decimals.""" + + str_amount = "" + try: + dec_amount = Decimal(amount) + if decimals: + dec_amount = dec_amount.quantize( + Decimal(1) / Decimal(10**decimals), context=Context(prec=36) + ) + str_amount = str(format(dec_amount.normalize(), "f")) + except ValueError: + logger.error(f"Invalid amount to display: {amount}") + exit(1) + except InvalidOperation: + logger.error(f"Invalid operation on amount to display: {amount}") + exit(1) + return str_amount + + +def make_instance_content( + rootfs: str, + rootfs_size: int, + payment: Optional[Payment] = None, + environment_variables: Optional[dict[str, str]] = None, + address: Optional[str] = None, + memory: Optional[int] = None, + vcpus: Optional[int] = None, + timeout_seconds: Optional[float] = None, + allow_amend: bool = False, + internet: bool = True, + aleph_api: bool = True, + hypervisor: Optional[HypervisorType] = None, + trusted_execution: Optional[TrustedExecutionEnvironment] = None, + volumes: Optional[list[Mapping]] = None, + ssh_keys: Optional[list[str]] = None, + metadata: Optional[dict[str, Any]] = None, + requirements: Optional[HostRequirements] = None, +) -> InstanceContent: + """ + Create InstanceContent object given the provided fields. + """ + + address = address or "0x0000000000000000000000000000000000000000" + payment = payment or Payment(chain=Chain.ETH, type=PaymentType.hold, receiver=None) + selected_hypervisor: HypervisorType = hypervisor or HypervisorType.qemu + vcpus = vcpus or settings.DEFAULT_VM_VCPUS + memory = memory or settings.DEFAULT_VM_MEMORY + timeout_seconds = timeout_seconds or settings.DEFAULT_VM_TIMEOUT + volumes = volumes if volumes is not None else [] + + return InstanceContent( + address=address, + allow_amend=allow_amend, + environment=InstanceEnvironment( + internet=internet, + aleph_api=aleph_api, + hypervisor=selected_hypervisor, + trusted_execution=trusted_execution, + ), + variables=environment_variables, + resources=MachineResources( + vcpus=vcpus, + memory=Mebibytes(memory), + seconds=int(timeout_seconds), + ), + rootfs=RootfsVolume( + parent=ParentVolume( + ref=ItemHash(rootfs), + use_latest=True, + ), + size_mib=PersistentVolumeSizeMib(rootfs_size), + persistence=VolumePersistence.host, + ), + volumes=[parse_volume(volume) for volume in volumes], + requirements=requirements, + time=datetime.now().timestamp(), + authorized_keys=ssh_keys, + metadata=metadata, + payment=payment, + ) + + +def make_program_content( + program_ref: str, + entrypoint: str, + runtime: str, + metadata: Optional[dict[str, Any]] = None, + address: Optional[str] = None, + vcpus: Optional[int] = None, + memory: Optional[int] = None, + timeout_seconds: Optional[float] = None, + internet: bool = False, + aleph_api: bool = True, + allow_amend: bool = False, + encoding: Encoding = Encoding.zip, + persistent: bool = False, + volumes: Optional[list[Mapping]] = None, + environment_variables: Optional[dict[str, str]] = None, + subscriptions: Optional[list[dict]] = None, + payment: Optional[Payment] = None, +) -> ProgramContent: + """ + Create ProgramContent object given the provided fields. + """ + + address = address or "0x0000000000000000000000000000000000000000" + payment = payment or Payment(chain=Chain.ETH, type=PaymentType.hold, receiver=None) + vcpus = vcpus or settings.DEFAULT_VM_VCPUS + memory = memory or settings.DEFAULT_VM_MEMORY + timeout_seconds = timeout_seconds or settings.DEFAULT_VM_TIMEOUT + volumes = volumes if volumes is not None else [] + subscriptions = ( + [Subscription(**sub) for sub in subscriptions] + if subscriptions is not None + else None + ) + + return ProgramContent( + type=MachineType.vm_function, + address=address, + allow_amend=allow_amend, + code=CodeContent( + encoding=encoding, + entrypoint=entrypoint, + ref=ItemHash(program_ref), + use_latest=True, + ), + on=FunctionTriggers( + http=True, + persistent=persistent, + message=subscriptions, + ), + environment=FunctionEnvironment( + reproducible=False, + internet=internet, + aleph_api=aleph_api, + ), + variables=environment_variables, + resources=MachineResources( + vcpus=vcpus, + memory=Mebibytes(memory), + seconds=int(timeout_seconds), + ), + runtime=FunctionRuntime( + ref=ItemHash(runtime), + use_latest=True, + comment=( + "Official aleph.im runtime" + if runtime == settings.DEFAULT_RUNTIME_ID + else "" + ), + ), + volumes=[parse_volume(volume) for volume in volumes], + time=datetime.now().timestamp(), + metadata=metadata, + authorized_keys=[], + payment=payment, + ) diff --git a/tests/unit/test_asynchronous.py b/tests/unit/test_asynchronous.py index b044e170..e2647590 100644 --- a/tests/unit/test_asynchronous.py +++ b/tests/unit/test_asynchronous.py @@ -7,6 +7,7 @@ Chain, ForgetMessage, InstanceMessage, + ItemHash, MessageType, Payment, PaymentType, @@ -184,12 +185,16 @@ async def test_create_confidential_instance(mock_session_with_post_success): ), hypervisor=HypervisorType.qemu, trusted_execution=TrustedExecutionEnvironment( - firmware="cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe", + firmware=ItemHash( + "cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe" + ), policy=0b1, ), requirements=HostRequirements( node=NodeRequirements( - node_hash="cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe", + node_hash=ItemHash( + "cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe" + ), ) ), ) @@ -285,5 +290,6 @@ async def test_create_instance_insufficient_funds_error( payment=Payment( chain=Chain.ETH, type=PaymentType.hold, + receiver=None, ), ) diff --git a/tests/unit/test_price.py b/tests/unit/test_price.py index b596c33b..830fb931 100644 --- a/tests/unit/test_price.py +++ b/tests/unit/test_price.py @@ -11,11 +11,11 @@ async def test_get_program_price_valid(): Test that the get_program_price method returns the correct PriceResponse when given a valid item hash. """ - expected_response = { - "required_tokens": 3.0555555555555556e-06, - "payment_type": "superfluid", - } - mock_session = make_mock_get_session(expected_response) + expected = PriceResponse( + required_tokens=3.0555555555555556e-06, + payment_type="superfluid", + ) + mock_session = make_mock_get_session(expected.dict()) async with mock_session: response = await mock_session.get_program_price("cacacacacacaca") assert response == PriceResponse(**expected_response) # type: ignore diff --git a/tests/unit/test_superfluid.py b/tests/unit/test_superfluid.py index c2f853bd..74bcc38e 100644 --- a/tests/unit/test_superfluid.py +++ b/tests/unit/test_superfluid.py @@ -7,6 +7,7 @@ from eth_utils import to_checksum_address from aleph.sdk.chains.ethereum import ETHAccount +from aleph.sdk.evm_utils import FlowUpdate def generate_fake_eth_address(): @@ -24,6 +25,7 @@ def mock_superfluid(): mock_superfluid.create_flow = AsyncMock(return_value="0xTransactionHash") mock_superfluid.delete_flow = AsyncMock(return_value="0xTransactionHash") mock_superfluid.update_flow = AsyncMock(return_value="0xTransactionHash") + mock_superfluid.manage_flow = AsyncMock(return_value="0xTransactionHash") # Mock get_flow to return a mock Web3FlowInfo mock_flow_info = {"timestamp": 0, "flowRate": 0, "deposit": 0, "owedDeposit": 0} @@ -98,3 +100,14 @@ async def test_get_flow(eth_account, mock_superfluid): assert flow_info["flowRate"] == 0 assert flow_info["deposit"] == 0 assert flow_info["owedDeposit"] == 0 + + +@pytest.mark.asyncio +async def test_manage_flow(eth_account, mock_superfluid): + receiver = generate_fake_eth_address() + flow = Decimal("0.005") + + tx_hash = await eth_account.manage_flow(receiver, flow, FlowUpdate.INCREASE) + + assert tx_hash == "0xTransactionHash" + mock_superfluid.manage_flow.assert_awaited_once() diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 8e4083c0..4ceb5a3f 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1,5 +1,6 @@ import base64 import datetime +from unittest.mock import MagicMock import pytest as pytest from aleph_message.models import ( @@ -158,6 +159,7 @@ def test_parse_immutable_volume(): def test_parse_ephemeral_volume(): volume_dict = { "comment": "Dummy hash", + "mount": "/opt/data", "ephemeral": True, "size_mib": 1, } @@ -169,6 +171,8 @@ def test_parse_ephemeral_volume(): def test_parse_persistent_volume(): volume_dict = { + "comment": "Dummy hash", + "mount": "/opt/data", "parent": { "ref": "QmX8K1c22WmQBAww5ShWQqwMiFif7XFrJD6iFBj7skQZXW", "use_latest": True, @@ -184,9 +188,9 @@ def test_parse_persistent_volume(): assert isinstance(volume, PersistentVolume) -def test_calculate_firmware_hash(mocker): - mock_path = mocker.Mock( - read_bytes=mocker.Mock(return_value=b"abc"), +def test_calculate_firmware_hash(): + mock_path = MagicMock( + read_bytes=MagicMock(return_value=b"abc"), ) assert ( From 5fdc4a8fe33d1ef35007a3bdad3fd5a6b4c5b5c1 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Fri, 4 Oct 2024 10:59:02 +0900 Subject: [PATCH 22/37] Migrate to Pydantic v2, update model validation and fix async issues - Migrated to Pydantic v2: - Replaced deprecated `parse_obj()` and `parse_raw()` with `model_validate()` and `model_validate_json()`. - Replaced `.dict()` with `.model_dump()` for serializing models to dictionaries. - Updated `validator` to `field_validator` and `root_validator` to `model_validator` to comply with Pydantic v2 syntax changes. - Fixed asyncio issues: - Added `await` for asynchronous methods like `raise_for_status()` in `RemoteAccount` and other HTTP operations to avoid `RuntimeWarning`. - Updated config handling: - Used `ClassVar` for constants in `Settings` and other configuration classes. - Replaced `Config` with `ConfigDict` in Pydantic models to follow v2 conventions. - Added default values for missing fields in chain configurations (`CHAINS_SEPOLIA_ACTIVE`, etc.). - Adjusted signature handling: - Updated the signing logic to prepend `0x` in the `BaseAccount` signature generation to ensure correct Ethereum address formatting. - Minor fixes: - Resolved issue with extra fields not being allowed by default by specifying `extra="allow"` or `extra="forbid"` where necessary. - Fixed tests to account for changes in model validation and serialization behavior. - Added `pydantic-settings` as a new dependency for configuration management. --- pyproject.toml | 6 +++--- src/aleph/sdk/conf.py | 15 +++++++++------ src/aleph/sdk/utils.py | 1 + tests/unit/aleph_vm_authentication.py | 9 +++++---- tests/unit/test_utils.py | 18 +++++++++--------- 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bc28fb15..f3384155 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,15 +34,15 @@ dependencies = [ "aleph-superfluid>=0.2.1", "base58==2.1.1", # Needed now as default with _load_account changement "coincurve; python_version<'3.11'", - "coincurve>=19; python_version>='3.11'", - "eth-abi>=4; python_version>='3.11'", + "coincurve>=19.0.0; python_version>='3.11'", + "eth-abi>=4.0.O; python_version>='3.11'", "eth-typing==4.3.1", "jwcrypto==1.5.6", "pydantic-settings>=2", "pynacl==1.5", # Needed now as default with _load_account changement "python-magic", "typing-extensions", - "web3==6.3", + "web3==6.3.0", ] optional-dependencies.all = [ diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index c81c9367..68098092 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -5,10 +5,11 @@ from shutil import which from typing import ClassVar, Dict, List, Optional, Union +from pydantic_settings import BaseSettings + from aleph_message.models import Chain from aleph_message.models.execution.environment import HypervisorType -from pydantic import BaseModel, Field -from pydantic_settings import BaseSettings, SettingsConfigDict +from pydantic import BaseModel, ConfigDict, Field from aleph.sdk.types import ChainInfo @@ -42,7 +43,7 @@ class Settings(BaseSettings): REMOTE_CRYPTO_HOST: Optional[str] = None REMOTE_CRYPTO_UNIX_SOCKET: Optional[str] = None ADDRESS_TO_USE: Optional[str] = None - HTTP_REQUEST_TIMEOUT: float = 15.0 + HTTP_REQUEST_TIMEOUT: ClassVar[float] = 15.0 DEFAULT_CHANNEL: str = "ALEPH-CLOUDSOLUTIONS" @@ -221,8 +222,10 @@ class Settings(BaseSettings): DNS_STATIC_DOMAIN: ClassVar[str] = "static.public.aleph.sh" DNS_RESOLVERS: ClassVar[List[str]] = ["9.9.9.9", "1.1.1.1"] - model_config = SettingsConfigDict( - env_prefix="ALEPH_", case_sensitive=False, env_file=".env" + model_config = ConfigDict( + env_prefix="ALEPH_", + case_sensitive=False, + env_file=".env" ) @@ -234,7 +237,7 @@ class MainConfiguration(BaseModel): path: Path chain: Chain - model_config = SettingsConfigDict(use_enum_values=True) + model_config = ConfigDict(use_enum_values = True) # Settings singleton diff --git a/src/aleph/sdk/utils.py b/src/aleph/sdk/utils.py index e933440a..80b2be58 100644 --- a/src/aleph/sdk/utils.py +++ b/src/aleph/sdk/utils.py @@ -63,6 +63,7 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from jwcrypto.jwa import JWA +import pydantic_core from aleph.sdk.conf import settings from aleph.sdk.types import GenericMessage, SEVInfo, SEVMeasurement diff --git a/tests/unit/aleph_vm_authentication.py b/tests/unit/aleph_vm_authentication.py index c1710c16..324868be 100644 --- a/tests/unit/aleph_vm_authentication.py +++ b/tests/unit/aleph_vm_authentication.py @@ -75,8 +75,8 @@ def payload_must_be_hex(cls, value: bytes) -> bytes: """Convert the payload from hexadecimal to bytes""" return bytes_from_hex(value.decode()) - @model_validator(mode="after") # type: ignore - def check_expiry(cls, values: SignedPubKeyHeader) -> SignedPubKeyHeader: + @model_validator(mode="after") + def check_expiry(cls, values) -> Dict[str, bytes]: """Check that the token has not expired""" payload: bytes = values.payload content = SignedPubKeyPayload.model_validate_json(payload) @@ -87,8 +87,9 @@ def check_expiry(cls, values: SignedPubKeyHeader) -> SignedPubKeyHeader: return values - @model_validator(mode="after") # type: ignore - def check_signature(cls, values: SignedPubKeyHeader) -> SignedPubKeyHeader: + @model_validator(mode="after") + def check_signature(cls, values: Dict[str, bytes]) -> Dict[str, bytes]: + """Check that the signature is valid""" signature: bytes = values.signature payload: bytes = values.payload content = SignedPubKeyPayload.model_validate_json(payload) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 4ceb5a3f..0d843394 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -115,17 +115,17 @@ def test_enum_as_str(): ( MessageType.aggregate, { - "address": "0x1", - "content": { - "Hello": { - "vcpus": 1, - "memory": 1024, - "seconds": 1, - "published_ports": None, + 'address': '0x1', + 'content': { + 'Hello': { + 'vcpus': 1, + 'memory': 1024, + 'seconds': 1, + 'published_ports': None, }, }, - "key": "test", - "time": 1.0, + 'key': 'test', + 'time': 1.0, }, ), ], From 696cf90924da3c7247d96915c408371ca7b3f597 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Fri, 4 Oct 2024 11:46:22 +0900 Subject: [PATCH 23/37] fix: lint tests were failing - Updated all instances of **extra_fields to ensure proper handling of Optional dictionaries using `(extra_fields or {})` pattern. - Added proper return statements in `AlephHttpClient.get_message_status` to return parsed JSON data as a `MessageStatus` object. - Updated `Settings` class in `conf.py` to correct DNS resolvers type and simplify the `model_config` definition. - Refactored `parse_volume` to ensure correct handling of Mapping types and MachineVolume types, avoiding TypeErrors. - Improved field validation and model validation in `SignedPubKeyHeader` by using correct Pydantic v2 validation decorators and ensuring compatibility with the new model behavior. - Applied formatting and consistency fixes for `model_dump` usage and indentation improvements in test files. --- src/aleph/sdk/conf.py | 9 +++------ src/aleph/sdk/utils.py | 1 - tests/unit/aleph_vm_authentication.py | 4 ++-- tests/unit/test_utils.py | 18 +++++++++--------- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index 68098092..b29bf4f7 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -5,11 +5,10 @@ from shutil import which from typing import ClassVar, Dict, List, Optional, Union -from pydantic_settings import BaseSettings - from aleph_message.models import Chain from aleph_message.models.execution.environment import HypervisorType from pydantic import BaseModel, ConfigDict, Field +from pydantic_settings import BaseSettings from aleph.sdk.types import ChainInfo @@ -223,9 +222,7 @@ class Settings(BaseSettings): DNS_RESOLVERS: ClassVar[List[str]] = ["9.9.9.9", "1.1.1.1"] model_config = ConfigDict( - env_prefix="ALEPH_", - case_sensitive=False, - env_file=".env" + env_prefix="ALEPH_", case_sensitive=False, env_file=".env" ) @@ -237,7 +234,7 @@ class MainConfiguration(BaseModel): path: Path chain: Chain - model_config = ConfigDict(use_enum_values = True) + model_config = ConfigDict(use_enum_values=True) # Settings singleton diff --git a/src/aleph/sdk/utils.py b/src/aleph/sdk/utils.py index 80b2be58..e933440a 100644 --- a/src/aleph/sdk/utils.py +++ b/src/aleph/sdk/utils.py @@ -63,7 +63,6 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from jwcrypto.jwa import JWA -import pydantic_core from aleph.sdk.conf import settings from aleph.sdk.types import GenericMessage, SEVInfo, SEVMeasurement diff --git a/tests/unit/aleph_vm_authentication.py b/tests/unit/aleph_vm_authentication.py index 324868be..2fd1cf45 100644 --- a/tests/unit/aleph_vm_authentication.py +++ b/tests/unit/aleph_vm_authentication.py @@ -76,7 +76,7 @@ def payload_must_be_hex(cls, value: bytes) -> bytes: return bytes_from_hex(value.decode()) @model_validator(mode="after") - def check_expiry(cls, values) -> Dict[str, bytes]: + def check_expiry(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": """Check that the token has not expired""" payload: bytes = values.payload content = SignedPubKeyPayload.model_validate_json(payload) @@ -88,7 +88,7 @@ def check_expiry(cls, values) -> Dict[str, bytes]: return values @model_validator(mode="after") - def check_signature(cls, values: Dict[str, bytes]) -> Dict[str, bytes]: + def check_signature(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": """Check that the signature is valid""" signature: bytes = values.signature payload: bytes = values.payload diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 0d843394..4ceb5a3f 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -115,17 +115,17 @@ def test_enum_as_str(): ( MessageType.aggregate, { - 'address': '0x1', - 'content': { - 'Hello': { - 'vcpus': 1, - 'memory': 1024, - 'seconds': 1, - 'published_ports': None, + "address": "0x1", + "content": { + "Hello": { + "vcpus": 1, + "memory": 1024, + "seconds": 1, + "published_ports": None, }, }, - 'key': 'test', - 'time': 1.0, + "key": "test", + "time": 1.0, }, ), ], From 5b9148a7c24130a8876f9f7a2aee4faddc71859e Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Fri, 4 Oct 2024 10:59:02 +0900 Subject: [PATCH 24/37] Migrate to Pydantic v2, update model validation and fix async issues - Migrated to Pydantic v2: - Replaced deprecated `parse_obj()` and `parse_raw()` with `model_validate()` and `model_validate_json()`. - Replaced `.dict()` with `.model_dump()` for serializing models to dictionaries. - Updated `validator` to `field_validator` and `root_validator` to `model_validator` to comply with Pydantic v2 syntax changes. - Fixed asyncio issues: - Added `await` for asynchronous methods like `raise_for_status()` in `RemoteAccount` and other HTTP operations to avoid `RuntimeWarning`. - Updated config handling: - Used `ClassVar` for constants in `Settings` and other configuration classes. - Replaced `Config` with `ConfigDict` in Pydantic models to follow v2 conventions. - Added default values for missing fields in chain configurations (`CHAINS_SEPOLIA_ACTIVE`, etc.). - Adjusted signature handling: - Updated the signing logic to prepend `0x` in the `BaseAccount` signature generation to ensure correct Ethereum address formatting. - Minor fixes: - Resolved issue with extra fields not being allowed by default by specifying `extra="allow"` or `extra="forbid"` where necessary. - Fixed tests to account for changes in model validation and serialization behavior. - Added `pydantic-settings` as a new dependency for configuration management. --- src/aleph/sdk/conf.py | 7 +++++-- src/aleph/sdk/utils.py | 1 + tests/unit/aleph_vm_authentication.py | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index b29bf4f7..c0df405e 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -5,10 +5,11 @@ from shutil import which from typing import ClassVar, Dict, List, Optional, Union +from pydantic_settings import BaseSettings + from aleph_message.models import Chain from aleph_message.models.execution.environment import HypervisorType from pydantic import BaseModel, ConfigDict, Field -from pydantic_settings import BaseSettings from aleph.sdk.types import ChainInfo @@ -222,7 +223,9 @@ class Settings(BaseSettings): DNS_RESOLVERS: ClassVar[List[str]] = ["9.9.9.9", "1.1.1.1"] model_config = ConfigDict( - env_prefix="ALEPH_", case_sensitive=False, env_file=".env" + env_prefix="ALEPH_", + case_sensitive=False, + env_file=".env" ) diff --git a/src/aleph/sdk/utils.py b/src/aleph/sdk/utils.py index e933440a..80b2be58 100644 --- a/src/aleph/sdk/utils.py +++ b/src/aleph/sdk/utils.py @@ -63,6 +63,7 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from jwcrypto.jwa import JWA +import pydantic_core from aleph.sdk.conf import settings from aleph.sdk.types import GenericMessage, SEVInfo, SEVMeasurement diff --git a/tests/unit/aleph_vm_authentication.py b/tests/unit/aleph_vm_authentication.py index 2fd1cf45..fd65be0c 100644 --- a/tests/unit/aleph_vm_authentication.py +++ b/tests/unit/aleph_vm_authentication.py @@ -15,7 +15,7 @@ from eth_account.messages import encode_defunct from jwcrypto import jwk from jwcrypto.jwa import JWA -from pydantic import BaseModel, ValidationError, field_validator, model_validator +from pydantic import BaseModel, ValidationError, model_validator, field_validator from aleph.sdk.utils import bytes_from_hex @@ -76,7 +76,7 @@ def payload_must_be_hex(cls, value: bytes) -> bytes: return bytes_from_hex(value.decode()) @model_validator(mode="after") - def check_expiry(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": + def check_expiry(cls, values) -> Dict[str, bytes]: """Check that the token has not expired""" payload: bytes = values.payload content = SignedPubKeyPayload.model_validate_json(payload) @@ -88,7 +88,7 @@ def check_expiry(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": return values @model_validator(mode="after") - def check_signature(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": + def check_signature(cls, values: Dict[str, bytes]) -> Dict[str, bytes]: """Check that the signature is valid""" signature: bytes = values.signature payload: bytes = values.payload From be79a36568ee8f660e93801bc50707d6cf5c4763 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Tue, 15 Oct 2024 12:02:00 +0900 Subject: [PATCH 25/37] Fix: Linting tests did not pass: --- src/aleph/sdk/conf.py | 7 ++----- src/aleph/sdk/utils.py | 1 - tests/unit/aleph_vm_authentication.py | 7 +++---- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index c0df405e..b29bf4f7 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -5,11 +5,10 @@ from shutil import which from typing import ClassVar, Dict, List, Optional, Union -from pydantic_settings import BaseSettings - from aleph_message.models import Chain from aleph_message.models.execution.environment import HypervisorType from pydantic import BaseModel, ConfigDict, Field +from pydantic_settings import BaseSettings from aleph.sdk.types import ChainInfo @@ -223,9 +222,7 @@ class Settings(BaseSettings): DNS_RESOLVERS: ClassVar[List[str]] = ["9.9.9.9", "1.1.1.1"] model_config = ConfigDict( - env_prefix="ALEPH_", - case_sensitive=False, - env_file=".env" + env_prefix="ALEPH_", case_sensitive=False, env_file=".env" ) diff --git a/src/aleph/sdk/utils.py b/src/aleph/sdk/utils.py index 80b2be58..e933440a 100644 --- a/src/aleph/sdk/utils.py +++ b/src/aleph/sdk/utils.py @@ -63,7 +63,6 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from jwcrypto.jwa import JWA -import pydantic_core from aleph.sdk.conf import settings from aleph.sdk.types import GenericMessage, SEVInfo, SEVMeasurement diff --git a/tests/unit/aleph_vm_authentication.py b/tests/unit/aleph_vm_authentication.py index fd65be0c..ea2f3c17 100644 --- a/tests/unit/aleph_vm_authentication.py +++ b/tests/unit/aleph_vm_authentication.py @@ -15,7 +15,7 @@ from eth_account.messages import encode_defunct from jwcrypto import jwk from jwcrypto.jwa import JWA -from pydantic import BaseModel, ValidationError, model_validator, field_validator +from pydantic import BaseModel, ValidationError, field_validator, model_validator from aleph.sdk.utils import bytes_from_hex @@ -76,7 +76,7 @@ def payload_must_be_hex(cls, value: bytes) -> bytes: return bytes_from_hex(value.decode()) @model_validator(mode="after") - def check_expiry(cls, values) -> Dict[str, bytes]: + def check_expiry(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": """Check that the token has not expired""" payload: bytes = values.payload content = SignedPubKeyPayload.model_validate_json(payload) @@ -88,8 +88,7 @@ def check_expiry(cls, values) -> Dict[str, bytes]: return values @model_validator(mode="after") - def check_signature(cls, values: Dict[str, bytes]) -> Dict[str, bytes]: - """Check that the signature is valid""" + def check_signature(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": signature: bytes = values.signature payload: bytes = values.payload content = SignedPubKeyPayload.model_validate_json(payload) From 322de8a7e582bfb0adf4fdfa4f8dcfa73c0a2636 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Tue, 5 Nov 2024 23:00:54 +0900 Subject: [PATCH 26/37] fix: Wrong aleph-message version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f3384155..6b084542 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "base58==2.1.1", # Needed now as default with _load_account changement "coincurve; python_version<'3.11'", "coincurve>=19.0.0; python_version>='3.11'", - "eth-abi>=4.0.O; python_version>='3.11'", + "eth-abi>=4.0.0; python_version>='3.11'", "eth-typing==4.3.1", "jwcrypto==1.5.6", "pydantic-settings>=2", From bbbdb3f172b7b9dc4b9ee135af8172297d9afc99 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Fri, 29 Nov 2024 22:08:27 +0900 Subject: [PATCH 27/37] fix: Hugo comments --- tests/unit/aleph_vm_authentication.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/aleph_vm_authentication.py b/tests/unit/aleph_vm_authentication.py index ea2f3c17..aa5c49c5 100644 --- a/tests/unit/aleph_vm_authentication.py +++ b/tests/unit/aleph_vm_authentication.py @@ -76,7 +76,7 @@ def payload_must_be_hex(cls, value: bytes) -> bytes: return bytes_from_hex(value.decode()) @model_validator(mode="after") - def check_expiry(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": + def check_expiry(cls, values: SignedPubKeyHeader) -> SignedPubKeyHeader: """Check that the token has not expired""" payload: bytes = values.payload content = SignedPubKeyPayload.model_validate_json(payload) @@ -88,7 +88,7 @@ def check_expiry(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": return values @model_validator(mode="after") - def check_signature(cls, values: "SignedPubKeyHeader") -> "SignedPubKeyHeader": + def check_signature(cls, values: SignedPubKeyHeader) -> SignedPubKeyHeader: signature: bytes = values.signature payload: bytes = values.payload content = SignedPubKeyPayload.model_validate_json(payload) From f09f346467f84e4910006dab78bf9d5a0a602e10 Mon Sep 17 00:00:00 2001 From: philogicae <38438271+philogicae@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:09:46 +0200 Subject: [PATCH 28/37] Add pydantic for better mypy tests + Fixes --- src/aleph/sdk/conf.py | 8 ++++---- tests/unit/aleph_vm_authentication.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/aleph/sdk/conf.py b/src/aleph/sdk/conf.py index b29bf4f7..2d3a03e1 100644 --- a/src/aleph/sdk/conf.py +++ b/src/aleph/sdk/conf.py @@ -7,8 +7,8 @@ from aleph_message.models import Chain from aleph_message.models.execution.environment import HypervisorType -from pydantic import BaseModel, ConfigDict, Field -from pydantic_settings import BaseSettings +from pydantic import BaseModel, Field +from pydantic_settings import BaseSettings, SettingsConfigDict from aleph.sdk.types import ChainInfo @@ -221,7 +221,7 @@ class Settings(BaseSettings): DNS_STATIC_DOMAIN: ClassVar[str] = "static.public.aleph.sh" DNS_RESOLVERS: ClassVar[List[str]] = ["9.9.9.9", "1.1.1.1"] - model_config = ConfigDict( + model_config = SettingsConfigDict( env_prefix="ALEPH_", case_sensitive=False, env_file=".env" ) @@ -234,7 +234,7 @@ class MainConfiguration(BaseModel): path: Path chain: Chain - model_config = ConfigDict(use_enum_values=True) + model_config = SettingsConfigDict(use_enum_values=True) # Settings singleton diff --git a/tests/unit/aleph_vm_authentication.py b/tests/unit/aleph_vm_authentication.py index aa5c49c5..c1710c16 100644 --- a/tests/unit/aleph_vm_authentication.py +++ b/tests/unit/aleph_vm_authentication.py @@ -75,7 +75,7 @@ def payload_must_be_hex(cls, value: bytes) -> bytes: """Convert the payload from hexadecimal to bytes""" return bytes_from_hex(value.decode()) - @model_validator(mode="after") + @model_validator(mode="after") # type: ignore def check_expiry(cls, values: SignedPubKeyHeader) -> SignedPubKeyHeader: """Check that the token has not expired""" payload: bytes = values.payload @@ -87,7 +87,7 @@ def check_expiry(cls, values: SignedPubKeyHeader) -> SignedPubKeyHeader: return values - @model_validator(mode="after") + @model_validator(mode="after") # type: ignore def check_signature(cls, values: SignedPubKeyHeader) -> SignedPubKeyHeader: signature: bytes = values.signature payload: bytes = values.payload From cf206562de2c53b2ca5b76919a5e3c3ee4d8b28a Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Wed, 15 Jan 2025 11:24:32 +0900 Subject: [PATCH 29/37] fix: Changing version of aleph-message --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 6b084542..7029a6d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,6 +134,7 @@ dependencies = [ "httpx", "secp256k1", ] + [tool.hatch.envs.testing.scripts] test = "pytest {args:} ./src/ ./tests/ ./examples/" test-cov = "pytest --cov {args:} ./src/ ./tests/ ./examples/" From 10f01cd0fca6b9411ca79ac7105e0154ff388958 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Wed, 15 Jan 2025 11:27:22 +0900 Subject: [PATCH 30/37] style: Missing type for URL --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7029a6d0..6b084542 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,7 +134,6 @@ dependencies = [ "httpx", "secp256k1", ] - [tool.hatch.envs.testing.scripts] test = "pytest {args:} ./src/ ./tests/ ./examples/" test-cov = "pytest --cov {args:} ./src/ ./tests/ ./examples/" From 1db00a819ebd03014ef180718bda0471c82ac13d Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Tue, 21 Jan 2025 20:11:24 +0900 Subject: [PATCH 31/37] fix: Changing version of aleph-message and fix mypy Changing the version from the branch to the main of aleph-message mypy rose some errors about missing name argument, so setting the as None because they are optional --- src/aleph/sdk/types.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/aleph/sdk/types.py b/src/aleph/sdk/types.py index cf23f19d..3f9781e3 100644 --- a/src/aleph/sdk/types.py +++ b/src/aleph/sdk/types.py @@ -100,3 +100,7 @@ class TokenType(str, Enum): GAS = "GAS" ALEPH = "ALEPH" + filename: Optional[str] = Field(default=None) + hash: Optional[str] = Field(default=None) + url: Optional[str] = Field(default=None) + error: Optional[str] = Field(default=None) From 3a1822750f7ac1a8007d9f78ceb5dd603bd0b054 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Tue, 21 Jan 2025 22:13:24 +0900 Subject: [PATCH 32/37] fix: Changing version of aleph-message --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6b084542..ed7a4e8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,7 @@ optional-dependencies.substrate = [ "substrate-interface", ] optional-dependencies.tezos = [ - "aleph-pytezos==3.13.4", + "aleph-pytezos==0.1.1", "pynacl", ] urls.Documentation = "https://aleph.im/" From dd80a664f522779c5d56824f0851fee6e447e67d Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Tue, 25 Feb 2025 23:59:31 +0900 Subject: [PATCH 33/37] Fix: Missing pydantic_core and wrong version of tezos --- pyproject.toml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ed7a4e8c..7bc3cc55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,15 +34,16 @@ dependencies = [ "aleph-superfluid>=0.2.1", "base58==2.1.1", # Needed now as default with _load_account changement "coincurve; python_version<'3.11'", - "coincurve>=19.0.0; python_version>='3.11'", - "eth-abi>=4.0.0; python_version>='3.11'", + "coincurve>=19; python_version>='3.11'", + "eth-abi>=4; python_version>='3.11'", "eth-typing==4.3.1", "jwcrypto==1.5.6", + "pydantic-core>=2", "pydantic-settings>=2", "pynacl==1.5", # Needed now as default with _load_account changement "python-magic", "typing-extensions", - "web3==6.3.0", + "web3==6.3", ] optional-dependencies.all = [ @@ -81,7 +82,7 @@ optional-dependencies.substrate = [ "substrate-interface", ] optional-dependencies.tezos = [ - "aleph-pytezos==0.1.1", + "aleph-pytezos==3.13.4", "pynacl", ] urls.Documentation = "https://aleph.im/" @@ -155,6 +156,7 @@ dependencies = [ "ruff==0.4.8", "isort==5.13.2", "pyproject-fmt==2.2.1", + "pydantic-core>=2", "pydantic-settings>=2", ] [tool.hatch.envs.linting.scripts] From 1524a3edd8fa412d5910c47926804d6c70e2ddf3 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Tue, 25 Feb 2025 23:59:49 +0900 Subject: [PATCH 34/37] Fix: Access to PersistentVolumeSizeMib is incompatible after migrating to Pydantic2 Using model_validate to access it --- src/aleph/sdk/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/aleph/sdk/utils.py b/src/aleph/sdk/utils.py index e933440a..6e01d49c 100644 --- a/src/aleph/sdk/utils.py +++ b/src/aleph/sdk/utils.py @@ -28,6 +28,7 @@ from uuid import UUID from zipfile import BadZipFile, ZipFile +import pydantic_core from aleph_message.models import ( Chain, InstanceContent, @@ -504,7 +505,9 @@ def make_instance_content( ref=ItemHash(rootfs), use_latest=True, ), - size_mib=PersistentVolumeSizeMib(rootfs_size), + size_mib=PersistentVolumeSizeMib.model_validate( + {"size_mib": rootfs_size} + ).size_mib, persistence=VolumePersistence.host, ), volumes=[parse_volume(volume) for volume in volumes], From 0a2cff2d0df6053cc1d33c49cfed767d594d728e Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Wed, 26 Feb 2025 00:04:05 +0900 Subject: [PATCH 35/37] Fix: Wrong name given to the variable --- tests/unit/test_price.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_price.py b/tests/unit/test_price.py index 830fb931..b596c33b 100644 --- a/tests/unit/test_price.py +++ b/tests/unit/test_price.py @@ -11,11 +11,11 @@ async def test_get_program_price_valid(): Test that the get_program_price method returns the correct PriceResponse when given a valid item hash. """ - expected = PriceResponse( - required_tokens=3.0555555555555556e-06, - payment_type="superfluid", - ) - mock_session = make_mock_get_session(expected.dict()) + expected_response = { + "required_tokens": 3.0555555555555556e-06, + "payment_type": "superfluid", + } + mock_session = make_mock_get_session(expected_response) async with mock_session: response = await mock_session.get_program_price("cacacacacacaca") assert response == PriceResponse(**expected_response) # type: ignore From 2af612f054c97541e7ab1c0e7d9939e7f4509d64 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Wed, 26 Feb 2025 00:19:20 +0900 Subject: [PATCH 36/37] Style: isort --- src/aleph/sdk/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aleph/sdk/utils.py b/src/aleph/sdk/utils.py index a1d1aa43..fbac4758 100644 --- a/src/aleph/sdk/utils.py +++ b/src/aleph/sdk/utils.py @@ -27,8 +27,8 @@ ) from uuid import UUID from zipfile import BadZipFile, ZipFile -import pydantic_core +import pydantic_core from aleph_message.models import ( Chain, InstanceContent, From 6ded6577c296dfe10b1a34cb3b2b134951bf4224 Mon Sep 17 00:00:00 2001 From: Antonyjin Date: Fri, 14 Mar 2025 23:54:39 +0900 Subject: [PATCH 37/37] Fix: PersistentVolumeSizeMib no longer exist This class has been deleted from aleph_message and the size is now inside the PersistentVolume class --- src/aleph/sdk/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aleph/sdk/utils.py b/src/aleph/sdk/utils.py index fbac4758..1c7c4ee9 100644 --- a/src/aleph/sdk/utils.py +++ b/src/aleph/sdk/utils.py @@ -57,7 +57,7 @@ from aleph_message.models.execution.volume import ( MachineVolume, ParentVolume, - PersistentVolumeSizeMib, + PersistentVolume, VolumePersistence, ) from aleph_message.utils import Mebibytes @@ -503,7 +503,7 @@ def make_instance_content( ref=ItemHash(rootfs), use_latest=True, ), - size_mib=PersistentVolumeSizeMib.model_validate( + size_mib=PersistentVolume.model_validate( {"size_mib": rootfs_size} ).size_mib, persistence=VolumePersistence.host,