Skip to content

Commit a409433

Browse files
committed
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.
1 parent 2790df9 commit a409433

File tree

14 files changed

+95
-89
lines changed

14 files changed

+95
-89
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ dependencies = [
2929
"eth_abi>=4.0.0; python_version>=\"3.11\"",
3030
"jwcrypto==1.5.6",
3131
"python-magic",
32+
"pydantic-settings",
3233
"typing_extensions",
3334
"aioresponses>=0.7.6",
3435
"aleph-superfluid>=0.2.1",

src/aleph/sdk/chains/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ async def sign_message(self, message: Dict) -> Dict:
7272
"""
7373
message = self._setup_sender(message)
7474
signature = await self.sign_raw(get_verification_buffer(message))
75-
message["signature"] = signature.hex()
75+
message["signature"] = "0x" + signature.hex()
7676
return message
7777

7878
@abstractmethod

src/aleph/sdk/chains/remote.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ async def from_crypto_host(
5252
session = aiohttp.ClientSession(connector=connector)
5353

5454
async with session.get(f"{host}/properties") as response:
55-
response.raise_for_status()
55+
await response.raise_for_status()
5656
data = await response.json()
5757
properties = AccountProperties(**data)
5858

@@ -75,7 +75,7 @@ def private_key(self):
7575
async def sign_message(self, message: Dict) -> Dict:
7676
"""Sign a message inplace."""
7777
async with self._session.post(f"{self._host}/sign", json=message) as response:
78-
response.raise_for_status()
78+
await response.raise_for_status()
7979
return await response.json()
8080

8181
async def sign_raw(self, buffer: bytes) -> bytes:

src/aleph/sdk/client/authenticated_http.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ async def _broadcast(
259259
url = "/api/v0/messages"
260260
logger.debug(f"Posting message on {url}")
261261

262-
message_dict = message.dict(include=self.BROADCAST_MESSAGE_FIELDS)
262+
message_dict = message.model_dump(include=self.BROADCAST_MESSAGE_FIELDS)
263263
async with self.http_session.post(
264264
url,
265265
json={
@@ -301,7 +301,7 @@ async def create_post(
301301
)
302302

303303
message, status, _ = await self.submit(
304-
content=content.dict(exclude_none=True),
304+
content=content.model_dump(exclude_none=True),
305305
message_type=MessageType.post,
306306
channel=channel,
307307
allow_inlining=inline,
@@ -329,7 +329,7 @@ async def create_aggregate(
329329
)
330330

331331
message, status, _ = await self.submit(
332-
content=content_.dict(exclude_none=True),
332+
content=content_.model_dump(exclude_none=True),
333333
message_type=MessageType.aggregate,
334334
channel=channel,
335335
allow_inlining=inline,
@@ -403,7 +403,7 @@ async def create_store(
403403
content = StoreContent(**values)
404404

405405
message, status, _ = await self.submit(
406-
content=content.dict(exclude_none=True),
406+
content=content.model_dump(exclude_none=True),
407407
message_type=MessageType.store,
408408
channel=channel,
409409
allow_inlining=True,
@@ -496,7 +496,7 @@ async def create_program(
496496
assert content.on.persistent == persistent
497497

498498
message, status, _ = await self.submit(
499-
content=content.dict(exclude_none=True),
499+
content=content.model_dump(exclude_none=True),
500500
message_type=MessageType.program,
501501
channel=channel,
502502
storage_engine=storage_engine,
@@ -572,7 +572,7 @@ async def create_instance(
572572
payment=payment,
573573
)
574574
message, status, response = await self.submit(
575-
content=content.dict(exclude_none=True),
575+
content=content.model_dump(exclude_none=True),
576576
message_type=MessageType.instance,
577577
channel=channel,
578578
storage_engine=storage_engine,
@@ -618,7 +618,7 @@ async def forget(
618618
)
619619

620620
message, status, _ = await self.submit(
621-
content=content.dict(exclude_none=True),
621+
content=content.model_dump(exclude_none=True),
622622
message_type=MessageType.forget,
623623
channel=channel,
624624
storage_engine=storage_engine,
@@ -662,11 +662,11 @@ async def _storage_push_file_with_message(
662662
# Prepare the STORE message
663663
message = await self.generate_signed_message(
664664
message_type=MessageType.store,
665-
content=store_content.dict(exclude_none=True),
665+
content=store_content.model_dump(exclude_none=True),
666666
channel=channel,
667667
)
668668
metadata = {
669-
"message": message.dict(exclude_none=True),
669+
"message": message.model_dump(exclude_none=True),
670670
"sync": sync,
671671
}
672672
data.add_field(

src/aleph/sdk/client/http.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ async def get_posts(
180180
posts: List[Post] = []
181181
for post_raw in posts_raw:
182182
try:
183-
posts.append(Post.parse_obj(post_raw))
183+
posts.append(Post.model_validate(post_raw))
184184
except ValidationError as e:
185185
if not ignore_invalid_messages:
186186
raise e

src/aleph/sdk/client/vm_confidential_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ async def measurement(self, vm_id: ItemHash) -> SEVMeasurement:
105105
status, text = await self.perform_operation(
106106
vm_id, "confidential/measurement", method="GET"
107107
)
108-
sev_measurement = SEVMeasurement.parse_raw(text)
108+
sev_measurement = SEVMeasurement.model_validate_json(text)
109109
return sev_measurement
110110

111111
async def validate_measure(

src/aleph/sdk/conf.py

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
import os
44
from pathlib import Path
55
from shutil import which
6-
from typing import Dict, Optional, Union
6+
from typing import ClassVar, Dict, Optional, Union
7+
8+
from pydantic_settings import BaseSettings
79

810
from aleph_message.models import Chain
911
from aleph_message.models.execution.environment import HypervisorType
10-
from pydantic import BaseModel, BaseSettings, Field
12+
from pydantic import BaseModel, ConfigDict, Field
1113

1214
from aleph.sdk.types import ChainInfo
1315

@@ -41,7 +43,7 @@ class Settings(BaseSettings):
4143
REMOTE_CRYPTO_HOST: Optional[str] = None
4244
REMOTE_CRYPTO_UNIX_SOCKET: Optional[str] = None
4345
ADDRESS_TO_USE: Optional[str] = None
44-
HTTP_REQUEST_TIMEOUT = 10.0
46+
HTTP_REQUEST_TIMEOUT: ClassVar[float] = 10.0
4547

4648
DEFAULT_CHANNEL: str = "ALEPH-CLOUDSOLUTIONS"
4749
DEFAULT_RUNTIME_ID: str = (
@@ -83,12 +85,12 @@ class Settings(BaseSettings):
8385

8486
CODE_USES_SQUASHFS: bool = which("mksquashfs") is not None # True if command exists
8587

86-
VM_URL_PATH = "https://aleph.sh/vm/{hash}"
87-
VM_URL_HOST = "https://{hash_base32}.aleph.sh"
88+
VM_URL_PATH: ClassVar[str] = "https://aleph.sh/vm/{hash}"
89+
VM_URL_HOST: ClassVar[str] = "https://{hash_base32}.aleph.sh"
8890

8991
# Web3Provider settings
90-
TOKEN_DECIMALS = 18
91-
TX_TIMEOUT = 60 * 3
92+
TOKEN_DECIMALS: ClassVar[int] = 18
93+
TX_TIMEOUT: ClassVar[int] = 60 * 3
9294
CHAINS: Dict[Union[Chain, str], ChainInfo] = {
9395
# TESTNETS
9496
"SEPOLIA": ChainInfo(
@@ -124,28 +126,29 @@ class Settings(BaseSettings):
124126
),
125127
}
126128
# Add all placeholders to allow easy dynamic setup of CHAINS
127-
CHAINS_SEPOLIA_ACTIVE: Optional[bool]
128-
CHAINS_ETH_ACTIVE: Optional[bool]
129-
CHAINS_AVAX_ACTIVE: Optional[bool]
130-
CHAINS_BASE_ACTIVE: Optional[bool]
131-
CHAINS_BSC_ACTIVE: Optional[bool]
132-
CHAINS_SEPOLIA_RPC: Optional[str]
133-
CHAINS_ETH_RPC: Optional[str]
134-
CHAINS_AVAX_RPC: Optional[str]
135-
CHAINS_BASE_RPC: Optional[str]
136-
CHAINS_BSC_RPC: Optional[str]
129+
CHAINS_SEPOLIA_ACTIVE: Optional[bool] = None
130+
CHAINS_ETH_ACTIVE: Optional[bool] = None
131+
CHAINS_AVAX_ACTIVE: Optional[bool] = None
132+
CHAINS_BASE_ACTIVE: Optional[bool] = None
133+
CHAINS_BSC_ACTIVE: Optional[bool] = None
134+
CHAINS_SEPOLIA_RPC: Optional[str] = None
135+
CHAINS_ETH_RPC: Optional[str] = None
136+
CHAINS_AVAX_RPC: Optional[str] = None
137+
CHAINS_BASE_RPC: Optional[str] = None
138+
CHAINS_BSC_RPC: Optional[str] = None
137139

138140
# Dns resolver
139-
DNS_IPFS_DOMAIN = "ipfs.public.aleph.sh"
140-
DNS_PROGRAM_DOMAIN = "program.public.aleph.sh"
141-
DNS_INSTANCE_DOMAIN = "instance.public.aleph.sh"
142-
DNS_STATIC_DOMAIN = "static.public.aleph.sh"
143-
DNS_RESOLVERS = ["9.9.9.9", "1.1.1.1"]
144-
145-
class Config:
146-
env_prefix = "ALEPH_"
147-
case_sensitive = False
148-
env_file = ".env"
141+
DNS_IPFS_DOMAIN: ClassVar[str] = "ipfs.public.aleph.sh"
142+
DNS_PROGRAM_DOMAIN: ClassVar[str] = "program.public.aleph.sh"
143+
DNS_INSTANCE_DOMAIN: ClassVar[str] = "instance.public.aleph.sh"
144+
DNS_STATIC_DOMAIN: ClassVar[str] = "static.public.aleph.sh"
145+
DNS_RESOLVERS: ClassVar[str] = ["9.9.9.9", "1.1.1.1"]
146+
147+
model_config = ConfigDict(
148+
env_prefix="ALEPH_",
149+
case_sensitive=False,
150+
env_file=".env"
151+
)
149152

150153

151154
class MainConfiguration(BaseModel):
@@ -156,8 +159,7 @@ class MainConfiguration(BaseModel):
156159
path: Path
157160
chain: Chain
158161

159-
class Config:
160-
use_enum_values = True
162+
model_config = ConfigDict(use_enum_values = True)
161163

162164

163165
# Settings singleton
@@ -213,7 +215,7 @@ def save_main_configuration(file_path: Path, data: MainConfiguration):
213215
Synchronously save a single ChainAccount object as JSON to a file.
214216
"""
215217
with file_path.open("w") as file:
216-
data_serializable = data.dict()
218+
data_serializable = data.model_dump()
217219
data_serializable["path"] = str(data_serializable["path"])
218220
json.dump(data_serializable, file, indent=4)
219221

src/aleph/sdk/query/responses.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
ItemType,
1010
MessageConfirmation,
1111
)
12-
from pydantic import BaseModel, Field
12+
from pydantic import BaseModel, ConfigDict, Field
1313

