Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migration to Pydantic V2 #115

Merged
merged 27 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4e4738e
feat: Upgrade the pydantic version
Antonyjin Sep 19, 2024
21a27a9
Fix: Refactor ItemHash to align with Pydantic v2 validation schema.
Antonyjin Sep 27, 2024
2404dfe
Fix: Refactor classes and functions to align with Pydantic v2.
Antonyjin Sep 27, 2024
7e26422
Fix: Refactor to use `model_dump` and `model_dump_json` in place of d…
Antonyjin Sep 27, 2024
e328fa9
Refactor to use `model_dump` and `model_dump_json`, and update `check…
Antonyjin Sep 27, 2024
c699881
Fix: Changes in files to comply with MyPy, Ruff, and Black
Antonyjin Oct 2, 2024
058b2f5
Fix: Comply with Black
Antonyjin Oct 2, 2024
de53838
Fix: Missing field caused by pydantic v2
Antonyjin Oct 4, 2024
7aa06f3
Fix: Linter test did not pass and suggestion from Hugo
Antonyjin Oct 7, 2024
3b33384
Fix: Replacing print by logger and remove unecessary workaround
Antonyjin Oct 14, 2024
176ecb4
Refactor use of field size constraints
hoh Oct 16, 2024
d73432a
Cleanup with black, isort
hoh Oct 16, 2024
8c326e8
Fix: Simplify model content validation
hoh Oct 16, 2024
01087fd
del: Deleting setup.py, now using pyproject.toml
Antonyjin Feb 25, 2025
ae27065
Fix: Missing dependencies after migrating to pydantic v2
Antonyjin Feb 25, 2025
8b9264f
Style: MyPy raised note about untyped function
Antonyjin Feb 25, 2025
7066c2d
Fix: Refactor ItemHash for Pydantic v2 compatibility
Antonyjin Feb 25, 2025
2250d2a
Fix: Refactoring PersistentVolumeSizeMib for Pydantic2 compatibility
Antonyjin Feb 25, 2025
9e105ad
Fix: Some parts of InstanceContent were incompatible with pydantic2
Antonyjin Feb 25, 2025
05dde6d
Fix: Extra forbid don't work the same on pydantic2, refactoring the r…
Antonyjin Feb 25, 2025
2a3c599
Fix: Fixing test to match Pydantic v2 error format
Antonyjin Feb 25, 2025
6806fbc
Fix: Missing field in InstantContent class
Antonyjin Feb 25, 2025
cf2bf32
Fix: Increasing size for instances and volumes
Antonyjin Mar 14, 2025
b2d9572
fix: Adapt the tests after the increase of volumes and instances size
Antonyjin Mar 14, 2025
ad9c940
style: ruff and black
Antonyjin Mar 14, 2025
ae16330
Fix: Re-add Volume sizes classes as types instead removing it.
nesitor Apr 7, 2025
b4e622c
Fix: Removed `basicConfig` call.
nesitor Apr 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 56 additions & 47 deletions aleph_message/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import datetime
import json
import logging
from copy import copy
from hashlib import sha256
from json import JSONDecodeError
from pathlib import Path
from typing import Any, Dict, List, Literal, Optional, Type, TypeVar, Union, cast

from pydantic import BaseModel, Extra, Field, validator
from pydantic import BaseModel, ConfigDict, Field, field_validator
from typing_extensions import TypeAlias

from .abstract import BaseContent, HashableModel
Expand All @@ -16,6 +17,9 @@
from .execution.program import ProgramContent
from .item_hash import ItemHash, ItemType

logger = logging.getLogger(__name__)


__all__ = [
"AggregateContent",
"AggregateMessage",
Expand Down Expand Up @@ -54,8 +58,7 @@ class MongodbId(BaseModel):

oid: str = Field(alias="$oid")

class Config:
extra = Extra.forbid
model_config = ConfigDict(extra="forbid")


class ChainRef(BaseModel):
Expand All @@ -76,8 +79,7 @@ class MessageConfirmationHash(BaseModel):
binary: str = Field(alias="$binary")
type: str = Field(alias="$type")

class Config:
extra = Extra.forbid
model_config = ConfigDict(extra="forbid")


class MessageConfirmation(BaseModel):
Expand All @@ -93,15 +95,13 @@ class MessageConfirmation(BaseModel):
default=None, description="The address that published the transaction."
)

class Config:
extra = Extra.forbid
model_config = ConfigDict(extra="forbid")


class AggregateContentKey(BaseModel):
name: str

class Config:
extra = Extra.forbid
model_config = ConfigDict(extra="forbid")


class PostContent(BaseContent):
Expand All @@ -116,16 +116,15 @@ class PostContent(BaseContent):
)
type: str = Field(description="User-generated 'content-type' of a POST message")

@validator("type")
@field_validator("type")
def check_type(cls, v, values):
if v == "amend":
ref = values.get("ref")
ref = values.data.get("ref")
if not ref:
raise ValueError("A 'ref' is required for POST type 'amend'")
return v

class Config:
extra = Extra.forbid
model_config = ConfigDict(extra="forbid")


class AggregateContent(BaseContent):
Expand All @@ -136,8 +135,7 @@ class AggregateContent(BaseContent):
)
content: Dict = Field(description="The content of an aggregate must be a dict")

class Config:
extra = Extra.forbid
model_config = ConfigDict(extra="forbid")


class StoreContent(BaseContent):
Expand All @@ -148,10 +146,11 @@ class StoreContent(BaseContent):
size: Optional[int] = None # Generated by the node on storage
content_type: Optional[str] = None # Generated by the node on storage
ref: Optional[str] = None
metadata: Optional[Dict[str, Any]] = Field(description="Metadata of the VM")
metadata: Optional[Dict[str, Any]] = Field(
default=None, description="Metadata of the VM"
)

class Config:
extra = Extra.allow
model_config = ConfigDict(extra="allow")


class ForgetContent(BaseContent):
Expand Down Expand Up @@ -214,9 +213,9 @@ class BaseMessage(BaseModel):

forgotten_by: Optional[List[str]]

@validator("item_content")
@field_validator("item_content")
def check_item_content(cls, v: Optional[str], values) -> Optional[str]:
item_type = values["item_type"]
item_type = values.data.get("item_type")
if v is None:
return None
elif item_type == ItemType.inline:
Expand All @@ -232,14 +231,14 @@ def check_item_content(cls, v: Optional[str], values) -> Optional[str]:
)
return v

@validator("item_hash")
@field_validator("item_hash")
def check_item_hash(cls, v: ItemHash, values) -> ItemHash:
item_type = values["item_type"]
item_type = values.data.get("item_type")
if item_type == ItemType.inline:
item_content: str = values["item_content"]
item_content: str = values.data.get("item_content")

# Double check that the hash function is supported
hash_type = values["hash_type"] or HashType.sha256
hash_type = values.data.get("hash_type") or HashType.sha256
assert hash_type.value == HashType.sha256

computed_hash: str = sha256(item_content.encode()).hexdigest()
Expand All @@ -255,49 +254,56 @@ def check_item_hash(cls, v: ItemHash, values) -> ItemHash:
assert item_type == ItemType.storage
return v

@validator("confirmed")
@field_validator("confirmed")
def check_confirmed(cls, v, values):
confirmations = values["confirmations"]
confirmations = values.data.get("confirmations")
if v is True and not bool(confirmations):
raise ValueError("Message cannot be 'confirmed' without 'confirmations'")
return v

@validator("time")
@field_validator("time")
def convert_float_to_datetime(cls, v, values):
if isinstance(v, float):
v = datetime.datetime.fromtimestamp(v)
assert isinstance(v, datetime.datetime)
return v

class Config:
extra = Extra.forbid
exclude = {"id_", "_id"}
model_config = ConfigDict(extra="forbid")

def custom_dump(self):
"""Exclude MongoDB identifiers from dumps for historical reasons."""
return self.model_dump(exclude={"id_", "_id"})


class PostMessage(BaseMessage):
"""Unique data posts (unique data points, events, ...)"""

type: Literal[MessageType.post]
content: PostContent
forgotten_by: Optional[List[str]] = None


class AggregateMessage(BaseMessage):
"""A key-value storage specific to an address"""

type: Literal[MessageType.aggregate]
content: AggregateContent
forgotten_by: Optional[list] = None


class StoreMessage(BaseMessage):
type: Literal[MessageType.store]
content: StoreContent
forgotten_by: Optional[list] = None
metadata: Optional[Dict[str, Any]] = None


class ForgetMessage(BaseMessage):
type: Literal[MessageType.forget]
content: ForgetContent
forgotten_by: Optional[list] = None

@validator("forgotten_by")
@field_validator("forgotten_by")
def cannot_be_forgotten(cls, v: Optional[List[str]], values) -> Optional[List[str]]:
assert values
if v:
Expand All @@ -308,25 +314,29 @@ def cannot_be_forgotten(cls, v: Optional[List[str]], values) -> Optional[List[st
class ProgramMessage(BaseMessage):
type: Literal[MessageType.program]
content: ProgramContent
forgotten_by: Optional[List[str]] = None

@validator("content")
@field_validator("content")
def check_content(cls, v, values):
item_type = values["item_type"]
"""Ensure that the content of the message is correctly formatted."""
item_type = values.data.get("item_type")
if item_type == ItemType.inline:
item_content = json.loads(values["item_content"])
if v.dict(exclude_none=True) != item_content:
# Print differences
vdict = v.dict(exclude_none=True)
for key, value in item_content.items():
if vdict[key] != value:
print(f"{key}: {vdict[key]} != {value}")
# Ensure that the content correct JSON
item_content = json.loads(values.data.get("item_content"))
# Ensure that the content matches the expected structure
if v.model_dump(exclude_none=True) != item_content:
logger.warning(
"Content and item_content differ for message %s",
values.data["item_hash"],
)
raise ValueError("Content and item_content differ")
return v


class InstanceMessage(BaseMessage):
type: Literal[MessageType.instance]
content: InstanceContent
forgotten_by: Optional[List[str]] = None


AlephMessage: TypeAlias = Union[
Expand Down Expand Up @@ -363,12 +373,12 @@ def parse_message(message_dict: Dict) -> AlephMessage:
message_class.__annotations__["type"].__args__[0]
)
if message_dict["type"] == message_type:
return message_class.parse_obj(message_dict)
return message_class.model_validate(message_dict)
else:
raise ValueError(f"Unknown message type {message_dict['type']}")


def add_item_content_and_hash(message_dict: Dict, inplace: bool = False):
def add_item_content_and_hash(message_dict: Dict, inplace: bool = False) -> Dict:
if not inplace:
message_dict = copy(message_dict)

Expand All @@ -390,7 +400,7 @@ def create_new_message(
"""
message_content = add_item_content_and_hash(message_dict)
if factory:
return cast(T, factory.parse_obj(message_content))
return cast(T, factory.model_validate(message_content))
else:
return cast(T, parse_message(message_content))

Expand All @@ -405,7 +415,7 @@ def create_message_from_json(
message_dict = json.loads(json_data)
message_content = add_item_content_and_hash(message_dict, inplace=True)
if factory:
return factory.parse_obj(message_content)
return factory.model_validate(message_content)
else:
return parse_message(message_content)

Expand All @@ -422,7 +432,7 @@ def create_message_from_file(
message_dict = decoder.load(fd)
message_content = add_item_content_and_hash(message_dict, inplace=True)
if factory:
return factory.parse_obj(message_content)
return factory.model_validate(message_content)
else:
return parse_message(message_content)

Expand All @@ -436,5 +446,4 @@ class MessagesResponse(BaseModel):
pagination_per_page: int
pagination_item: str

class Config:
extra = Extra.forbid
model_config = ConfigDict(extra="forbid")
5 changes: 2 additions & 3 deletions aleph_message/models/abstract.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel, Extra
from pydantic import BaseModel, ConfigDict


def hashable(obj):
Expand All @@ -24,5 +24,4 @@ class BaseContent(BaseModel):
address: str
time: float

class Config:
extra = Extra.forbid
model_config = ConfigDict(extra="forbid")
2 changes: 1 addition & 1 deletion aleph_message/models/execution/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Payment(HashableModel):

chain: Chain
"""Which chain to check for funds"""
receiver: Optional[str]
receiver: Optional[str] = None
"""Optional alternative address to send tokens to"""
type: PaymentType
"""Whether to pay by holding $ALEPH or by streaming tokens"""
Expand Down
Loading
Loading