diff --git a/hathor/conf/settings.py b/hathor/conf/settings.py index d135e0881..e7b5a509c 100644 --- a/hathor/conf/settings.py +++ b/hathor/conf/settings.py @@ -16,7 +16,7 @@ from enum import StrEnum, auto, unique from math import log from pathlib import Path -from typing import Any, NamedTuple, Optional, Union +from typing import NamedTuple, Optional, Union import pydantic @@ -559,43 +559,36 @@ def parse_hex_str(hex_str: Union[str, bytes]) -> bytes: return bytes.fromhex(hex_str.lstrip('x')) if not isinstance(hex_str, bytes): - raise TypeError(f'expected \'str\' or \'bytes\', got {hex_str}') + raise ValueError(f'expected \'str\' or \'bytes\', got {hex_str}') return hex_str -def _validate_consensus_algorithm(consensus_algorithm: ConsensusSettings, values: dict[str, Any]) -> ConsensusSettings: +def _validate_consensus_algorithm(model: HathorSettings) -> HathorSettings: """Validate that if Proof-of-Authority is enabled, block rewards must not be set.""" + consensus_algorithm = model.CONSENSUS_ALGORITHM if consensus_algorithm.is_pow(): - return consensus_algorithm + return model - assert consensus_algorithm.is_poa() - blocks_per_halving = values.get('BLOCKS_PER_HALVING') - initial_token_units_per_block = values.get('INITIAL_TOKEN_UNITS_PER_BLOCK') - minimum_token_units_per_block = values.get('MINIMUM_TOKEN_UNITS_PER_BLOCK') - assert initial_token_units_per_block is not None, 'INITIAL_TOKEN_UNITS_PER_BLOCK must be set' - assert minimum_token_units_per_block is not None, 'MINIMUM_TOKEN_UNITS_PER_BLOCK must be set' - - if blocks_per_halving is not None or initial_token_units_per_block != 0 or minimum_token_units_per_block != 0: + if (model.BLOCKS_PER_HALVING is not None or + model.INITIAL_TOKEN_UNITS_PER_BLOCK != 0 or + model.MINIMUM_TOKEN_UNITS_PER_BLOCK != 0): raise ValueError('PoA networks do not support block rewards') - - return consensus_algorithm + return model -def _validate_tokens(genesis_tokens: int, values: dict[str, Any]) -> int: +def _validate_tokens(model: HathorSettings) -> HathorSettings: """Validate genesis tokens.""" - genesis_token_units = values.get('GENESIS_TOKEN_UNITS') - decimal_places = values.get('DECIMAL_PLACES') - assert genesis_token_units is not None, 'GENESIS_TOKEN_UNITS must be set' - assert decimal_places is not None, 'DECIMAL_PLACES must be set' + genesis_tokens = model.GENESIS_TOKENS + genesis_token_units = model.GENESIS_TOKEN_UNITS + decimal_places = model.DECIMAL_PLACES if genesis_tokens != genesis_token_units * (10 ** decimal_places): raise ValueError( - f'invalid tokens: GENESIS_TOKENS={genesis_tokens}, GENESIS_TOKEN_UNITS={genesis_token_units}, ' - f'DECIMAL_PLACES={decimal_places}', + f'invalid tokens: GENESIS_TOKENS={genesis_tokens}, ' + f'GENESIS_TOKEN_UNITS={genesis_token_units}, DECIMAL_PLACES={decimal_places}' ) - - return genesis_tokens + return model def _validate_token_deposit_percentage(token_deposit_percentage: float) -> float: @@ -610,43 +603,43 @@ def _validate_token_deposit_percentage(token_deposit_percentage: float) -> float _VALIDATORS = dict( - _parse_hex_str=pydantic.validator( + _parse_hex_str=pydantic.field_validator( 'P2PKH_VERSION_BYTE', 'MULTISIG_VERSION_BYTE', 'GENESIS_OUTPUT_SCRIPT', 'GENESIS_BLOCK_HASH', 'GENESIS_TX1_HASH', 'GENESIS_TX2_HASH', - pre=True, - allow_reuse=True + mode='before', )(parse_hex_str), - _parse_soft_voided_tx_id=pydantic.validator( + _parse_soft_voided_tx_id=pydantic.field_validator( 'SOFT_VOIDED_TX_IDS', - pre=True, - allow_reuse=True, - each_item=True - )(parse_hex_str), - _parse_skipped_verification_tx_id=pydantic.validator( + mode='before', + )(lambda v: [parse_hex_str(x) for x in v] if isinstance(v, list) else v), + _parse_skipped_verification_tx_id=pydantic.field_validator( 'SKIP_VERIFICATION', - pre=True, - allow_reuse=True, - each_item=True - )(parse_hex_str), - _parse_checkpoints=pydantic.validator( + mode='before', + )(lambda v: [parse_hex_str(x) for x in v] if isinstance(v, list) else v), + _parse_checkpoints=pydantic.field_validator( 'CHECKPOINTS', - pre=True + mode='before', )(_parse_checkpoints), - _parse_blueprints=pydantic.validator( + _parse_blueprints=pydantic.field_validator( 'BLUEPRINTS', - pre=True + mode='before', )(_parse_blueprints), - _validate_consensus_algorithm=pydantic.validator( - 'CONSENSUS_ALGORITHM' + _validate_consensus_algorithm=pydantic.model_validator( + mode='after', )(_validate_consensus_algorithm), - _validate_tokens=pydantic.validator( - 'GENESIS_TOKENS' + _validate_tokens=pydantic.model_validator( + mode='after', )(_validate_tokens), - _validate_token_deposit_percentage=pydantic.validator( - 'TOKEN_DEPOSIT_PERCENTAGE' + _validate_token_deposit_percentage=pydantic.field_validator( + 'TOKEN_DEPOSIT_PERCENTAGE', + mode='after', )(_validate_token_deposit_percentage), + _parse_feature_activation=pydantic.field_validator( + 'FEATURE_ACTIVATION', + mode='before', + )(lambda v: FeatureActivationSettings.model_validate(v) if isinstance(v, dict) else v), ) diff --git a/hathor/consensus/consensus_settings.py b/hathor/consensus/consensus_settings.py index 0a78f4fcd..2cf0d1cbf 100644 --- a/hathor/consensus/consensus_settings.py +++ b/hathor/consensus/consensus_settings.py @@ -17,14 +17,14 @@ import hashlib from abc import ABC, abstractmethod from enum import Enum, unique -from typing import TYPE_CHECKING, Annotated, Any, Literal, TypeAlias +from typing import TYPE_CHECKING, Annotated, Any, Literal, TypeAlias, Union -from pydantic import Field, NonNegativeInt, PrivateAttr, validator +from pydantic import Discriminator, NonNegativeInt, PrivateAttr, Tag, field_validator, model_validator from typing_extensions import override from hathor.transaction import TxVersion from hathor.util import json_dumpb -from hathor.utils.pydantic import BaseModel +from hathor.utils.pydantic import BaseModel, Hex if TYPE_CHECKING: from hathor.conf.settings import HathorSettings @@ -97,34 +97,20 @@ def get_peer_hello_hash(self) -> str | None: class PoaSignerSettings(BaseModel): - public_key: bytes + public_key: Hex[bytes] start_height: NonNegativeInt = 0 end_height: NonNegativeInt | None = None - @validator('public_key', pre=True) - def _parse_hex_str(cls, hex_str: str | bytes) -> bytes: - from hathor.conf.settings import parse_hex_str - return parse_hex_str(hex_str) - - @validator('end_height') - def _validate_end_height(cls, end_height: int | None, values: dict[str, Any]) -> int | None: - start_height = values.get('start_height') - assert start_height is not None, 'start_height must be set' - - if end_height is None: - return None - - if end_height <= start_height: - raise ValueError(f'end_height ({end_height}) must be greater than start_height ({start_height})') - - return end_height + @model_validator(mode='after') + def _validate_end_height(self) -> 'PoaSignerSettings': + # Validate end_height > start_height after initialization + if self.end_height is not None and self.end_height <= self.start_height: + raise ValueError(f'end_height ({self.end_height}) must be greater than start_height ({self.start_height})') + return self def to_json_dict(self) -> dict[str, Any]: """Return this signer settings instance as a json dict.""" - json_dict = self.dict() - # TODO: We can use a custom serializer to convert bytes to hex when we update to Pydantic V2. - json_dict['public_key'] = self.public_key.hex() - return json_dict + return self.model_dump() class PoaSettings(_BaseConsensusSettings): @@ -133,7 +119,8 @@ class PoaSettings(_BaseConsensusSettings): # A list of Proof-of-Authority signer public keys that have permission to produce blocks. signers: tuple[PoaSignerSettings, ...] - @validator('signers') + @field_validator('signers', mode='after') + @classmethod def _validate_signers(cls, signers: tuple[PoaSignerSettings, ...]) -> tuple[PoaSignerSettings, ...]: if len(signers) == 0: raise ValueError('At least one signer must be provided in PoA networks') @@ -165,4 +152,10 @@ def _calculate_peer_hello_hash(self) -> str | None: return hashlib.sha256(data).digest().hex() -ConsensusSettings: TypeAlias = Annotated[PowSettings | PoaSettings, Field(discriminator='type')] +ConsensusSettings: TypeAlias = Annotated[ + Union[ + Annotated[PowSettings, Tag(ConsensusType.PROOF_OF_WORK)], + Annotated[PoaSettings, Tag(ConsensusType.PROOF_OF_AUTHORITY)], + ], + Discriminator('type') +] diff --git a/hathor/consensus/poa/poa_signer.py b/hathor/consensus/poa/poa_signer.py index 689ecee71..9065ea6f0 100644 --- a/hathor/consensus/poa/poa_signer.py +++ b/hathor/consensus/poa/poa_signer.py @@ -15,11 +15,11 @@ from __future__ import annotations import hashlib -from typing import TYPE_CHECKING, Any, NewType +from typing import TYPE_CHECKING, NewType from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec -from pydantic import Field, validator +from pydantic import ConfigDict, Field, field_validator, model_validator from hathor.consensus import poa from hathor.crypto.util import ( @@ -33,46 +33,52 @@ from hathor.transaction.poa import PoaBlock -class PoaSignerFile(BaseModel, arbitrary_types_allowed=True): +class PoaSignerFile(BaseModel): """Class that represents a Proof-of-Authority signer configuration file.""" + model_config = ConfigDict(arbitrary_types_allowed=True) + private_key: ec.EllipticCurvePrivateKeyWithSerialization = Field(alias='private_key_hex') public_key: ec.EllipticCurvePublicKey = Field(alias='public_key_hex') address: str - @validator('private_key', pre=True) + @field_validator('private_key', mode='before') + @classmethod def _parse_private_key(cls, private_key_hex: str) -> ec.EllipticCurvePrivateKeyWithSerialization: """Parse a private key hex into a private key instance.""" private_key_bytes = bytes.fromhex(private_key_hex) return get_private_key_from_bytes(private_key_bytes) - @validator('public_key', pre=True) - def _validate_public_key_first_bytes( - cls, - public_key_hex: str, - values: dict[str, Any] - ) -> ec.EllipticCurvePublicKey: - """Parse a public key hex into a public key instance, and validate that it corresponds to the private key.""" - private_key = values.get('private_key') - assert isinstance(private_key, ec.EllipticCurvePrivateKey), 'private_key must be set' - - public_key_bytes = bytes.fromhex(public_key_hex) - actual_public_key = private_key.public_key() + @model_validator(mode='after') + def _validate_keys_and_address(self) -> 'PoaSignerFile': + """Validate that public key and address correspond to the private key.""" + actual_public_key = self.private_key.public_key() + actual_public_key_bytes = get_public_key_bytes_compressed(actual_public_key) - if public_key_bytes != get_public_key_bytes_compressed(actual_public_key): + # Validate the provided public key matches the one derived from private key + provided_public_key_bytes = get_public_key_bytes_compressed(self.public_key) + if provided_public_key_bytes != actual_public_key_bytes: raise ValueError('invalid public key') - return actual_public_key + if self.address != get_address_b58_from_public_key(actual_public_key): + raise ValueError('invalid address') - @validator('address') - def _validate_address(cls, address: str, values: dict[str, Any]) -> str: - """Validate that the provided address corresponds to the provided private key.""" - private_key = values.get('private_key') - assert isinstance(private_key, ec.EllipticCurvePrivateKey), 'private_key must be set' + return self - if address != get_address_b58_from_public_key(private_key.public_key()): - raise ValueError('invalid address') + @field_validator('public_key', mode='before') + @classmethod + def _parse_public_key(cls, public_key_hex: str | ec.EllipticCurvePublicKey) -> ec.EllipticCurvePublicKey: + """Parse public key hex to public key object.""" + if isinstance(public_key_hex, ec.EllipticCurvePublicKey): + return public_key_hex + # The public key is provided as compressed bytes + public_key_bytes = bytes.fromhex(public_key_hex) + # For compressed public keys, we need to use a different approach + from cryptography.hazmat.primitives.asymmetric import ec as ec_module - return address + # Load compressed public key + return ec_module.EllipticCurvePublicKey.from_encoded_point( + ec_module.SECP256K1(), public_key_bytes + ) def get_signer(self) -> PoaSigner: """Get a PoaSigner for this file.""" diff --git a/hathor/event/model/base_event.py b/hathor/event/model/base_event.py index e59db1f7c..7f96e2732 100644 --- a/hathor/event/model/base_event.py +++ b/hathor/event/model/base_event.py @@ -12,17 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, Optional +from typing import Optional -from pydantic import NonNegativeInt, validator +from pydantic import ConfigDict, NonNegativeInt, model_validator -from hathor.event.model.event_data import BaseEventData, EventData +from hathor.event.model.event_data import EventData from hathor.event.model.event_type import EventType from hathor.pubsub import EventArguments from hathor.utils.pydantic import BaseModel -class BaseEvent(BaseModel, use_enum_values=True): +class BaseEvent(BaseModel): + model_config = ConfigDict(use_enum_values=True) + # Event unique id, determines event order id: NonNegativeInt # Timestamp in which the event was emitted, this follows the unix_timestamp format, it's only informative, events @@ -57,12 +59,12 @@ def from_event_arguments( group_id=group_id, ) - @validator('data') - def data_type_must_match_event_type(cls, v: BaseEventData, values: dict[str, Any]) -> BaseEventData: - event_type = EventType(values['type']) + @model_validator(mode='after') + def data_type_must_match_event_type(self) -> 'BaseEvent': + event_type = EventType(self.type) expected_data_type = event_type.data_type() - if type(v) is not expected_data_type: + if type(self.data) is not expected_data_type: raise ValueError('event data type does not match event type') - return v + return self diff --git a/hathor/event/model/event_data.py b/hathor/event/model/event_data.py index 65228fa9b..5be0de51a 100644 --- a/hathor/event/model/event_data.py +++ b/hathor/event/model/event_data.py @@ -14,9 +14,9 @@ from __future__ import annotations -from typing import Any, Optional, TypeAlias, Union, cast +from typing import Any, Optional, TypeAlias -from pydantic import Extra, validator +from pydantic import ConfigDict, model_validator from typing_extensions import Self from hathor.crypto.util import get_address_b58_from_bytes @@ -27,13 +27,15 @@ from hathor.utils.pydantic import BaseModel -class DecodedTxOutput(BaseModel, extra=Extra.ignore): +class DecodedTxOutput(BaseModel): + model_config = ConfigDict(extra='ignore') type: str address: str - timelock: Optional[int] + timelock: Optional[int] = None -class TxOutput(BaseModel, extra=Extra.ignore): +class TxOutput(BaseModel): + model_config = ConfigDict(extra='ignore') value: int token_data: int script: str @@ -70,8 +72,18 @@ class SpentOutput(BaseModel): index: int tx_ids: list[str] + @model_validator(mode='before') + @classmethod + def _parse_list_format(cls, data: Any) -> Any: + """Accept both dict format and list format [index, tx_ids] from metadata.to_json().""" + if isinstance(data, list): + return {'index': data[0], 'tx_ids': data[1]} + return data + + +class TxMetadata(BaseModel): + model_config = ConfigDict(extra='ignore') -class TxMetadata(BaseModel, extra=Extra.ignore): hash: str spent_outputs: list[SpentOutput] conflict_with: list[str] @@ -82,32 +94,10 @@ class TxMetadata(BaseModel, extra=Extra.ignore): score: float accumulated_weight_raw: str score_raw: str - first_block: Optional[str] + first_block: Optional[str] = None height: int validation: str - nc_execution: str | None - - @validator('spent_outputs', pre=True, each_item=True) - def _parse_spent_outputs(cls, spent_output: Union[SpentOutput, list[Union[int, list[str]]]]) -> SpentOutput: - """ - This validator method is called by pydantic when parsing models, and is not supposed to be called directly. - It either returns a SpentOutput if it receives one, or tries to parse it as a list (as returned from - metadata.to_json() method). Examples: - - >>> TxMetadata._parse_spent_outputs(SpentOutput(index=0, tx_ids=['tx1', 'tx2'])) - SpentOutput(index=0, tx_ids=['tx1', 'tx2']) - >>> TxMetadata._parse_spent_outputs([0, ['tx1', 'tx2']]) - SpentOutput(index=0, tx_ids=['tx1', 'tx2']) - """ - if isinstance(spent_output, list): - index, tx_ids = spent_output - - return SpentOutput( - index=cast(int, index), - tx_ids=cast(list[str], tx_ids) - ) - - return spent_output + nc_execution: str | None = None class BaseEventData(BaseModel): @@ -125,12 +115,14 @@ def from_event_arguments(cls, args: EventArguments) -> 'EmptyData': return cls() -class TxDataWithoutMeta(BaseEventData, extra=Extra.ignore): +class TxDataWithoutMeta(BaseEventData): """Class that represents transaction data on an event.""" + model_config = ConfigDict(extra='ignore') + hash: str nonce: Optional[int] = None timestamp: int - signal_bits: int | None + signal_bits: int | None = None version: int weight: float inputs: list['TxInput'] @@ -138,11 +130,11 @@ class TxDataWithoutMeta(BaseEventData, extra=Extra.ignore): parents: list[str] tokens: list[str] # TODO: Token name and symbol could be in a different class because they're only used by TokenCreationTransaction - token_name: Optional[str] - token_symbol: Optional[str] + token_name: Optional[str] = None + token_symbol: Optional[str] = None aux_pow: Optional[str] = None headers: list[TxHeader] = [] - name: str | None + name: str | None = None @classmethod def from_event_arguments(cls, args: EventArguments) -> Self: diff --git a/hathor/event/resources/event.py b/hathor/event/resources/event.py index e328a8852..922d059e7 100644 --- a/hathor/event/resources/event.py +++ b/hathor/event/resources/event.py @@ -66,7 +66,7 @@ def render_GET(self, request: Request) -> bytes: class GetEventsParams(QueryParams): - last_ack_event_id: Optional[NonNegativeInt] + last_ack_event_id: Optional[NonNegativeInt] = None size: int = Field(default=EVENT_API_DEFAULT_BATCH_SIZE, ge=0, le=EVENT_API_MAX_BATCH_SIZE) diff --git a/hathor/event/storage/rocksdb_storage.py b/hathor/event/storage/rocksdb_storage.py index ca1f0c7fc..2478131e1 100644 --- a/hathor/event/storage/rocksdb_storage.py +++ b/hathor/event/storage/rocksdb_storage.py @@ -52,7 +52,7 @@ def iter_from_event(self, key: int) -> Iterator[BaseEvent]: it.seek(int_to_bytes(key, 8)) for event_bytes in it: - yield BaseEvent.parse_raw(event_bytes) + yield BaseEvent.model_validate_json(event_bytes) # XXX: on Python 3.12, not deleting it here can cause EXC_BAD_ACCESS if the db is released before the iterator # in the garbage collector. This race condition might happen between tests. @@ -66,7 +66,7 @@ def _db_get_last_event(self) -> Optional[BaseEvent]: for i in it: last_element = i break - return None if last_element is None else BaseEvent.parse_raw(last_element) + return None if last_element is None else BaseEvent.model_validate_json(last_element) def _db_get_last_group_id(self) -> Optional[int]: last_group_id = self._db.get((self._cf_meta, _KEY_LAST_GROUP_ID)) @@ -81,7 +81,7 @@ def _save_event(self, event: BaseEvent, *, database: Union['rocksdb.DB', 'rocksd if (self._last_event is None and event.id != 0) or \ (self._last_event is not None and event.id != self._last_event.id + 1): raise ValueError('invalid event.id, ids must be sequential and leave no gaps') - event_data = json_dumpb(event.dict()) + event_data = json_dumpb(event.model_dump()) key = int_to_bytes(event.id, 8) database.put((self._cf_event, key), event_data) self._last_event = event @@ -104,7 +104,7 @@ def get_event(self, key: int) -> Optional[BaseEvent]: event = self._db.get((self._cf_event, int_to_bytes(key, 8))) if event is None: return None - return BaseEvent.parse_raw(event) + return BaseEvent.model_validate_json(event) def get_last_event(self) -> Optional[BaseEvent]: return self._last_event diff --git a/hathor/event/websocket/protocol.py b/hathor/event/websocket/protocol.py index c8da7e1f6..c825c52e1 100644 --- a/hathor/event/websocket/protocol.py +++ b/hathor/event/websocket/protocol.py @@ -186,7 +186,7 @@ def send_invalid_request_response( def _send_response(self, response: Response) -> None: """Actually sends a response to this connection.""" - payload = json_dumpb(response.dict()) + payload = json_dumpb(response.model_dump()) try: self.sendMessage(payload) diff --git a/hathor/event/websocket/request.py b/hathor/event/websocket/request.py index 64446887d..ce19924b6 100644 --- a/hathor/event/websocket/request.py +++ b/hathor/event/websocket/request.py @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Annotated, Literal, Optional +from typing import Annotated, Literal, Optional, Union -from pydantic import Field, NonNegativeInt +from pydantic import Discriminator, NonNegativeInt, RootModel, Tag from hathor.utils.pydantic import BaseModel @@ -28,7 +28,7 @@ class StartStreamRequest(BaseModel): window_size: The amount of events the client is able to process. """ type: Literal['START_STREAM'] - last_ack_event_id: Optional[NonNegativeInt] + last_ack_event_id: Optional[NonNegativeInt] = None window_size: NonNegativeInt @@ -54,13 +54,19 @@ class StopStreamRequest(BaseModel): type: Literal['STOP_STREAM'] -Request = Annotated[StartStreamRequest | AckRequest | StopStreamRequest, Field(discriminator='type')] +Request = Annotated[ + Union[ + Annotated[StartStreamRequest, Tag('START_STREAM')], + Annotated[AckRequest, Tag('ACK')], + Annotated[StopStreamRequest, Tag('STOP_STREAM')], + ], + Discriminator('type') +] -class RequestWrapper(BaseModel): +class RequestWrapper(RootModel[Request]): """Class that wraps the Request union type for parsing.""" - __root__: Request @classmethod def parse_raw_request(cls, raw: bytes) -> Request: - return cls.parse_raw(raw).__root__ + return cls.model_validate_json(raw).root diff --git a/hathor/event/websocket/response.py b/hathor/event/websocket/response.py index b8f83016b..5bb293ad0 100644 --- a/hathor/event/websocket/response.py +++ b/hathor/event/websocket/response.py @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. from enum import Enum -from typing import Optional +from typing import Literal, Optional -from pydantic import Field, NonNegativeInt +from pydantic import ConfigDict, NonNegativeInt from hathor.event.model.base_event import BaseEvent from hathor.utils.pydantic import BaseModel @@ -36,7 +36,7 @@ class EventResponse(Response): stream_id: The ID of the current stream. """ - type: str = Field(default='EVENT', const=True) + type: Literal['EVENT'] = 'EVENT' peer_id: str network: str event: BaseEvent @@ -53,7 +53,7 @@ class InvalidRequestType(Enum): ACK_TOO_LARGE = 'ACK_TOO_LARGE' -class InvalidRequestResponse(Response, use_enum_values=True): +class InvalidRequestResponse(Response): """Class to let the client know that it performed an invalid request. Args: @@ -61,6 +61,7 @@ class InvalidRequestResponse(Response, use_enum_values=True): invalid_request: The request that was invalid. error_message: A message describing why the request was invalid. """ + model_config = ConfigDict(use_enum_values=True) type: InvalidRequestType invalid_request: Optional[str] diff --git a/hathor/feature_activation/model/criteria.py b/hathor/feature_activation/model/criteria.py index c70b76b3f..39afc1a9b 100644 --- a/hathor/feature_activation/model/criteria.py +++ b/hathor/feature_activation/model/criteria.py @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Optional -from pydantic import Field, NonNegativeInt, validator +from pydantic import ConfigDict, Field, NonNegativeInt, model_validator from hathor import version from hathor.utils.pydantic import BaseModel @@ -23,7 +23,7 @@ from hathor.feature_activation.settings import Settings as FeatureSettings -class Criteria(BaseModel, validate_all=True): +class Criteria(BaseModel): """ Represents the configuration for a certain feature activation criteria. @@ -51,6 +51,8 @@ class Criteria(BaseModel, validate_all=True): signal_support_by_default: the default miner support signal for this feature. """ + model_config = ConfigDict(validate_default=True) + evaluation_interval: Optional[int] = None max_signal_bits: Optional[int] = None @@ -60,7 +62,7 @@ class Criteria(BaseModel, validate_all=True): threshold: Optional[NonNegativeInt] = None minimum_activation_height: NonNegativeInt = 0 lock_in_on_timeout: bool = False - version: str = Field(..., regex=version.BUILD_VERSION_REGEX) + version: str = Field(..., pattern=version.BUILD_VERSION_REGEX) signal_support_by_default: bool = False def to_validated(self, evaluation_interval: int, max_signal_bits: int) -> 'ValidatedCriteria': @@ -87,56 +89,38 @@ class ValidatedCriteria(Criteria): """ Wrapper class for Criteria that holds its field validations. Can be created using Criteria.to_validated(). """ - @validator('bit') - def _validate_bit(cls, bit: int, values: dict[str, Any]) -> int: - """Validates that the bit is lower than the max_signal_bits.""" - max_signal_bits = values.get('max_signal_bits') - assert max_signal_bits is not None, 'max_signal_bits must be set' - - if bit >= max_signal_bits: - raise ValueError(f'bit must be lower than max_signal_bits: {bit} >= {max_signal_bits}') - - return bit - @validator('timeout_height') - def _validate_timeout_height(cls, timeout_height: int, values: dict[str, Any]) -> int: - """Validates that the timeout_height is greater than the start_height.""" - evaluation_interval = values.get('evaluation_interval') - assert evaluation_interval is not None, 'evaluation_interval must be set' + @model_validator(mode='after') + def _validate_all(self) -> 'ValidatedCriteria': + """Validates all criteria fields.""" + # Validate bit + assert self.max_signal_bits is not None, 'max_signal_bits must be set' + if self.bit >= self.max_signal_bits: + raise ValueError(f'bit must be lower than max_signal_bits: {self.bit} >= {self.max_signal_bits}') - start_height = values.get('start_height') - assert start_height is not None, 'start_height must be set' + # Validate evaluation_interval is set + assert self.evaluation_interval is not None, 'evaluation_interval must be set' - minimum_timeout_height = start_height + 2 * evaluation_interval - - if timeout_height < minimum_timeout_height: + # Validate timeout_height + minimum_timeout_height = self.start_height + 2 * self.evaluation_interval + if self.timeout_height < minimum_timeout_height: raise ValueError(f'timeout_height must be at least two evaluation intervals after the start_height: ' - f'{timeout_height} < {minimum_timeout_height}') - - return timeout_height + f'{self.timeout_height} < {minimum_timeout_height}') - @validator('threshold') - def _validate_threshold(cls, threshold: Optional[int], values: dict[str, Any]) -> Optional[int]: - """Validates that the threshold is not greater than the evaluation_interval.""" - evaluation_interval = values.get('evaluation_interval') - assert evaluation_interval is not None, 'evaluation_interval must be set' - - if threshold is not None and threshold > evaluation_interval: + # Validate threshold + if self.threshold is not None and self.threshold > self.evaluation_interval: raise ValueError( - f'threshold must not be greater than evaluation_interval: {threshold} > {evaluation_interval}' + f'threshold must not be greater than evaluation_interval: ' + f'{self.threshold} > {self.evaluation_interval}' ) - return threshold - - @validator('start_height', 'timeout_height', 'minimum_activation_height') - def _validate_evaluation_interval_multiple(cls, value: int, values: dict[str, Any]) -> int: - """Validates that the value is a multiple of evaluation_interval.""" - evaluation_interval = values.get('evaluation_interval') - assert evaluation_interval is not None, 'evaluation_interval must be set' - - if value % evaluation_interval != 0: - raise ValueError( - f'Should be a multiple of evaluation_interval: {value} % {evaluation_interval} != 0' - ) + # Validate evaluation_interval multiples + for field_name in ('start_height', 'timeout_height', 'minimum_activation_height'): + value = getattr(self, field_name) + if value % self.evaluation_interval != 0: + raise ValueError( + f'{field_name} should be a multiple of evaluation_interval: ' + f'{value} % {self.evaluation_interval} != 0' + ) - return value + return self diff --git a/hathor/feature_activation/resources/feature.py b/hathor/feature_activation/resources/feature.py index 8aeea6f48..246ed8c18 100644 --- a/hathor/feature_activation/resources/feature.py +++ b/hathor/feature_activation/resources/feature.py @@ -14,6 +14,7 @@ from typing import Optional +from pydantic import ConfigDict from twisted.web.http import Request from hathor._openapi.register import register_resource @@ -131,7 +132,9 @@ class GetBlockFeaturesParams(QueryParams): block: str -class GetBlockFeatureResponse(Response, use_enum_values=True): +class GetBlockFeatureResponse(Response): + model_config = ConfigDict(use_enum_values=True) + bit: int signal: int feature: Feature @@ -142,7 +145,9 @@ class GetBlockFeaturesResponse(Response): signal_bits: list[GetBlockFeatureResponse] -class GetFeatureResponse(Response, use_enum_values=True): +class GetFeatureResponse(Response): + model_config = ConfigDict(use_enum_values=True) + name: Feature state: str acceptance: Optional[float] diff --git a/hathor/feature_activation/settings.py b/hathor/feature_activation/settings.py index 3d36e052b..31619b29b 100644 --- a/hathor/feature_activation/settings.py +++ b/hathor/feature_activation/settings.py @@ -13,17 +13,18 @@ # limitations under the License. from collections import defaultdict -from typing import Any, NamedTuple, Optional +from typing import NamedTuple, Optional -from pydantic import Field, NonNegativeInt, PositiveInt, validator +from pydantic import ConfigDict, Field, NonNegativeInt, PositiveInt, model_validator from hathor.feature_activation.feature import Feature from hathor.feature_activation.model.criteria import Criteria from hathor.utils.pydantic import BaseModel -class Settings(BaseModel, validate_all=True): +class Settings(BaseModel): """Feature Activation settings.""" + model_config = ConfigDict(validate_default=True) # The number of blocks in the feature activation evaluation interval. # Equivalent to 1 week (20160 * 30 seconds = 1 week) @@ -41,40 +42,34 @@ class Settings(BaseModel, validate_all=True): # neither their values changed, to preserve history. features: dict[Feature, Criteria] = {} - @validator('default_threshold') - def _validate_default_threshold(cls, default_threshold: int, values: dict[str, Any]) -> int: + @model_validator(mode='after') + def _validate_default_threshold(self) -> 'Settings': """Validates that the default_threshold is not greater than the evaluation_interval.""" - evaluation_interval = values.get('evaluation_interval') - assert evaluation_interval is not None, 'evaluation_interval must be set' - - if default_threshold > evaluation_interval: + if self.default_threshold > self.evaluation_interval: raise ValueError( f'default_threshold must not be greater than evaluation_interval: ' - f'{default_threshold} > {evaluation_interval}' + f'{self.default_threshold} > {self.evaluation_interval}' ) + return self - return default_threshold - - @validator('features') - def _validate_features(cls, features: dict[Feature, Criteria], values: dict[str, Any]) -> dict[Feature, Criteria]: + @model_validator(mode='after') + def _validate_features(self) -> 'Settings': """Validate Criteria by calling its to_validated() method, injecting the necessary attributes.""" - evaluation_interval = values.get('evaluation_interval') - max_signal_bits = values.get('max_signal_bits') - assert evaluation_interval is not None, 'evaluation_interval must be set' - assert max_signal_bits is not None, 'max_signal_bits must be set' - - return { - feature: criteria.to_validated(evaluation_interval, max_signal_bits) - for feature, criteria in features.items() + validated_features = { + feature: criteria.to_validated(self.evaluation_interval, self.max_signal_bits) + for feature, criteria in self.features.items() } + # Use object.__setattr__ because the model is frozen + object.__setattr__(self, 'features', validated_features) + return self - @validator('features') - def _validate_conflicting_bits(cls, features: dict[Feature, Criteria]) -> dict[Feature, Criteria]: + @model_validator(mode='after') + def _validate_conflicting_bits(self) -> 'Settings': """ Validates that a bit is only reused if the start_height of a new feature is greater than the timeout_height of the previous feature that used that bit. """ - intervals_by_bit = _get_intervals_by_bit(features) + intervals_by_bit = _get_intervals_by_bit(self.features) for intervals in intervals_by_bit.values(): overlap = _find_overlap(intervals) @@ -86,7 +81,7 @@ def _validate_conflicting_bits(cls, features: dict[Feature, Criteria]) -> dict[F f'{first.feature.value} and {second.feature.value}' ) - return features + return self class FeatureInterval(NamedTuple): diff --git a/hathor/feature_activation/storage/feature_activation_storage.py b/hathor/feature_activation/storage/feature_activation_storage.py index 101f213dd..3b7cda903 100644 --- a/hathor/feature_activation/storage/feature_activation_storage.py +++ b/hathor/feature_activation/storage/feature_activation_storage.py @@ -48,9 +48,9 @@ def validate_settings(self) -> None: self._save_settings(new_settings) return - db_settings: FeatureActivationSettings = FeatureActivationSettings.parse_raw(db_settings_bytes) - db_basic_settings = db_settings.copy(deep=True, exclude={'features'}) - new_basic_settings = new_settings.copy(deep=True, exclude={'features'}) + db_settings: FeatureActivationSettings = FeatureActivationSettings.model_validate_json(db_settings_bytes) + db_basic_settings = db_settings.model_copy(deep=True, update={'features': {}}) + new_basic_settings = new_settings.model_copy(deep=True, update={'features': {}}) self._validate_basic_settings(db_basic_settings=db_basic_settings, new_basic_settings=new_basic_settings) self._validate_features(db_features=db_settings.features, new_features=new_settings.features) diff --git a/hathor/nanocontracts/nc_exec_logs.py b/hathor/nanocontracts/nc_exec_logs.py index 90d9aa075..99334db80 100644 --- a/hathor/nanocontracts/nc_exec_logs.py +++ b/hathor/nanocontracts/nc_exec_logs.py @@ -22,7 +22,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Literal, assert_never -from pydantic import Field, validator +from pydantic import Field, field_serializer, field_validator from typing_extensions import override from hathor.nanocontracts import NCFail @@ -31,7 +31,7 @@ from hathor.reactor import ReactorProtocol from hathor.transaction import Transaction from hathor.types import VertexId -from hathor.utils.pydantic import BaseModel +from hathor.utils.pydantic import BaseModel, Hex if TYPE_CHECKING: from hathor.conf.settings import HathorSettings @@ -76,13 +76,13 @@ class _BaseNCEntry(BaseModel): level: NCLogLevel timestamp: float - @override - def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]: - json_dict = super().dict(*args, **kwargs) - json_dict['level'] = self.level.name - return json_dict + @field_serializer('level') + @classmethod + def _serialize_level(cls, level: NCLogLevel) -> str: + return level.name - @validator('level', pre=True) + @field_validator('level', mode='before') + @classmethod def _parse_level(cls, level: NCLogLevel | int | str) -> NCLogLevel: if isinstance(level, NCLogLevel): return level @@ -95,16 +95,16 @@ def _parse_level(cls, level: NCLogLevel | int | str) -> NCLogLevel: class NCLogEntry(_BaseNCEntry): """An entry representing a single log in a NC execution.""" - type: Literal['LOG'] = Field(const=True, default='LOG') + type: Literal['LOG'] = 'LOG' message: str key_values: dict[str, str] = Field(default_factory=dict) class NCCallBeginEntry(_BaseNCEntry): """An entry representing a single method call beginning in a NC execution.""" - type: Literal['CALL_BEGIN'] = Field(const=True, default='CALL_BEGIN') - level: NCLogLevel = Field(const=True, default=NCLogLevel.DEBUG) - nc_id: VertexId + type: Literal['CALL_BEGIN'] = 'CALL_BEGIN' + level: Literal[NCLogLevel.DEBUG] = NCLogLevel.DEBUG + nc_id: Hex[ContractId] call_type: CallType method_name: str str_args: str = '()' @@ -127,25 +127,11 @@ def from_call_record(call_record: CallRecord, *, timestamp: float) -> NCCallBegi actions=actions ) - @override - def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]: - json_dict = super().dict(*args, **kwargs) - json_dict['nc_id'] = self.nc_id.hex() - return json_dict - - @validator('nc_id', pre=True) - def _parse_nc_id(cls, vertex_id: VertexId | str) -> VertexId: - if isinstance(vertex_id, VertexId): - return vertex_id - if isinstance(vertex_id, str): - return bytes.fromhex(vertex_id) - raise TypeError(f'invalid vertex_id type: {type(vertex_id)}') - class NCCallEndEntry(_BaseNCEntry): """An entry representing a single method call ending in a NC execution.""" - type: Literal['CALL_END'] = Field(const=True, default='CALL_END') - level: NCLogLevel = Field(const=True, default=NCLogLevel.DEBUG) + type: Literal['CALL_END'] = 'CALL_END' + level: Literal[NCLogLevel.DEBUG] = NCLogLevel.DEBUG class NCExecEntry(BaseModel): @@ -166,7 +152,7 @@ def from_call_info(call_info: CallInfo, error_tb: str | None) -> NCExecEntry: def filter(self, log_level: NCLogLevel) -> NCExecEntry: """Create a new NCExecEntry while keeping logs with the provided log level or higher.""" - return self.copy( + return self.model_copy( update=dict( logs=[log for log in self.logs if log.level >= log_level], ), @@ -183,15 +169,15 @@ class NCExecEntries(BaseModel): @staticmethod def from_json(json_dict: dict[str, Any]) -> NCExecEntries: entries = { - bytes.fromhex(block_id_hex): [NCExecEntry.parse_obj(entry) for entry in entries] + bytes.fromhex(block_id_hex): [NCExecEntry.model_validate(entry) for entry in entries] for block_id_hex, entries in json_dict.items() } return NCExecEntries(entries=entries) @override - def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]: + def model_dump(self, *args: Any, **kwargs: Any) -> dict[str, Any]: return { - block_id.hex(): [entry.dict(*args, **kwargs) for entry in block_entries] + block_id.hex(): [entry.model_dump(*args, **kwargs) for entry in block_entries] for block_id, block_entries in self.entries.items() } @@ -298,7 +284,7 @@ def save_logs(self, tx: Transaction, call_info: CallInfo, exception_and_tb: tupl assert_never(self._config) new_entry = NCExecEntry.from_call_info(call_info, tb) - new_line_dict = {meta.first_block.hex(): new_entry.dict()} + new_line_dict = {meta.first_block.hex(): new_entry.model_dump()} path = self._get_file_path(tx.hash) with path.open(mode='a') as f: @@ -365,4 +351,4 @@ def get_json_logs( ) -> dict[str, Any] | None: """Return NC execution logs to the provided NC ID as json.""" logs = self.get_logs(nano_contract_id, log_level=log_level, block_id=block_id) - return None if logs is None else logs.dict() + return None if logs is None else logs.model_dump() diff --git a/hathor/nanocontracts/resources/builtin.py b/hathor/nanocontracts/resources/builtin.py index 6ec5e5cba..de7074b33 100644 --- a/hathor/nanocontracts/resources/builtin.py +++ b/hathor/nanocontracts/resources/builtin.py @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Iterator +from typing import Iterator, Literal -from pydantic import Field +from pydantic import ConfigDict, Field from sortedcontainers import SortedKeyList from twisted.web.http import Request @@ -100,9 +100,11 @@ def render_GET(self, request: Request) -> bytes: return response.json_dumpb() -class BuiltinBlueprintsParams(QueryParams, use_enum_values=True): - before: str | None - after: str | None +class BuiltinBlueprintsParams(QueryParams): + model_config = ConfigDict(use_enum_values=True) + + before: str | None = None + after: str | None = None count: int = Field(default=10, gt=0, le=100) search: str | None = None @@ -113,7 +115,7 @@ class BuiltinBlueprintItem(Response): class BuiltinBlueprintsResponse(Response): - success: bool = Field(default=True, const=True) + success: Literal[True] = True blueprints: list[BuiltinBlueprintItem] before: str | None after: str | None diff --git a/hathor/nanocontracts/resources/history.py b/hathor/nanocontracts/resources/history.py index 45d49b7c5..721cb1ea5 100644 --- a/hathor/nanocontracts/resources/history.py +++ b/hathor/nanocontracts/resources/history.py @@ -148,8 +148,8 @@ def render_GET(self, request: 'Request') -> bytes: class NCHistoryParams(QueryParams): id: str - after: Optional[str] - before: Optional[str] + after: Optional[str] = None + before: Optional[str] = None count: int = Field(default=100, lt=500) order: SortOrder = SortOrder.DESC include_nc_logs: bool = Field(default=False) diff --git a/hathor/nanocontracts/resources/nc_creation.py b/hathor/nanocontracts/resources/nc_creation.py index b9a8277c1..b87b1949a 100644 --- a/hathor/nanocontracts/resources/nc_creation.py +++ b/hathor/nanocontracts/resources/nc_creation.py @@ -14,6 +14,8 @@ from __future__ import annotations +from typing import Literal + from pydantic import Field from twisted.web.http import Request @@ -205,10 +207,10 @@ def _get_nc_creation_item(self, nc_id: bytes) -> NCCreationItem | None: class NCCreationParams(QueryParams): - before: str | None - after: str | None + before: str | None = None + after: str | None = None count: int = Field(default=10, le=100) - search: str | None + search: str | None = None order: SortOrder = SortOrder.DESC @@ -222,7 +224,7 @@ class NCCreationItem(Response): class NCCreationResponse(Response): - success: bool = Field(default=True, const=True) + success: Literal[True] = True nc_creation_txs: list[NCCreationItem] before: str | None after: str | None diff --git a/hathor/nanocontracts/resources/nc_exec_logs.py b/hathor/nanocontracts/resources/nc_exec_logs.py index 388eff124..1861682f5 100644 --- a/hathor/nanocontracts/resources/nc_exec_logs.py +++ b/hathor/nanocontracts/resources/nc_exec_logs.py @@ -12,9 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any +from typing import Any, Literal -from pydantic import Field from twisted.web.http import Request from hathor._openapi.register import register_resource @@ -23,7 +22,7 @@ from hathor.nanocontracts.nc_exec_logs import NCLogLevel from hathor.transaction import Transaction from hathor.transaction.storage.exceptions import TransactionDoesNotExist -from hathor.utils.api import ErrorResponse, QueryParams +from hathor.utils.api import ErrorResponse, QueryParams, Response @register_resource @@ -107,8 +106,8 @@ class NCExecLogsParams(QueryParams): all_execs: bool = False -class NCExecLogsResponse(QueryParams): - success: bool = Field(const=True, default=True) +class NCExecLogsResponse(Response): + success: Literal[True] = True nc_id: str nc_execution: str | None logs: dict[str, Any] diff --git a/hathor/nanocontracts/resources/on_chain.py b/hathor/nanocontracts/resources/on_chain.py index 689846b86..779e4678e 100644 --- a/hathor/nanocontracts/resources/on_chain.py +++ b/hathor/nanocontracts/resources/on_chain.py @@ -13,6 +13,7 @@ # limitations under the License. from enum import Enum +from typing import Literal from pydantic import Field from twisted.web.http import Request @@ -151,8 +152,8 @@ def is_desc(self) -> bool: class OnChainBlueprintsParams(QueryParams): - before: str | None - after: str | None + before: str | None = None + after: str | None = None count: int = Field(default=10, le=100) search: str | None = None order: SortOrder = SortOrder.DESC @@ -165,7 +166,7 @@ class OnChainBlueprintItem(Response): class OnChainBlueprintsResponse(Response): - success: bool = Field(default=True, const=True) + success: Literal[True] = True blueprints: list[OnChainBlueprintItem] before: str | None after: str | None diff --git a/hathor/nanocontracts/resources/state.py b/hathor/nanocontracts/resources/state.py index 96696facb..2ee203677 100644 --- a/hathor/nanocontracts/resources/state.py +++ b/hathor/nanocontracts/resources/state.py @@ -249,9 +249,9 @@ class NCStateParams(QueryParams): fields: list[str] = Field(alias='fields[]', default_factory=list) balances: list[str] = Field(alias='balances[]', default_factory=list) calls: list[str] = Field(alias='calls[]', default_factory=list) - block_hash: Optional[str] - block_height: Optional[int] - timestamp: Optional[int] + block_hash: Optional[str] = None + block_height: Optional[int] = None + timestamp: Optional[int] = None class NCValueSuccessResponse(Response): diff --git a/hathor/p2p/sync_v2/agent.py b/hathor/p2p/sync_v2/agent.py index 08f6f2ba9..28947dd2e 100644 --- a/hathor/p2p/sync_v2/agent.py +++ b/hathor/p2p/sync_v2/agent.py @@ -671,7 +671,7 @@ def send_get_next_blocks(self, start_hash: bytes, end_hash: bytes, quantity: int end_hash=end_hash, quantity=quantity, ) - self.send_message(ProtocolMessages.GET_NEXT_BLOCKS, payload.json()) + self.send_message(ProtocolMessages.GET_NEXT_BLOCKS, payload.model_dump_json()) self.receiving_stream = True def handle_get_next_blocks(self, payload: str) -> None: @@ -681,7 +681,7 @@ def handle_get_next_blocks(self, payload: str) -> None: if self._is_streaming: self.protocol.send_error_and_close_connection('GET-NEXT-BLOCKS received before previous one finished') return - data = GetNextBlocksPayload.parse_raw(payload) + data = GetNextBlocksPayload.model_validate_json(payload) start_block = self._validate_block(data.start_hash) if start_block is None: return @@ -842,12 +842,12 @@ def handle_get_best_block(self, _payload: str) -> None: block=best_block.hash, height=best_block.static_metadata.height, ) - self.send_message(ProtocolMessages.BEST_BLOCK, payload.json()) + self.send_message(ProtocolMessages.BEST_BLOCK, payload.model_dump_json()) def handle_best_block(self, payload: str) -> None: """ Handle a BEST-BLOCK message. """ - data = BestBlockPayload.parse_raw(payload) + data = BestBlockPayload.model_validate_json(payload) best_block = _HeightInfo(height=data.height, id=data.block) deferred = self._deferred_best_block @@ -921,7 +921,7 @@ def send_get_transactions_bfs(self, first_block_hash=first_block_hash, last_block_hash=last_block_hash, ) - self.send_message(ProtocolMessages.GET_TRANSACTIONS_BFS, payload.json()) + self.send_message(ProtocolMessages.GET_TRANSACTIONS_BFS, payload.model_dump_json()) self.receiving_stream = True def handle_get_transactions_bfs(self, payload: str) -> None: @@ -930,7 +930,7 @@ def handle_get_transactions_bfs(self, payload: str) -> None: if self._is_streaming: self.log.warn('ignore GET-TRANSACTIONS-BFS, already streaming') return - data = GetTransactionsBFSPayload.parse_raw(payload) + data = GetTransactionsBFSPayload.model_validate_json(payload) if len(data.start_from) > MAX_GET_TRANSACTIONS_BFS_LEN: self.log.error('too many transactions in GET-TRANSACTIONS-BFS', state=self.state) diff --git a/hathor/p2p/sync_v2/payloads.py b/hathor/p2p/sync_v2/payloads.py index 002b2d67f..8842b7186 100644 --- a/hathor/p2p/sync_v2/payloads.py +++ b/hathor/p2p/sync_v2/payloads.py @@ -12,62 +12,30 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pydantic import validator - from hathor.types import VertexId -from hathor.utils.pydantic import BaseModel +from hathor.utils.pydantic import BaseModel, Hex class PayloadBaseModel(BaseModel): - - @classmethod - def convert_hex_to_bytes(cls, value: str | VertexId) -> VertexId: - """Convert a string in hex format to bytes. If bytes are given, it does nothing.""" - if isinstance(value, str): - return bytes.fromhex(value) - elif isinstance(value, VertexId): - return value - raise ValueError('invalid type') - - class Config: - json_encoders = { - VertexId: lambda x: x.hex() - } + """Base model for P2P message payloads with automatic hex encoding for bytes fields.""" + pass class GetNextBlocksPayload(PayloadBaseModel): """GET-NEXT-BLOCKS message is used to request a stream of blocks in the best blockchain.""" - - start_hash: VertexId - end_hash: VertexId + start_hash: Hex[VertexId] + end_hash: Hex[VertexId] quantity: int - @validator('start_hash', 'end_hash', pre=True) - def validate_bytes_fields(cls, value: str | bytes) -> VertexId: - return cls.convert_hex_to_bytes(value) - class BestBlockPayload(PayloadBaseModel): """BEST-BLOCK message is used to send information about the current best block.""" - - block: VertexId + block: Hex[VertexId] height: int - @validator('block', pre=True) - def validate_bytes_fields(cls, value: str | VertexId) -> VertexId: - return cls.convert_hex_to_bytes(value) - class GetTransactionsBFSPayload(PayloadBaseModel): """GET-TRANSACTIONS-BFS message is used to request a stream of transactions confirmed by blocks.""" - start_from: list[VertexId] - first_block_hash: VertexId - last_block_hash: VertexId - - @validator('first_block_hash', 'last_block_hash', pre=True) - def validate_bytes_fields(cls, value: str | VertexId) -> VertexId: - return cls.convert_hex_to_bytes(value) - - @validator('start_from', pre=True, each_item=True) - def validate_start_from(cls, value: str | VertexId) -> VertexId: - return cls.convert_hex_to_bytes(value) + start_from: list[Hex[VertexId]] + first_block_hash: Hex[VertexId] + last_block_hash: Hex[VertexId] diff --git a/hathor/sysctl/sysctl.py b/hathor/sysctl/sysctl.py index 28339365d..96c7d8c5e 100644 --- a/hathor/sysctl/sysctl.py +++ b/hathor/sysctl/sysctl.py @@ -14,7 +14,7 @@ from typing import Any, Callable, Iterator, NamedTuple, Optional, ParamSpec, TypeVar -from pydantic import validate_arguments +from pydantic import validate_call from structlog import get_logger from hathor.sysctl.exception import SysctlEntryNotFound, SysctlReadOnlyEntry, SysctlWriteOnlyEntry @@ -58,7 +58,7 @@ def register(self, path: str, getter: Optional[Getter], setter: Optional[Setter] """Register a new parameter for sysctl.""" assert path not in self._commands if setter is not None: - setter = validate_arguments(setter) + setter = validate_call(setter) self._commands[path] = SysctlCommand( getter=getter, setter=setter, diff --git a/hathor/transaction/resources/block_at_height.py b/hathor/transaction/resources/block_at_height.py index 60dd4f062..f8de50638 100644 --- a/hathor/transaction/resources/block_at_height.py +++ b/hathor/transaction/resources/block_at_height.py @@ -90,7 +90,7 @@ def render_GET(self, request: 'Request') -> bytes: class BlockAtHeightParams(QueryParams): height: int - include_transactions: str | None + include_transactions: str | None = None BlockAtHeightResource.openapi = { diff --git a/hathor/transaction/static_metadata.py b/hathor/transaction/static_metadata.py index 0957baaca..a201f126a 100644 --- a/hathor/transaction/static_metadata.py +++ b/hathor/transaction/static_metadata.py @@ -289,6 +289,6 @@ def _calculate_closest_ancestor_block( @override def json_dumpb(self) -> bytes: from hathor.util import json_dumpb - json_dict = self.dict() + json_dict = self.model_dump() json_dict['closest_ancestor_block'] = json_dict['closest_ancestor_block'].hex() return json_dumpb(json_dict) diff --git a/hathor/utils/api.py b/hathor/utils/api.py index a8e592809..a8f52b36b 100644 --- a/hathor/utils/api.py +++ b/hathor/utils/api.py @@ -13,9 +13,9 @@ # limitations under the License. from email.message import Message -from typing import Type, TypeVar, Union +from typing import Literal, Type, TypeVar, Union -from pydantic import Field, ValidationError +from pydantic import ValidationError from twisted.web.http import Request from hathor.api_util import get_args @@ -58,7 +58,7 @@ def from_request(cls: Type[T], request: Request) -> Union[T, 'ErrorResponse']: args[decoded_key] = decoded_values try: - return cls.parse_obj(args) + return cls.model_validate(args) except ValidationError as error: return ErrorResponse(error=str(error)) @@ -68,5 +68,5 @@ class Response(BaseModel): class ErrorResponse(Response): - success: bool = Field(default=False, const=True) + success: Literal[False] = False error: str diff --git a/hathor/utils/named_tuple.py b/hathor/utils/named_tuple.py index ab29fdf37..6adb85744 100644 --- a/hathor/utils/named_tuple.py +++ b/hathor/utils/named_tuple.py @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, NamedTuple, Optional, TypeVar +from typing import Any, NamedTuple, TypeVar, get_type_hints -import pydantic +from pydantic import TypeAdapter, create_model from hathor.utils.pydantic import BaseModel @@ -25,7 +25,7 @@ def validated_named_tuple_from_dict( named_tuple_type: type[T], attributes_dict: dict[str, Any], *, - validators: Optional[dict[str, classmethod]] = None + validators: dict[str, Any] | None = None ) -> T: """ Takes an attributes dict and returns a validated instance of the specified NamedTuple subclass. @@ -34,20 +34,36 @@ def validated_named_tuple_from_dict( Args: named_tuple_type: the NamedTuple subclass to create an instance from attributes_dict: a dict with all required attributes for the NamedTuple subclass - validators: custom pydantic validators (read https://docs.pydantic.dev/latest/usage/validators) + validators: custom pydantic field_validators (dict of name -> decorated validator) Returns: a validated instance of the specified NamedTuple subclass """ - model = pydantic.create_model_from_namedtuple( - named_tuple_type, + if not validators: + # Simple case: use TypeAdapter directly (Pydantic v2 native NamedTuple support) + adapter = TypeAdapter(named_tuple_type) + return adapter.validate_python(attributes_dict) + + # Complex case with validators: create a dynamic model + type_hints = get_type_hints(named_tuple_type) + defaults = getattr(named_tuple_type, '_field_defaults', {}) + + field_definitions: dict[str, Any] = { + name: (hint, defaults.get(name, ...)) + for name, hint in type_hints.items() + } + + model = create_model( + f'{named_tuple_type.__name__}Model', __base__=BaseModel, - __validators__=validators + __validators__=validators, + **field_definitions ) - # This intermediate step shouldn't be necessary, but for some reason pydantic.create_model_from_namedtuple - # doesn't support default attribute values, so we do this to add them + # Fill in defaults via intermediate NamedTuple, then validate all_attributes = named_tuple_type(**attributes_dict) # type: ignore[call-overload] - validated_attributes = model(**all_attributes._asdict()) - validated_attributes_dict = {k: v for k, v in validated_attributes} + validated = model.model_validate(all_attributes._asdict()) - return named_tuple_type(**validated_attributes_dict) # type: ignore[call-overload] + # Use dict comprehension to get validated attributes directly from the model + # instead of model_dump() which would convert nested Pydantic models to dicts + validated_dict = {name: getattr(validated, name) for name in type_hints} + return named_tuple_type(**validated_dict) # type: ignore[call-overload] diff --git a/hathor/utils/pydantic.py b/hathor/utils/pydantic.py index 44c7d6766..20bb97591 100644 --- a/hathor/utils/pydantic.py +++ b/hathor/utils/pydantic.py @@ -12,8 +12,68 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pydantic import BaseModel as PydanticBaseModel, Extra -from pydantic.generics import GenericModel as PydanticGenericModel +from typing import TYPE_CHECKING, Annotated, Any, TypeVar + +from pydantic import BaseModel as PydanticBaseModel, ConfigDict +from pydantic.functional_serializers import PlainSerializer +from pydantic.functional_validators import BeforeValidator + +BytesT = TypeVar('BytesT', bound=bytes) + + +def _hex_to_bytes(value: Any) -> bytes: + """Convert hex string to bytes, or pass through if already bytes.""" + if isinstance(value, bytes): + return value + if isinstance(value, str): + return bytes.fromhex(value) + raise ValueError(f'Expected bytes or hex string, got {type(value).__name__}') + + +def _bytes_to_hex(value: bytes) -> str: + """Convert bytes to hex string.""" + return value.hex() + + +if TYPE_CHECKING: + # For type checking: Hex[T] is just T (an identity type alias) + # This allows mypy to treat Hex[VertexId] as VertexId + Hex = Annotated[BytesT, ...] +else: + # At runtime: Hex[T] returns Annotated[T, validators, serializers] + # Pydantic uses this for automatic hex encoding/decoding + # + # Usage: + # from hathor.types import VertexId, ContractId + # from hathor.utils.pydantic import Hex + # + # class MyModel(BaseModel): + # tx_id: Hex[VertexId] # Preserves VertexId type + # contract: Hex[ContractId] # Preserves ContractId type + # items: list[Hex[VertexId]] # Works with generic types too + # + # Behavior: + # - Deserialization: Accepts both bytes and hex str, converts to the base type + # - Serialization: Always outputs as hex string in JSON + + class _HexMeta(type): + """Metaclass that makes Hex[T] return Annotated[bytes, ...] at runtime. + + We use `bytes` as the base type for Pydantic schema generation because + Pydantic v2 doesn't know how to handle custom bytes subclasses like + ContractId or VertexId. The validators/serializers handle the conversion. + """ + + def __getitem__(cls, base_type: type[BytesT]) -> type[BytesT]: + return Annotated[ # type: ignore[return-value] + bytes, + BeforeValidator(_hex_to_bytes), + PlainSerializer(_bytes_to_hex, return_type=str), + ] + + class Hex(metaclass=_HexMeta): + """Hex[T] wraps a bytes-derived type to enable hex serialization.""" + pass class BaseModel(PydanticBaseModel): @@ -21,24 +81,10 @@ class BaseModel(PydanticBaseModel): This class defines a project BaseModel to be used instead of pydantic's, setting stricter global configurations. Other configurations can be set on a case by case basis. - Read: https://docs.pydantic.dev/usage/model_config/#change-behaviour-globally + Read: https://docs.pydantic.dev/latest/concepts/config/ """ + model_config = ConfigDict(extra='forbid', frozen=True) def json_dumpb(self) -> bytes: """Utility method for converting a Model into bytes representation of a JSON.""" - from hathor.util import json_dumpb - return json_dumpb(self.dict()) - - class Config: - allow_mutation = False - extra = Extra.forbid - - -class GenericModel(BaseModel, PydanticGenericModel): - """Substitute for pydantic's GenericModel. - This class defines a project GenericModel to be used instead of pydantic's, setting stricter global configurations. - Other configurations can be set on a case by case basis. - - Read: https://docs.pydantic.dev/usage/model_config/#change-behaviour-globally - """ - pass + return self.model_dump_json().encode('utf-8') diff --git a/hathor/websocket/messages.py b/hathor/websocket/messages.py index d84065745..f205818a2 100644 --- a/hathor/websocket/messages.py +++ b/hathor/websocket/messages.py @@ -12,9 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, Optional - -from pydantic import Field +from typing import Any, Literal, Optional from hathor.utils.pydantic import BaseModel @@ -24,13 +22,13 @@ class WebSocketMessage(BaseModel): class WebSocketErrorMessage(WebSocketMessage): - type: str = Field('error', const=True) - success: bool = Field(False, const=True) + type: Literal['error'] = 'error' + success: Literal[False] = False errmsg: str class CapabilitiesMessage(WebSocketMessage): - type: str = Field('capabilities', const=True) + type: Literal['capabilities'] = 'capabilities' capabilities: list[str] @@ -39,33 +37,33 @@ class StreamBase(WebSocketMessage): class StreamErrorMessage(StreamBase): - type: str = Field('stream:history:error', const=True) + type: Literal['stream:history:error'] = 'stream:history:error' id: str errmsg: str class StreamBeginMessage(StreamBase): - type: str = Field('stream:history:begin', const=True) + type: Literal['stream:history:begin'] = 'stream:history:begin' id: str seq: int window_size: Optional[int] class StreamEndMessage(StreamBase): - type: str = Field('stream:history:end', const=True) + type: Literal['stream:history:end'] = 'stream:history:end' id: str seq: int class StreamVertexMessage(StreamBase): - type: str = Field('stream:history:vertex', const=True) + type: Literal['stream:history:vertex'] = 'stream:history:vertex' id: str seq: int data: dict[str, Any] class StreamAddressMessage(StreamBase): - type: str = Field('stream:history:address', const=True) + type: Literal['stream:history:address'] = 'stream:history:address' id: str seq: int index: int diff --git a/hathor_cli/generate_genesis.py b/hathor_cli/generate_genesis.py index 86cbd5a24..87bed8445 100644 --- a/hathor_cli/generate_genesis.py +++ b/hathor_cli/generate_genesis.py @@ -37,7 +37,7 @@ def main() -> None: parser.add_argument('--min-tx-weight', type=float, help='The MIN_TX_WEIGHT', required=True) raw_args = parser.parse_args(sys.argv[1:]) - args = GenerateGenesisArgs.parse_obj((vars(raw_args))) + args = GenerateGenesisArgs.model_validate((vars(raw_args))) block, tx1, tx2 = generate_new_genesis( tokens=args.tokens, diff --git a/hathor_cli/run_node.py b/hathor_cli/run_node.py index 9ed17ea66..2e5b781b8 100644 --- a/hathor_cli/run_node.py +++ b/hathor_cli/run_node.py @@ -586,7 +586,7 @@ def parse_args(self, argv: list[str]) -> Namespace: def _parse_args_obj(self, args: dict[str, Any]) -> 'RunNodeArgs': from hathor_cli.run_node_args import RunNodeArgs - return RunNodeArgs.parse_obj(args) + return RunNodeArgs.model_validate(args) def run(self) -> None: self.reactor.run() diff --git a/hathor_cli/run_node_args.py b/hathor_cli/run_node_args.py index ed3595ce4..89b0d2826 100644 --- a/hathor_cli/run_node_args.py +++ b/hathor_cli/run_node_args.py @@ -14,18 +14,20 @@ from typing import Optional -from pydantic import Extra +from pydantic import ConfigDict from hathor.feature_activation.feature import Feature # skip-cli-import-custom-check from hathor.nanocontracts.nc_exec_logs import NCLogConfig # skip-cli-import-custom-check from hathor.utils.pydantic import BaseModel # skip-cli-import-custom-check -class RunNodeArgs(BaseModel, extra=Extra.allow): +class RunNodeArgs(BaseModel): """ Class that represents the CLI arguments used by the run_node command. Arguments must also be added to hathor_cli.run_node.RunNode.create_parser. """ + model_config = ConfigDict(extra='allow') + hostname: Optional[str] auto_hostname: bool unsafe_mode: Optional[str] diff --git a/hathor_cli/side_dag.py b/hathor_cli/side_dag.py index d44fa1503..28fba8ec5 100644 --- a/hathor_cli/side_dag.py +++ b/hathor_cli/side_dag.py @@ -48,7 +48,7 @@ class SideDagArgs(RunNodeArgs): - poa_signer_file: str | None + poa_signer_file: str | None = None class SideDagRunNode(RunNode): @@ -56,7 +56,7 @@ class SideDagRunNode(RunNode): @override def _parse_args_obj(self, args: dict[str, Any]) -> RunNodeArgs: - return SideDagArgs.parse_obj(args) + return SideDagArgs.model_validate(args) @classmethod @override diff --git a/hathor_tests/event/event_simulation_tester.py b/hathor_tests/event/event_simulation_tester.py index 6288e7ad7..172a79fac 100644 --- a/hathor_tests/event/event_simulation_tester.py +++ b/hathor_tests/event/event_simulation_tester.py @@ -62,12 +62,12 @@ def _send_request(self, request: Request) -> None: ) def _get_success_responses(self) -> list[EventResponse]: - return list(map(EventResponse.parse_obj, self._get_transport_messages())) + return list(map(EventResponse.model_validate, self._get_transport_messages())) def _get_error_response(self) -> InvalidRequestResponse: responses = self._get_transport_messages() assert len(responses) == 1 - return InvalidRequestResponse.parse_obj(responses[0]) + return InvalidRequestResponse.model_validate(responses[0]) def _get_transport_messages(self) -> list[dict[str, Any]]: values = self.transport.value() diff --git a/hathor_tests/event/test_base_event.py b/hathor_tests/event/test_base_event.py index 6608b27ba..1d0afabd8 100644 --- a/hathor_tests/event/test_base_event.py +++ b/hathor_tests/event/test_base_event.py @@ -72,7 +72,7 @@ def test_create_base_event(event_id: int, group_id: int | None) -> None: group_id=group_id ) - assert event.dict() == expected + assert event.model_dump() == expected @pytest.mark.parametrize('event_id', [-1, -1000]) diff --git a/hathor_tests/event/test_event_reorg.py b/hathor_tests/event/test_event_reorg.py index 1ef7a2812..6056d3809 100644 --- a/hathor_tests/event/test_event_reorg.py +++ b/hathor_tests/event/test_event_reorg.py @@ -86,4 +86,4 @@ def test_reorg_events(self) -> None: self.assertEqual(EventType(actual_event.type), expected_event_type) for expected_data_key, expected_data_value in expected_partial_data.items(): - self.assertEqual(actual_event.data.dict()[expected_data_key], expected_data_value) + self.assertEqual(actual_event.data.model_dump()[expected_data_key], expected_data_value) diff --git a/hathor_tests/event/test_event_simulation_scenarios.py b/hathor_tests/event/test_event_simulation_scenarios.py index 7b5430f07..97c067100 100644 --- a/hathor_tests/event/test_event_simulation_scenarios.py +++ b/hathor_tests/event/test_event_simulation_scenarios.py @@ -56,9 +56,9 @@ def assert_response_equal(self, responses: list[EventResponse], expected: list[E for a, b in zip(responses, expected): self.assertEqual(type(a), type(b)) - self.assertEqual(a.__fields__, b.__fields__) - self.assertEqual(a.event.__fields__, b.event.__fields__) - self.assertEqual(a.event.data.__fields__, b.event.data.__fields__) + self.assertEqual(type(a).model_fields, type(b).model_fields) + self.assertEqual(type(a.event).model_fields, type(b.event).model_fields) + self.assertEqual(type(a.event.data).model_fields, type(b.event.data).model_fields) for field in ['type', 'peer_id', 'network', 'latest_event_id', 'stream_id']: self.assertEqual(getattr(a, field), getattr(b, field)) diff --git a/hathor_tests/event/test_tx_metadata.py b/hathor_tests/event/test_tx_metadata.py index 03486287b..4a5bfcfbd 100644 --- a/hathor_tests/event/test_tx_metadata.py +++ b/hathor_tests/event/test_tx_metadata.py @@ -44,7 +44,7 @@ def test_from_spent_output_list() -> None: SpentOutput(index=0, tx_ids=['a', 'b']), SpentOutput(index=1, tx_ids=['c', 'd']), ] - metadata = TxMetadata.parse_obj( + metadata = TxMetadata.model_validate( dict( hash='some_hash', spent_outputs=[ @@ -74,7 +74,7 @@ def test_from_spent_output_dict() -> None: SpentOutput(index=0, tx_ids=['a', 'b']), SpentOutput(index=1, tx_ids=['c', 'd']), ] - metadata = TxMetadata.parse_obj( + metadata = TxMetadata.model_validate( dict( hash='some_hash', spent_outputs=[ diff --git a/hathor_tests/feature_activation/test_criteria.py b/hathor_tests/feature_activation/test_criteria.py index f84e4ca4d..ebfb0ca0d 100644 --- a/hathor_tests/feature_activation/test_criteria.py +++ b/hathor_tests/feature_activation/test_criteria.py @@ -52,10 +52,10 @@ def test_valid_criteria(criteria: dict[str, Any]) -> None: @pytest.mark.parametrize( ['bit', 'error'], [ - (-10, 'ensure this value is greater than or equal to 0'), - (-1, 'ensure this value is greater than or equal to 0'), - (2, 'bit must be lower than max_signal_bits: 2 >= 2'), - (10, 'bit must be lower than max_signal_bits: 10 >= 2') + (-10, 'Input should be greater than or equal to 0'), + (-1, 'Input should be greater than or equal to 0'), + (2, 'Value error, bit must be lower than max_signal_bits: 2 >= 2'), + (10, 'Value error, bit must be lower than max_signal_bits: 10 >= 2') ] ) def test_bit(bit: int, error: str) -> None: @@ -70,11 +70,11 @@ def test_bit(bit: int, error: str) -> None: @pytest.mark.parametrize( ['start_height', 'error'], [ - (-10, 'ensure this value is greater than or equal to 0'), - (-1, 'ensure this value is greater than or equal to 0'), - (1, 'Should be a multiple of evaluation_interval: 1 % 1000 != 0'), - (45, 'Should be a multiple of evaluation_interval: 45 % 1000 != 0'), - (100, 'Should be a multiple of evaluation_interval: 100 % 1000 != 0') + (-10, 'Input should be greater than or equal to 0'), + (-1, 'Input should be greater than or equal to 0'), + (1, 'Value error, start_height should be a multiple of evaluation_interval: 1 % 1000 != 0'), + (45, 'Value error, start_height should be a multiple of evaluation_interval: 45 % 1000 != 0'), + (100, 'Value error, start_height should be a multiple of evaluation_interval: 100 % 1000 != 0') ] ) def test_start_height(start_height: int, error: str) -> None: @@ -89,12 +89,15 @@ def test_start_height(start_height: int, error: str) -> None: @pytest.mark.parametrize( ['timeout_height', 'error'], [ - (-10, 'ensure this value is greater than or equal to 0'), - (-1, 'ensure this value is greater than or equal to 0'), - (1, 'timeout_height must be at least two evaluation intervals after the start_height: 1 < 3000'), - (45, 'timeout_height must be at least two evaluation intervals after the start_height: 45 < 3000'), - (100, 'timeout_height must be at least two evaluation intervals after the start_height: 100 < 3000'), - (3111, 'Should be a multiple of evaluation_interval: 3111 % 1000 != 0') + (-10, 'Input should be greater than or equal to 0'), + (-1, 'Input should be greater than or equal to 0'), + (1, 'Value error, timeout_height must be at least two evaluation intervals after the start_height: ' + '1 < 3000'), + (45, 'Value error, timeout_height must be at least two evaluation intervals after the start_height: ' + '45 < 3000'), + (100, 'Value error, timeout_height must be at least two evaluation intervals after the start_height: ' + '100 < 3000'), + (3111, 'Value error, timeout_height should be a multiple of evaluation_interval: 3111 % 1000 != 0') ] ) def test_timeout_height(timeout_height: int, error: str) -> None: @@ -109,10 +112,10 @@ def test_timeout_height(timeout_height: int, error: str) -> None: @pytest.mark.parametrize( ['threshold', 'error'], [ - (-10, 'ensure this value is greater than or equal to 0'), - (-1, 'ensure this value is greater than or equal to 0'), - (1001, 'threshold must not be greater than evaluation_interval: 1001 > 1000'), - (100000, 'threshold must not be greater than evaluation_interval: 100000 > 1000') + (-10, 'Input should be greater than or equal to 0'), + (-1, 'Input should be greater than or equal to 0'), + (1001, 'Value error, threshold must not be greater than evaluation_interval: 1001 > 1000'), + (100000, 'Value error, threshold must not be greater than evaluation_interval: 100000 > 1000') ] ) def test_threshold(threshold: int, error: str) -> None: @@ -127,11 +130,11 @@ def test_threshold(threshold: int, error: str) -> None: @pytest.mark.parametrize( ['minimum_activation_height', 'error'], [ - (-10, 'ensure this value is greater than or equal to 0'), - (-1, 'ensure this value is greater than or equal to 0'), - (1, 'Should be a multiple of evaluation_interval: 1 % 1000 != 0'), - (45, 'Should be a multiple of evaluation_interval: 45 % 1000 != 0'), - (100, 'Should be a multiple of evaluation_interval: 100 % 1000 != 0'), + (-10, 'Input should be greater than or equal to 0'), + (-1, 'Input should be greater than or equal to 0'), + (1, 'Value error, minimum_activation_height should be a multiple of evaluation_interval: 1 % 1000 != 0'), + (45, 'Value error, minimum_activation_height should be a multiple of evaluation_interval: 45 % 1000 != 0'), + (100, 'Value error, minimum_activation_height should be a multiple of evaluation_interval: 100 % 1000 != 0'), ] ) def test_minimum_activation_height(minimum_activation_height: int, error: str) -> None: @@ -143,7 +146,7 @@ def test_minimum_activation_height(minimum_activation_height: int, error: str) - assert errors[0]['msg'] == error -_invalid_version_msg = r'string does not match regex "^(\d+\.\d+\.\d+(-(rc|alpha|beta)\.\d+)?|nightly-[a-f0-9]{7,8})$"' +_invalid_version_msg = r"String should match pattern '^(\d+\.\d+\.\d+(-(rc|alpha|beta)\.\d+)?|nightly-[a-f0-9]{7,8})$'" @pytest.mark.parametrize( diff --git a/hathor_tests/feature_activation/test_feature_service.py b/hathor_tests/feature_activation/test_feature_service.py index 636b20b63..e18e39588 100644 --- a/hathor_tests/feature_activation/test_feature_service.py +++ b/hathor_tests/feature_activation/test_feature_service.py @@ -90,7 +90,7 @@ def get_storage(settings: HathorSettings, *, up_to_height: int) -> TransactionSt def get_settings(*, features: dict[Feature, Criteria]) -> HathorSettings: - feature_settings = FeatureSettings.construct( + feature_settings = FeatureSettings.model_construct( evaluation_interval=4, default_threshold=3, features=features, @@ -133,7 +133,7 @@ def test_get_state_first_interval(block_height: int) -> None: ) def test_get_state_from_defined(block_height: int, start_height: int, expected_state: FeatureState) -> None: features = { - Feature.NOP_FEATURE_1: Criteria.construct( + Feature.NOP_FEATURE_1: Criteria.model_construct( bit=Mock(), start_height=start_height, timeout_height=Mock(), @@ -158,7 +158,7 @@ def test_get_state_from_started_to_failed( timeout_height: int, ) -> None: features = { - Feature.NOP_FEATURE_1: Criteria.construct( + Feature.NOP_FEATURE_1: Criteria.model_construct( bit=3, start_height=0, timeout_height=timeout_height, @@ -184,7 +184,7 @@ def test_get_state_from_started_to_must_signal_on_timeout( timeout_height: int, ) -> None: features = { - Feature.NOP_FEATURE_1: Criteria.construct( + Feature.NOP_FEATURE_1: Criteria.model_construct( bit=3, start_height=0, timeout_height=timeout_height, @@ -210,11 +210,11 @@ def test_get_state_from_started_to_locked_in_on_default_threshold( block_height: int, default_threshold: int ) -> None: - feature_settings = FeatureSettings.construct( + feature_settings = FeatureSettings.model_construct( evaluation_interval=4, default_threshold=default_threshold, features={ - Feature.NOP_FEATURE_1: Criteria.construct( + Feature.NOP_FEATURE_1: Criteria.model_construct( bit=1, start_height=0, timeout_height=400, @@ -241,7 +241,7 @@ def test_get_state_from_started_to_locked_in_on_custom_threshold( custom_threshold: int ) -> None: features = { - Feature.NOP_FEATURE_1: Criteria.construct( + Feature.NOP_FEATURE_1: Criteria.model_construct( bit=1, start_height=0, timeout_height=400, @@ -275,7 +275,7 @@ def test_get_state_from_started_to_started( timeout_height: int, ) -> None: features = { - Feature.NOP_FEATURE_1: Criteria.construct( + Feature.NOP_FEATURE_1: Criteria.model_construct( bit=3, start_height=0, timeout_height=timeout_height, @@ -299,7 +299,7 @@ def test_get_state_from_must_signal_to_locked_in( block_height: int, ) -> None: features = { - Feature.NOP_FEATURE_1: Criteria.construct( + Feature.NOP_FEATURE_1: Criteria.model_construct( bit=3, start_height=0, timeout_height=8, @@ -325,7 +325,7 @@ def test_get_state_from_locked_in_to_active( minimum_activation_height: int, ) -> None: features = { - Feature.NOP_FEATURE_1: Criteria.construct( + Feature.NOP_FEATURE_1: Criteria.model_construct( bit=3, start_height=0, timeout_height=8, @@ -352,7 +352,7 @@ def test_get_state_from_locked_in_to_locked_in( minimum_activation_height: int, ) -> None: features = { - Feature.NOP_FEATURE_1: Criteria.construct( + Feature.NOP_FEATURE_1: Criteria.model_construct( bit=3, start_height=0, timeout_height=8, @@ -375,7 +375,7 @@ def test_get_state_from_locked_in_to_locked_in( @pytest.mark.parametrize('block_height', [20, 21, 22, 23]) def test_get_state_from_active(block_height: int) -> None: features = { - Feature.NOP_FEATURE_1: Criteria.construct( + Feature.NOP_FEATURE_1: Criteria.model_construct( bit=3, start_height=0, timeout_height=8, @@ -397,7 +397,7 @@ def test_get_state_from_active(block_height: int) -> None: @pytest.mark.parametrize('block_height', [16, 17, 18, 19]) def test_caching_mechanism(block_height: int) -> None: features = { - Feature.NOP_FEATURE_1: Criteria.construct( + Feature.NOP_FEATURE_1: Criteria.model_construct( bit=3, start_height=0, timeout_height=8, @@ -431,7 +431,7 @@ def test_caching_mechanism(block_height: int) -> None: @pytest.mark.parametrize('block_height', [16, 17, 18, 19]) def test_is_feature_active(block_height: int) -> None: features = { - Feature.NOP_FEATURE_1: Criteria.construct( + Feature.NOP_FEATURE_1: Criteria.model_construct( bit=3, start_height=0, timeout_height=8, @@ -456,7 +456,7 @@ def test_is_feature_active(block_height: int) -> None: @pytest.mark.parametrize('block_height', [12, 13, 14, 15]) def test_get_state_from_failed(block_height: int) -> None: features = { - Feature.NOP_FEATURE_1: Criteria.construct( + Feature.NOP_FEATURE_1: Criteria.model_construct( bit=Mock(), start_height=0, timeout_height=8, @@ -485,8 +485,8 @@ def test_get_state_undefined_feature() -> None: def test_get_feature_info() -> None: - criteria_mock_1 = Criteria.construct(bit=Mock(), start_height=Mock(), timeout_height=Mock(), version=Mock()) - criteria_mock_2 = Criteria.construct(bit=Mock(), start_height=Mock(), timeout_height=Mock(), version=Mock()) + criteria_mock_1 = Criteria.model_construct(bit=Mock(), start_height=Mock(), timeout_height=Mock(), version=Mock()) + criteria_mock_2 = Criteria.model_construct(bit=Mock(), start_height=Mock(), timeout_height=Mock(), version=Mock()) settings = get_settings(features={ Feature.NOP_FEATURE_1: criteria_mock_1, Feature.NOP_FEATURE_2: criteria_mock_2 diff --git a/hathor_tests/feature_activation/test_settings.py b/hathor_tests/feature_activation/test_settings.py index b2c7eac9a..ca5fd080a 100644 --- a/hathor_tests/feature_activation/test_settings.py +++ b/hathor_tests/feature_activation/test_settings.py @@ -121,15 +121,15 @@ def test_conflicting_bits(features: list[dict[str, Any]]) -> None: FeatureSettings(**data) # type: ignore[arg-type] errors = e.value.errors() - assert errors[0]['msg'] == 'At least one pair of Features have the same bit configured for an overlapping ' \ - 'interval: NOP_FEATURE_1 and NOP_FEATURE_2' + assert errors[0]['msg'] == 'Value error, At least one pair of Features have the same bit configured for an ' \ + 'overlapping interval: NOP_FEATURE_1 and NOP_FEATURE_2' @pytest.mark.parametrize( ['evaluation_interval', 'default_threshold', 'error'], [ - (10, 50, 'default_threshold must not be greater than evaluation_interval: 50 > 10'), - (100, 101, 'default_threshold must not be greater than evaluation_interval: 101 > 100') + (10, 50, 'Value error, default_threshold must not be greater than evaluation_interval: 50 > 10'), + (100, 101, 'Value error, default_threshold must not be greater than evaluation_interval: 101 > 100') ] ) def test_default_threshold(evaluation_interval: int, default_threshold: int, error: str) -> None: diff --git a/hathor_tests/nanocontracts/test_fallback_method.py b/hathor_tests/nanocontracts/test_fallback_method.py index 0a7d6deb9..c773e82f5 100644 --- a/hathor_tests/nanocontracts/test_fallback_method.py +++ b/hathor_tests/nanocontracts/test_fallback_method.py @@ -94,7 +94,7 @@ def test_fallback_only_args_success(self) -> None: last_call_info = self.runner.get_last_call_info() assert last_call_info.nc_logger.__entries__ == [ - NCCallBeginEntry.construct( + NCCallBeginEntry.model_construct( timestamp=ANY, nc_id=self.contract_id, call_type=CallType.PUBLIC, @@ -102,7 +102,7 @@ def test_fallback_only_args_success(self) -> None: str_args="('unknown', NCParsedArgs(args=('hello', 123), kwargs={}))", actions=[dict(amount=123, token_uid='00', type='deposit')] ), - NCCallEndEntry.construct(timestamp=ANY), + NCCallEndEntry.model_construct(timestamp=ANY), ] def test_fallback_only_kwargs_success(self) -> None: @@ -111,7 +111,7 @@ def test_fallback_only_kwargs_success(self) -> None: last_call_info = self.runner.get_last_call_info() assert last_call_info.nc_logger.__entries__ == [ - NCCallBeginEntry.construct( + NCCallBeginEntry.model_construct( timestamp=ANY, nc_id=self.contract_id, call_type=CallType.PUBLIC, @@ -119,7 +119,7 @@ def test_fallback_only_kwargs_success(self) -> None: str_args="('unknown', NCParsedArgs(args=(), kwargs={'greeting': 'hello', 'x': 123}))", actions=[dict(amount=123, token_uid='00', type='deposit')] ), - NCCallEndEntry.construct(timestamp=ANY), + NCCallEndEntry.model_construct(timestamp=ANY), ] def test_fallback_args_kwargs_success(self) -> None: @@ -128,7 +128,7 @@ def test_fallback_args_kwargs_success(self) -> None: last_call_info = self.runner.get_last_call_info() assert last_call_info.nc_logger.__entries__ == [ - NCCallBeginEntry.construct( + NCCallBeginEntry.model_construct( timestamp=ANY, nc_id=self.contract_id, call_type=CallType.PUBLIC, @@ -136,7 +136,7 @@ def test_fallback_args_kwargs_success(self) -> None: str_args="('unknown', NCParsedArgs(args=('hello',), kwargs={'x': 123}))", actions=[dict(amount=123, token_uid='00', type='deposit')] ), - NCCallEndEntry.construct(timestamp=ANY), + NCCallEndEntry.model_construct(timestamp=ANY), ] def test_cannot_call_fallback_directly(self) -> None: @@ -158,7 +158,7 @@ def test_fallback_args_bytes_success(self) -> None: last_call_info = self.runner.get_last_call_info() assert last_call_info.nc_logger.__entries__ == [ - NCCallBeginEntry.construct( + NCCallBeginEntry.model_construct( timestamp=ANY, nc_id=self.contract_id, call_type=CallType.PUBLIC, @@ -166,7 +166,7 @@ def test_fallback_args_bytes_success(self) -> None: str_args=f"('unknown', NCRawArgs('{args_bytes.hex()}'))", actions=[dict(amount=123, token_uid='00', type='deposit')] ), - NCCallEndEntry.construct(timestamp=ANY), + NCCallEndEntry.model_construct(timestamp=ANY), ] def test_dag_fallback(self) -> None: diff --git a/hathor_tests/nanocontracts/test_nc_exec_logs.py b/hathor_tests/nanocontracts/test_nc_exec_logs.py index f5972a3f8..0c09ffcb4 100644 --- a/hathor_tests/nanocontracts/test_nc_exec_logs.py +++ b/hathor_tests/nanocontracts/test_nc_exec_logs.py @@ -92,19 +92,19 @@ def _get_initialize_entries(self, tx: Transaction) -> list[NCCallBeginEntry | NC assert self.manager.tx_storage.nc_catalog is not None blueprint_class = self.manager.tx_storage.nc_catalog.blueprints[nano_header.nc_id] return [ - NCCallBeginEntry.construct( - nc_id=tx.hash, + NCCallBeginEntry.model_construct( + nc_id=ContractId(tx.hash), call_type=CallType.PUBLIC, method_name='initialize', timestamp=ANY, actions=[], ), - NCLogEntry.construct( + NCLogEntry.model_construct( level=NCLogLevel.INFO, message=f'initialize() called on {blueprint_class.__name__}', timestamp=ANY, ), - NCCallEndEntry.construct(timestamp=ANY), + NCCallEndEntry.model_construct(timestamp=ANY), ] def _prepare(self, nc_log_config: NCLogConfig = NCLogConfig.ALL) -> None: @@ -279,38 +279,38 @@ def test_log_levels_and_key_values(self) -> None: assert not_none(self.nc_log_storage.get_logs(nc2.hash)).entries == { b2.hash: [NCExecEntry( logs=[ - NCCallBeginEntry.construct( - nc_id=nc1.hash, + NCCallBeginEntry.model_construct( + nc_id=ContractId(nc1.hash), call_type=CallType.PUBLIC, method_name='log_levels', timestamp=ANY, actions=[], ), - NCLogEntry.construct( + NCLogEntry.model_construct( level=NCLogLevel.DEBUG, message='log_levels() called', key_values=dict(test1='1'), timestamp=ANY, ), - NCLogEntry.construct( + NCLogEntry.model_construct( level=NCLogLevel.INFO, message='log_levels() called', key_values=dict(test2='2'), timestamp=ANY, ), - NCLogEntry.construct( + NCLogEntry.model_construct( level=NCLogLevel.WARN, message='log_levels() called', key_values=dict(test3='3'), timestamp=ANY, ), - NCLogEntry.construct( + NCLogEntry.model_construct( level=NCLogLevel.ERROR, message='log_levels() called', key_values=dict(test4='4'), timestamp=ANY, ), - NCCallEndEntry.construct(timestamp=ANY), + NCCallEndEntry.model_construct(timestamp=ANY), ], )], } @@ -319,13 +319,13 @@ def test_log_levels_and_key_values(self) -> None: assert not_none(self.nc_log_storage.get_logs(nc2.hash, log_level=NCLogLevel.WARN)).entries == { b2.hash: [NCExecEntry( logs=[ - NCLogEntry.construct( + NCLogEntry.model_construct( level=NCLogLevel.WARN, message='log_levels() called', key_values=dict(test3='3'), timestamp=ANY, ), - NCLogEntry.construct( + NCLogEntry.model_construct( level=NCLogLevel.ERROR, message='log_levels() called', key_values=dict(test4='4'), @@ -364,17 +364,17 @@ def test_nc_fail(self) -> None: result = not_none(self.nc_log_storage.get_logs(nc2.hash)) assert result.entries == { - b2.hash: [NCExecEntry.construct( + b2.hash: [NCExecEntry.model_construct( error_traceback=ANY, logs=[ - NCCallBeginEntry.construct( - nc_id=nc1.hash, + NCCallBeginEntry.model_construct( + nc_id=ContractId(nc1.hash), call_type=CallType.PUBLIC, method_name='fail', timestamp=ANY, actions=[], ), - NCLogEntry.construct(level=NCLogLevel.WARN, message='fail() called', timestamp=ANY), + NCLogEntry.model_construct(level=NCLogLevel.WARN, message='fail() called', timestamp=ANY), ], )], } @@ -413,17 +413,17 @@ def test_value_error(self) -> None: result = not_none(self.nc_log_storage.get_logs(nc2.hash)) assert result.entries == { - b2.hash: [NCExecEntry.construct( + b2.hash: [NCExecEntry.model_construct( error_traceback=ANY, logs=[ - NCCallBeginEntry.construct( - nc_id=nc1.hash, + NCCallBeginEntry.model_construct( + nc_id=ContractId(nc1.hash), call_type=CallType.PUBLIC, method_name='value_error', timestamp=ANY, actions=[], ), - NCLogEntry.construct(level=NCLogLevel.WARN, message='value_error() called', timestamp=ANY), + NCLogEntry.model_construct(level=NCLogLevel.WARN, message='value_error() called', timestamp=ANY), ], )], } @@ -541,8 +541,8 @@ def test_call_another_contract_public(self) -> None: b2.hash: [NCExecEntry( error_traceback=None, logs=[ - NCCallBeginEntry.construct( - nc_id=nc1.hash, + NCCallBeginEntry.model_construct( + nc_id=ContractId(nc1.hash), call_type=CallType.PUBLIC, method_name='call_another_public', str_args=str((nc2.hash,)), @@ -555,14 +555,14 @@ def test_call_another_contract_public(self) -> None: ) ], ), - NCLogEntry.construct( + NCLogEntry.model_construct( level=NCLogLevel.DEBUG, message='call_another_public() called on MyBlueprint1', key_values=dict(contract_id=nc2.hash_hex), timestamp=ANY, ), - NCCallBeginEntry.construct( - nc_id=nc2.hash, + NCCallBeginEntry.model_construct( + nc_id=ContractId(nc2.hash), call_type=CallType.PUBLIC, method_name='sum', str_args=str((1, 2)), @@ -575,33 +575,33 @@ def test_call_another_contract_public(self) -> None: ) ], ), - NCLogEntry.construct( + NCLogEntry.model_construct( level=NCLogLevel.DEBUG, message='sum() called on MyBlueprint2', key_values=dict(a='1', b='2'), timestamp=ANY ), - NCCallEndEntry.construct(timestamp=ANY), - NCCallBeginEntry.construct( - nc_id=nc2.hash, + NCCallEndEntry.model_construct(timestamp=ANY), + NCCallBeginEntry.model_construct( + nc_id=ContractId(nc2.hash), call_type=CallType.VIEW, method_name='hello_world', timestamp=ANY, actions=None, ), - NCLogEntry.construct( + NCLogEntry.model_construct( level=NCLogLevel.DEBUG, message='hello_world() called on MyBlueprint2', timestamp=ANY, ), - NCCallEndEntry.construct(timestamp=ANY), - NCLogEntry.construct( + NCCallEndEntry.model_construct(timestamp=ANY), + NCLogEntry.model_construct( level=NCLogLevel.DEBUG, message='results on MyBlueprint1', key_values=dict(result1='3', result2='hello world'), timestamp=ANY ), - NCCallEndEntry.construct(timestamp=ANY), + NCCallEndEntry.model_construct(timestamp=ANY), ], )], } diff --git a/hathor_tests/others/test_cli_builder.py b/hathor_tests/others/test_cli_builder.py index ba13309d7..53925ed95 100644 --- a/hathor_tests/others/test_cli_builder.py +++ b/hathor_tests/others/test_cli_builder.py @@ -26,7 +26,7 @@ def setUp(self): def _build_with_error(self, cmd_args: list[str], err_msg: str) -> None: raw_args = self.parser.parse_args(cmd_args) - args = RunNodeArgs.parse_obj(vars(raw_args)) + args = RunNodeArgs.model_validate(vars(raw_args)) builder = CliBuilder(args) with self.assertRaises(BuilderError) as cm: manager = builder.create_manager(self.reactor) @@ -36,7 +36,7 @@ def _build_with_error(self, cmd_args: list[str], err_msg: str) -> None: def _build(self, cmd_args: list[str]) -> HathorManager: raw_args = self.parser.parse_args(cmd_args) - args = RunNodeArgs.parse_obj(vars(raw_args)) + args = RunNodeArgs.model_validate(vars(raw_args)) builder = CliBuilder(args) manager = builder.create_manager(self.reactor) self.assertIsNotNone(manager) diff --git a/hathor_tests/others/test_hathor_settings.py b/hathor_tests/others/test_hathor_settings.py index 009e7f8bb..9b22dcb06 100644 --- a/hathor_tests/others/test_hathor_settings.py +++ b/hathor_tests/others/test_hathor_settings.py @@ -72,10 +72,10 @@ def test_valid_hathor_settings_from_yaml(filepath): @pytest.mark.parametrize( ['filepath', 'error'], [ - ('fixtures/invalid_byte_hathor_settings_fixture.yml', "expected 'str' or 'bytes', got 64"), + ('fixtures/invalid_byte_hathor_settings_fixture.yml', "Value error, expected 'str' or 'bytes', got 64"), ( 'fixtures/invalid_features_hathor_settings_fixture.yml', - 'Should be a multiple of evaluation_interval: 2001 % 1000 != 0' + 'Value error, timeout_height should be a multiple of evaluation_interval: 2001 % 1000 != 0' ) ] ) diff --git a/hathor_tests/poa/test_poa.py b/hathor_tests/poa/test_poa.py index 9534a7d9c..65ad84e2b 100644 --- a/hathor_tests/poa/test_poa.py +++ b/hathor_tests/poa/test_poa.py @@ -86,7 +86,7 @@ def get_signer() -> tuple[PoaSigner, bytes]: public_key = private_key.public_key() public_key_bytes = get_public_key_bytes_compressed(public_key) address = get_address_b58_from_public_key(public_key) - file = PoaSignerFile.parse_obj(dict( + file = PoaSignerFile.model_validate(dict( private_key_hex=private_key_bytes.hex(), public_key_hex=public_key_bytes.hex(), address=address @@ -95,7 +95,7 @@ def get_signer() -> tuple[PoaSigner, bytes]: poa_signer, public_key_bytes = get_signer() settings = Mock(spec_set=HathorSettings) - settings.CONSENSUS_ALGORITHM = PoaSettings.construct(signers=()) + settings.CONSENSUS_ALGORITHM = PoaSettings.model_construct(signers=()) settings.AVG_TIME_BETWEEN_BLOCKS = 30 block_verifier = PoaBlockVerifier(settings=settings) storage = Mock() @@ -132,7 +132,7 @@ def get_signer() -> tuple[PoaSigner, bytes]: block.timestamp = 153 # Test no signers - settings.CONSENSUS_ALGORITHM = PoaSettings.construct(signers=()) + settings.CONSENSUS_ALGORITHM = PoaSettings.model_construct(signers=()) with pytest.raises(PoaValidationError) as e: block_verifier.verify_poa(block) assert str(e.value) == 'invalid PoA signature' @@ -259,7 +259,7 @@ def get_signer() -> tuple[PoaSigner, bytes]: ] ) def test_get_signer_index_distance(n_signers: int, height: int, signer_index: int, expected: int) -> None: - settings = PoaSettings.construct(signers=tuple(PoaSignerSettings(public_key=b'') for _ in range(n_signers))) + settings = PoaSettings.model_construct(signers=tuple(PoaSignerSettings(public_key=b'') for _ in range(n_signers))) result = poa.get_signer_index_distance(settings=settings, signer_index=signer_index, height=height) assert result == expected @@ -291,7 +291,7 @@ def test_get_signer_index_distance(n_signers: int, height: int, signer_index: in ] ) def test_calculate_weight(n_signers: int, height: int, signer_index: int, expected: float) -> None: - settings = PoaSettings.construct(signers=tuple(PoaSignerSettings(public_key=b'') for _ in range(n_signers))) + settings = PoaSettings.model_construct(signers=tuple(PoaSignerSettings(public_key=b'') for _ in range(n_signers))) block = Mock() block.get_height = Mock(return_value=height) @@ -364,8 +364,8 @@ def test_poa_signer_settings() -> None: # Test fails with pytest.raises(ValidationError) as e: _ = PoaSignerSettings(public_key=b'some_key', start_height=10, end_height=10) - assert 'end_height (10) must be greater than start_height (10)' in str(e.value) + assert 'Value error, end_height (10) must be greater than start_height (10)' in str(e.value) with pytest.raises(ValidationError) as e: _ = PoaSignerSettings(public_key=b'some_key', start_height=10, end_height=5) - assert 'end_height (5) must be greater than start_height (10)' in str(e.value) + assert 'Value error, end_height (5) must be greater than start_height (10)' in str(e.value) diff --git a/hathor_tests/resources/event/test_event.py b/hathor_tests/resources/event/test_event.py index ebc670750..050348d2f 100644 --- a/hathor_tests/resources/event/test_event.py +++ b/hathor_tests/resources/event/test_event.py @@ -42,7 +42,7 @@ def web(): @pytest.fixture def data(): - return EventMocker.tx_data.dict() + return EventMocker.tx_data.model_dump() def test_get_events(web, data): diff --git a/hathor_tests/sysctl/test_sysctl.py b/hathor_tests/sysctl/test_sysctl.py index f676bdf74..2fb96580b 100644 --- a/hathor_tests/sysctl/test_sysctl.py +++ b/hathor_tests/sysctl/test_sysctl.py @@ -12,8 +12,8 @@ class SysctlTest(unittest.TestCase): - # We need this patch because pydantic.validate_arguments fails when it gets a mock function. - @patch('hathor.sysctl.sysctl.validate_arguments', new=lambda x: x) # type: ignore + # We need this patch because pydantic.validate_call fails when it gets a mock function. + @patch('hathor.sysctl.sysctl.validate_call', new=lambda x: x) # type: ignore def setUp(self) -> None: super().setUp() diff --git a/hathor_tests/utils_modules/test_named_tuple.py b/hathor_tests/utils_modules/test_named_tuple.py index e56aaf8f4..c05d46b95 100644 --- a/hathor_tests/utils_modules/test_named_tuple.py +++ b/hathor_tests/utils_modules/test_named_tuple.py @@ -35,16 +35,16 @@ class OuterTuple(NamedTuple): b: InnerTuple c: InnerModel - @classmethod - def validate_a(cls, a: int) -> int: - if a > 10: - raise ValueError('"a" cannot be greater than 10') - return a +def _validate_a(a: int) -> int: + """Validator for 'a' field - must not be greater than 10.""" + if a > 10: + raise ValueError('"a" cannot be greater than 10') + return a VALIDATORS = dict( - validate_a=pydantic.validator('a')(OuterTuple.validate_a) + validate_a=pydantic.field_validator('a', mode='before')(_validate_a) ) @@ -86,4 +86,4 @@ def test_validated_named_tuple_from_dict_error(attributes): validated_named_tuple_from_dict(OuterTuple, attributes, validators=VALIDATORS) errors = e.value.errors() - assert errors[0]['msg'] == '"a" cannot be greater than 10' + assert errors[0]['msg'] == 'Value error, "a" cannot be greater than 10' diff --git a/poetry.lock b/poetry.lock index fda97f764..f780e4ea1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -124,6 +124,18 @@ files = [ [package.dependencies] frozenlist = ">=1.1.0" +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "appnope" version = "0.1.3" @@ -1613,57 +1625,137 @@ files = [ [[package]] name = "pydantic" -version = "1.10.26" -description = "Data validation and settings management using python type hints" +version = "2.11.10" +description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic-1.10.26-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f7ae36fa0ecef8d39884120f212e16c06bb096a38f523421278e2f39c1784546"}, - {file = "pydantic-1.10.26-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d95a76cf503f0f72ed7812a91de948440b2bf564269975738a4751e4fadeb572"}, - {file = "pydantic-1.10.26-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a943ce8e00ad708ed06a1d9df5b4fd28f5635a003b82a4908ece6f24c0b18464"}, - {file = "pydantic-1.10.26-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:465ad8edb29b15c10b779b16431fe8e77c380098badf6db367b7a1d3e572cf53"}, - {file = "pydantic-1.10.26-cp310-cp310-win_amd64.whl", hash = "sha256:80e6be6272839c8a7641d26ad569ab77772809dd78f91d0068dc0fc97f071945"}, - {file = "pydantic-1.10.26-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:116233e53889bcc536f617e38c1b8337d7fa9c280f0fd7a4045947515a785637"}, - {file = "pydantic-1.10.26-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3cfdd361addb6eb64ccd26ac356ad6514cee06a61ab26b27e16b5ed53108f77"}, - {file = "pydantic-1.10.26-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e4451951a9a93bf9a90576f3e25240b47ee49ab5236adccb8eff6ac943adf0f"}, - {file = "pydantic-1.10.26-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9858ed44c6bea5f29ffe95308db9e62060791c877766c67dd5f55d072c8612b5"}, - {file = "pydantic-1.10.26-cp311-cp311-win_amd64.whl", hash = "sha256:ac1089f723e2106ebde434377d31239e00870a7563245072968e5af5cc4d33df"}, - {file = "pydantic-1.10.26-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:468d5b9cacfcaadc76ed0a4645354ab6f263ec01a63fb6d05630ea1df6ae453f"}, - {file = "pydantic-1.10.26-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2c1b0b914be31671000ca25cf7ea17fcaaa68cfeadf6924529c5c5aa24b7ab1f"}, - {file = "pydantic-1.10.26-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15b13b9f8ba8867095769e1156e0d7fbafa1f65b898dd40fd1c02e34430973cb"}, - {file = "pydantic-1.10.26-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad7025ca324ae263d4313998e25078dcaec5f9ed0392c06dedb57e053cc8086b"}, - {file = "pydantic-1.10.26-cp312-cp312-win_amd64.whl", hash = "sha256:4482b299874dabb88a6c3759e3d85c6557c407c3b586891f7d808d8a38b66b9c"}, - {file = "pydantic-1.10.26-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ae7913bb40a96c87e3d3f6fe4e918ef53bf181583de4e71824360a9b11aef1c"}, - {file = "pydantic-1.10.26-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8154c13f58d4de5d3a856bb6c909c7370f41fb876a5952a503af6b975265f4ba"}, - {file = "pydantic-1.10.26-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8af0507bf6118b054a9765fb2e402f18a8b70c964f420d95b525eb711122d62"}, - {file = "pydantic-1.10.26-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dcb5a7318fb43189fde6af6f21ac7149c4bcbcfffc54bc87b5becddc46084847"}, - {file = "pydantic-1.10.26-cp313-cp313-win_amd64.whl", hash = "sha256:71cde228bc0600cf8619f0ee62db050d1880dcc477eba0e90b23011b4ee0f314"}, - {file = "pydantic-1.10.26-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6b40730cc81d53d515dc0b8bb5c9b43fadb9bed46de4a3c03bd95e8571616dba"}, - {file = "pydantic-1.10.26-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c3bbb9c0eecdf599e4db9b372fa9cc55be12e80a0d9c6d307950a39050cb0e37"}, - {file = "pydantic-1.10.26-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc2e3fe7bc4993626ef6b6fa855defafa1d6f8996aa1caef2deb83c5ac4d043a"}, - {file = "pydantic-1.10.26-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:36d9e46b588aaeb1dcd2409fa4c467fe0b331f3cc9f227b03a7a00643704e962"}, - {file = "pydantic-1.10.26-cp314-cp314-win_amd64.whl", hash = "sha256:81ce3c8616d12a7be31b4aadfd3434f78f6b44b75adbfaec2fe1ad4f7f999b8c"}, - {file = "pydantic-1.10.26-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc5c91a3b3106caf07ac6735ec6efad8ba37b860b9eb569923386debe65039ad"}, - {file = "pydantic-1.10.26-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dde599e0388e04778480d57f49355c9cc7916de818bf674de5d5429f2feebfb6"}, - {file = "pydantic-1.10.26-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8be08b5cfe88e58198722861c7aab737c978423c3a27300911767931e5311d0d"}, - {file = "pydantic-1.10.26-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0141f4bafe5eda539d98c9755128a9ea933654c6ca4306b5059fc87a01a38573"}, - {file = "pydantic-1.10.26-cp38-cp38-win_amd64.whl", hash = "sha256:eb664305ffca8a9766a8629303bb596607d77eae35bb5f32ff9245984881b638"}, - {file = "pydantic-1.10.26-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:502b9d30d18a2dfaf81b7302f6ba0e5853474b1c96212449eb4db912cb604b7d"}, - {file = "pydantic-1.10.26-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0d8f6087bf697dec3bf7ffcd7fe8362674f16519f3151789f33cbe8f1d19fc15"}, - {file = "pydantic-1.10.26-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dd40a99c358419910c85e6f5d22f9c56684c25b5e7abc40879b3b4a52f34ae90"}, - {file = "pydantic-1.10.26-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ce3293b86ca9f4125df02ff0a70be91bc7946522467cbd98e7f1493f340616ba"}, - {file = "pydantic-1.10.26-cp39-cp39-win_amd64.whl", hash = "sha256:1a4e3062b71ab1d5df339ba12c48f9ed5817c5de6cb92a961dd5c64bb32e7b96"}, - {file = "pydantic-1.10.26-py3-none-any.whl", hash = "sha256:c43ad70dc3ce7787543d563792426a16fd7895e14be4b194b5665e36459dd917"}, - {file = "pydantic-1.10.26.tar.gz", hash = "sha256:8c6aa39b494c5af092e690127c283d84f363ac36017106a9e66cb33a22ac412e"}, + {file = "pydantic-2.11.10-py3-none-any.whl", hash = "sha256:802a655709d49bd004c31e865ef37da30b540786a46bfce02333e0e24b5fe29a"}, + {file = "pydantic-2.11.10.tar.gz", hash = "sha256:dc280f0982fbda6c38fada4e476dc0a4f3aeaf9c6ad4c28df68a666ec3c61423"}, ] [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pyflakes" @@ -1851,7 +1943,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -2470,7 +2561,21 @@ files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] -markers = {dev = "implementation_name == \"cpython\""} + +[[package]] +name = "typing-inspection" +version = "0.4.2" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" [[package]] name = "urllib3" @@ -2684,7 +2789,6 @@ files = [ {file = "zope_interface-8.2-cp314-cp314-win_amd64.whl", hash = "sha256:561ce42390bee90bae51cf1c012902a8033b2aaefbd0deed81e877562a116d48"}, {file = "zope_interface-8.2.tar.gz", hash = "sha256:afb20c371a601d261b4f6edb53c3c418c249db1a9717b0baafc9a9bb39ba1224"}, ] -markers = {dev = "implementation_name == \"cpython\""} [package.extras] docs = ["Sphinx", "furo", "repoze.sphinx.autointerface"] @@ -2719,4 +2823,4 @@ sentry = ["sentry-sdk", "structlog-sentry"] [metadata] lock-version = "2.1" python-versions = ">=3.11,<4" -content-hash = "00c97ccd8d0b45e844ae915c1fa9aab9d5616ad0be8562956179a665906ee277" +content-hash = "d34b52881fb2e8c028006d901ad7cbdf33fc762d8be5547b6508fa9fb7ac8e27" diff --git a/pyproject.toml b/pyproject.toml index b4a64f209..926c77f90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ idna = "~3.4" setproctitle = "^1.3.3" sentry-sdk = {version = "^1.5.11", optional = true} structlog-sentry = {version = "^1.4.0", optional = true} -pydantic = "~1.10.26" +pydantic = "^2.0" pyyaml = "^6.0.1" typing-extensions = "~4.12.2" python-healthchecklib = "^0.1.0" @@ -168,9 +168,12 @@ warn_return_any = true # disallow_subclassing_any = true # disallow_untyped_calls = true +# Pydantic v2 mypy plugin configuration +# See: https://docs.pydantic.dev/latest/integrations/mypy/ [tool.pydantic-mypy] -init_typed = true init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = true warn_untyped_fields = true [tool.pytest.ini_options]