1414

1515
class Post(BaseModel):
@@ -48,9 +48,9 @@ class Post(BaseModel):
4848
ref: Optional[Union[str, Any]] = Field(
4949
description="Other message referenced by this one"
5050
)
51+
address: Optional[str] = Field(description="Address of the sender")
5152

52-
class Config:
53-
allow_extra = False
53+
model_config = ConfigDict(extra="forbid")
5454

5555

5656
class PaginationResponse(BaseModel):
@@ -64,14 +64,14 @@ class PostsResponse(PaginationResponse):
6464
"""Response from an aleph.im node API on the path /api/v0/posts.json"""
6565

6666
posts: List[Post]
67-
pagination_item = "posts"
67+
pagination_item: str = "posts"
6868

6969

7070
class MessagesResponse(PaginationResponse):
7171
"""Response from an aleph.im node API on the path /api/v0/messages.json"""
7272

7373
messages: List[AlephMessage]
74-
pagination_item = "messages"
74+
pagination_item: str = "messages"
7575

7676

7777
class PriceResponse(BaseModel):

src/aleph/sdk/utils.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@
2828
from uuid import UUID
2929
from zipfile import BadZipFile, ZipFile
3030

31+
from pydantic import BaseModel
32+
3133
from aleph_message.models import ItemHash, MessageType
3234
from aleph_message.models.execution.program import Encoding
3335
from aleph_message.models.execution.volume import MachineVolume
3436
from cryptography.hazmat.backends import default_backend
3537
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
3638
from jwcrypto.jwa import JWA
37-
from pydantic.json import pydantic_encoder
39+
import pydantic_core
3840

3941
from aleph.sdk.conf import settings
4042
from aleph.sdk.types import GenericMessage, SEVInfo, SEVMeasurement
@@ -173,7 +175,7 @@ def extended_json_encoder(obj: Any) -> Any:
173175
elif isinstance(obj, time):
174176
return obj.hour * 3600 + obj.minute * 60 + obj.second + obj.microsecond / 1e6
175177
else:
176-
return pydantic_encoder(obj)
178+
return pydantic_core.to_jsonable_python(obj)
177179

178180

179181
def parse_volume(volume_dict: Union[Mapping, MachineVolume]) -> MachineVolume:
@@ -185,7 +187,7 @@ def parse_volume(volume_dict: Union[Mapping, MachineVolume]) -> MachineVolume:
185187
return volume_dict
186188
for volume_type in get_args(MachineVolume):
187189
try:
188-
return volume_type.parse_obj(volume_dict)
190+
return volume_type.model_validate(volume_dict)
189191
except ValueError:
190192
continue
191193
else:

0 commit comments

Comments
 (0)