From 0a8d217a178f989275e128c54acd3bb544a4b082 Mon Sep 17 00:00:00 2001 From: Dom DiPasquale Date: Fri, 22 Dec 2023 11:29:47 -0800 Subject: [PATCH] Migrate to pydantic 2 (#291) * Upgrade & fix models & tests * address deprecated pydantic warnings * remove print statments, uncomment test * fix optional -> None fields --- requirements.txt | 2 +- setup.py | 2 +- src/vonage/ncco_builder/connect_endpoints.py | 49 +++-- src/vonage/ncco_builder/input_types.py | 26 +-- src/vonage/ncco_builder/ncco.py | 205 ++++++++++-------- src/vonage/ncco_builder/pay_prompts.py | 19 +- src/vonage/subaccounts.py | 20 +- src/vonage/verify2.py | 37 ++-- .../ncco_samples/ncco_builder_samples.py | 48 ++-- .../test_connect_endpoints.py | 2 +- tests/test_ncco_builder/test_input_types.py | 8 +- tests/test_ncco_builder/test_ncco_actions.py | 35 +-- tests/test_ncco_builder/test_ncco_builder.py | 4 +- tests/test_verify2.py | 4 +- tests/test_voice.py | 1 - 15 files changed, 260 insertions(+), 202 deletions(-) diff --git a/requirements.txt b/requirements.txt index 32c7341d..6b622a6e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ pytest==7.4.2 responses==0.22.0 coverage -pydantic>=1.10,==1.* +pydantic==2.5.2 bump2version build diff --git a/setup.py b/setup.py index d7a503cf..a57d0cfc 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ "requests>=2.4.2", "pytz>=2018.5", "Deprecated", - "pydantic>=1.10,==1.*", + "pydantic>=2.5.2", ], python_requires=">=3.8", tests_require=["cryptography>=2.3.1"], diff --git a/src/vonage/ncco_builder/connect_endpoints.py b/src/vonage/ncco_builder/connect_endpoints.py index f9b84ff6..d77a0b8c 100644 --- a/src/vonage/ncco_builder/connect_endpoints.py +++ b/src/vonage/ncco_builder/connect_endpoints.py @@ -1,49 +1,62 @@ -from pydantic import BaseModel, HttpUrl, AnyUrl, Field, constr -from typing import Optional, Dict +from pydantic import BaseModel, HttpUrl, AnyUrl, constr, field_serializer +from typing import Dict from typing_extensions import Literal class ConnectEndpoints: class Endpoint(BaseModel): - type: str = None + type: Literal['phone', 'app', 'websocket', 'sip', 'vbc'] = None class PhoneEndpoint(Endpoint): - type = Field('phone', const=True) - number: constr(regex=r'^[1-9]\d{6,14}$') - dtmfAnswer: Optional[constr(regex='^[0-9*#p]+$')] - onAnswer: Optional[Dict[str, HttpUrl]] + type: Literal['phone'] = 'phone' + + number: constr(pattern=r'^[1-9]\d{6,14}$') + dtmfAnswer: constr(pattern='^[0-9*#p]+$') = None + onAnswer: Dict[str, HttpUrl] = None + + @field_serializer('onAnswer') + def serialize_dt(self, oa: Dict[str, HttpUrl], _info): + if oa is None: + return oa + + return {k: str(v) for k, v in oa.items()} class AppEndpoint(Endpoint): - type = Field('app', const=True) + type: Literal['app'] = 'app' user: str class WebsocketEndpoint(Endpoint): - type = Field('websocket', const=True) + type: Literal['websocket'] = 'websocket' + uri: AnyUrl contentType: Literal['audio/l16;rate=16000', 'audio/l16;rate=8000'] - headers: Optional[dict] + headers: dict = None + + @field_serializer('uri') + def serialize_uri(self, uri: AnyUrl, _info): + return str(uri) class SipEndpoint(Endpoint): - type = Field('sip', const=True) + type: Literal['sip'] = 'sip' uri: str - headers: Optional[dict] + headers: dict = None class VbcEndpoint(Endpoint): - type = Field('vbc', const=True) + type: Literal['vbc'] = 'vbc' extension: str @classmethod def create_endpoint_model_from_dict(cls, d) -> Endpoint: if d['type'] == 'phone': - return cls.PhoneEndpoint.parse_obj(d) + return cls.PhoneEndpoint.model_validate(d) elif d['type'] == 'app': - return cls.AppEndpoint.parse_obj(d) + return cls.AppEndpoint.model_validate(d) elif d['type'] == 'websocket': - return cls.WebsocketEndpoint.parse_obj(d) + return cls.WebsocketEndpoint.model_validate(d) elif d['type'] == 'sip': - return cls.WebsocketEndpoint.parse_obj(d) + return cls.WebsocketEndpoint.model_validate(d) elif d['type'] == 'vbc': - return cls.WebsocketEndpoint.parse_obj(d) + return cls.WebsocketEndpoint.model_validate(d) else: raise ValueError( 'Invalid "type" specified for endpoint object. Cannot create a ConnectEndpoints.Endpoint model.' diff --git a/src/vonage/ncco_builder/input_types.py b/src/vonage/ncco_builder/input_types.py index 761ba31d..56737f24 100644 --- a/src/vonage/ncco_builder/input_types.py +++ b/src/vonage/ncco_builder/input_types.py @@ -1,26 +1,26 @@ from pydantic import BaseModel, confloat, conint -from typing import Optional, List +from typing import List class InputTypes: class Dtmf(BaseModel): - timeOut: Optional[conint(ge=0, le=10)] - maxDigits: Optional[conint(ge=1, le=20)] - submitOnHash: Optional[bool] + timeOut: conint(ge=0, le=10) = None + maxDigits: conint(ge=1, le=20) = None + submitOnHash: bool = None class Speech(BaseModel): - uuid: Optional[str] - endOnSilence: Optional[confloat(ge=0.4, le=10.0)] - language: Optional[str] - context: Optional[List[str]] - startTimeout: Optional[conint(ge=1, le=60)] - maxDuration: Optional[conint(ge=1, le=60)] - saveAudio: Optional[bool] + uuid: str = None + endOnSilence: confloat(ge=0.4, le=10.0) = None + language: str = None + context: List[str] = None + startTimeout: conint(ge=1, le=60) = None + maxDuration: conint(ge=1, le=60) = None + saveAudio: bool = None @classmethod def create_dtmf_model(cls, dict) -> Dtmf: - return cls.Dtmf.parse_obj(dict) + return cls.Dtmf.model_validate(dict) @classmethod def create_speech_model(cls, dict) -> Speech: - return cls.Speech.parse_obj(dict) + return cls.Speech.model_validate(dict) diff --git a/src/vonage/ncco_builder/ncco.py b/src/vonage/ncco_builder/ncco.py index de630129..ed1c96d1 100644 --- a/src/vonage/ncco_builder/ncco.py +++ b/src/vonage/ncco_builder/ncco.py @@ -1,6 +1,6 @@ -from pydantic import BaseModel, Field, validator, constr, confloat, conint -from typing import Optional, Union, List -from typing_extensions import Literal +from pydantic import BaseModel, Field, ValidationInfo, field_validator, constr, confloat, conint +from typing import Any, Dict, Union, List +from typing_extensions import Annotated, Literal from .connect_endpoints import ConnectEndpoints from .input_types import InputTypes @@ -11,29 +11,33 @@ class Ncco: class Action(BaseModel): - action: str = None + action: Literal['record', 'conversation', 'connect', + 'talk', 'stream', 'input', 'notify', 'pay'] = None class Record(Action): """Use the record action to record a call or part of a call.""" - action = Field('record', const=True) - format: Optional[Literal['mp3', 'wav', 'ogg']] - split: Optional[Literal['conversation']] - channels: Optional[conint(ge=1, le=32)] - endOnSilence: Optional[conint(ge=3, le=10)] - endOnKey: Optional[constr(regex='^[0-9*#]$')] - timeOut: Optional[conint(ge=3, le=7200)] - beepStart: Optional[bool] - eventUrl: Optional[Union[List[str], str]] - eventMethod: Optional[constr(to_upper=True)] - - @validator('channels') - def enable_split(cls, v, values): + action: Literal['record'] = 'record' + format: Literal['mp3', 'wav', 'ogg'] = None + split: Literal['conversation'] = None + channels: conint(ge=1, le=32) = None + endOnSilence: conint(ge=3, le=10) = None + endOnKey: constr(pattern='^[0-9*#]$') = None + timeOut: conint(ge=3, le=7200) = None + beepStart: bool = None + eventUrl: Union[List[str], str] = None + eventMethod: constr(to_upper=True) = None + + @field_validator('channels') + @classmethod + def enable_split(cls, v, info: ValidationInfo): + values = info.data if values['split'] is None: values['split'] = 'conversation' return v - @validator('eventUrl') + @field_validator('eventUrl') + @classmethod def ensure_url_in_list(cls, v): return Ncco._ensure_object_in_list(v) @@ -42,22 +46,25 @@ class Conversation(Action): while preserving the communication context. Using conversation with the same name reuses the same persisted conversation.""" - action = Field('conversation', const=True) + action: Literal['conversation'] = 'conversation' name: str - musicOnHoldUrl: Optional[Union[List[str], str]] - startOnEnter: Optional[bool] - endOnExit: Optional[bool] - record: Optional[bool] - canSpeak: Optional[List[str]] - canHear: Optional[List[str]] - mute: Optional[bool] - - @validator('musicOnHoldUrl') - def ensure_url_in_list(cls, v): + musicOnHoldUrl: Union[List[str], str] = None + startOnEnter: bool = None + endOnExit: bool = None + record: bool = None + canSpeak: List[str] = None + canHear: List[str] = None + mute: bool = None + + @field_validator('musicOnHoldUrl') + @classmethod + def ensure_url_in_list(cls, v: Any): return Ncco._ensure_object_in_list(v) - @validator('mute') - def can_mute(cls, v, values): + @field_validator('mute') + @classmethod + def can_mute(cls, v, info: ValidationInfo): + values = info.data if 'canSpeak' in values and values['canSpeak'] is not None: raise ValueError('Cannot use mute option if canSpeak option is specified.') return v @@ -65,21 +72,25 @@ def can_mute(cls, v, values): class Connect(Action): """You can use the connect action to connect a call to endpoints such as phone numbers or a VBC extension.""" - action = Field('connect', const=True) - endpoint: Union[dict, ConnectEndpoints.Endpoint, List[dict]] - from_: Optional[constr(regex=r'^[1-9]\d{6,14}$')] - randomFromNumber: Optional[bool] - eventType: Optional[Literal['synchronous']] - timeout: Optional[int] - limit: Optional[conint(le=7200)] - machineDetection: Optional[Literal['continue', 'hangup']] - advancedMachineDetection: Optional[dict] - eventUrl: Optional[Union[List[str], str]] - eventMethod: Optional[constr(to_upper=True)] - ringbackTone: Optional[str] - - @validator('endpoint') - def validate_endpoint(cls, v): + action: Literal['connect'] = 'connect' + endpoint: Union[dict, ConnectEndpoints.Endpoint, List] + from_: Annotated[str, Field(alias='from_', serialization_alias='from', + pattern=r'^[1-9]\d{6,14}$')] = None + + randomFromNumber: bool = None + eventType: Literal['synchronous'] = None + timeout: int = None + limit: conint(le=7200) = None + machineDetection: Literal['continue', 'hangup'] = None + advancedMachineDetection: dict = None + eventUrl: Union[List[str], str] = None + eventMethod: constr(to_upper=True) = None + ringbackTone: str = None + + @field_validator('endpoint') + @classmethod + def validate_endpoint(cls, v: Any): + if type(v) is dict: return [ConnectEndpoints.create_endpoint_model_from_dict(v)] elif type(v) is list: @@ -87,24 +98,24 @@ def validate_endpoint(cls, v): else: return [v] - @validator('from_') - def set_from_field(cls, v, values): - values['from'] = v - - @validator('randomFromNumber') - def check_from_not_set(cls, v, values): - if v is True and 'from' in values: - if values['from'] is not None: + @field_validator('randomFromNumber') + @classmethod + def check_from_not_set(cls, v, info: ValidationInfo): + values = info.data + if v is True and 'from_' in values: + if values['from_'] is not None: raise ValueError( 'Cannot set a "from" ("from_") field and also the "randomFromNumber" = True option' ) return v - @validator('eventUrl') + @field_validator('eventUrl') + @classmethod def ensure_url_in_list(cls, v): return Ncco._ensure_object_in_list(v) - @validator('advancedMachineDetection') + @field_validator('advancedMachineDetection') + @classmethod def validate_advancedMachineDetection(cls, v): if 'behavior' in v and v['behavior'] not in ('continue', 'hangup'): raise ValueError( @@ -116,61 +127,63 @@ def validate_advancedMachineDetection(cls, v): ) return v - class Config: - smart_union = True - class Talk(Action): """The talk action sends synthesized speech to a Conversation.""" - action = Field('talk', const=True) + action: Literal['talk'] = 'talk' text: constr(max_length=1500) - bargeIn: Optional[bool] - loop: Optional[conint(ge=0)] - level: Optional[confloat(ge=-1, le=1)] - language: Optional[str] - style: Optional[int] - premium: Optional[bool] + bargeIn: bool = None + loop: conint(ge=0) = None + level: confloat(ge=-1, le=1) = None + language: str = None + style: int = None + premium: bool = None class Stream(Action): """The stream action allows you to send an audio stream to a Conversation.""" - action = Field('stream', const=True) + action: Literal['stream'] = 'stream' streamUrl: Union[List[str], str] - level: Optional[confloat(ge=-1, le=1)] - bargeIn: Optional[bool] - loop: Optional[conint(ge=0)] + level: confloat(ge=-1, le=1) = None + bargeIn: bool = None + loop: conint(ge=0) = None - @validator('streamUrl') + @field_validator('streamUrl') + @classmethod def ensure_url_in_list(cls, v): return Ncco._ensure_object_in_list(v) class Input(Action): """Collect digits or speech input by the person you are are calling.""" - action = Field('input', const=True) + action: Literal['input'] = 'input' + type: Union[ Literal['dtmf', 'speech'], List[Literal['dtmf']], List[Literal['speech']], List[Literal['dtmf', 'speech']], ] - dtmf: Optional[Union[InputTypes.Dtmf, dict]] - speech: Optional[Union[InputTypes.Speech, dict]] - eventUrl: Optional[Union[List[str], str]] - eventMethod: Optional[constr(to_upper=True)] + dtmf: Union[InputTypes.Dtmf, dict] = None + speech: Union[InputTypes.Speech, dict] = None + eventUrl: Union[List[str], str] = None + eventMethod: constr(to_upper=True) = None - @validator('type', 'eventUrl') + @field_validator('type', 'eventUrl') + @classmethod def ensure_value_in_list(cls, v): return Ncco._ensure_object_in_list(v) - @validator('dtmf') + @field_validator('dtmf') + @classmethod def ensure_input_object_is_dtmf_model(cls, v): if type(v) is dict: return InputTypes.create_dtmf_model(v) else: return v - @validator('speech') + @field_validator('speech') + @classmethod def ensure_input_object_is_speech_model(cls, v): if type(v) is dict: return InputTypes.create_speech_model(v) @@ -180,12 +193,14 @@ def ensure_input_object_is_speech_model(cls, v): class Notify(Action): """Use the notify action to send a custom payload to your event URL.""" - action = Field('notify', const=True) + action: Literal['notify'] = 'notify' + payload: dict eventUrl: Union[List[str], str] - eventMethod: Optional[constr(to_upper=True)] + eventMethod: constr(to_upper=True) = None - @validator('eventUrl') + @field_validator('eventUrl') + @classmethod def ensure_url_in_list(cls, v): return Ncco._ensure_object_in_list(v) @@ -193,29 +208,33 @@ def ensure_url_in_list(cls, v): class Pay(Action): """The pay action collects credit card information with DTMF input in a secure (PCI-DSS compliant) way.""" - action = Field('pay', const=True) + action: Literal['pay'] = 'pay' amount: confloat(ge=0) - currency: Optional[constr(to_lower=True)] - eventUrl: Optional[Union[List[str], str]] - prompts: Optional[Union[List[PayPrompts.TextPrompt], PayPrompts.TextPrompt, dict]] - voice: Optional[Union[PayPrompts.VoicePrompt, dict]] + currency: constr(to_lower=True) = None + eventUrl: Union[List[str], str] = None + prompts: Union[List[PayPrompts.TextPrompt], PayPrompts.TextPrompt, dict] = None + voice: Union[PayPrompts.VoicePrompt, dict] = None - @validator('amount') + @field_validator('amount') + @classmethod def round_amount(cls, v): return round(v, 2) - @validator('eventUrl') + @field_validator('eventUrl') + @classmethod def ensure_url_in_list(cls, v): return Ncco._ensure_object_in_list(v) - @validator('prompts') + @field_validator('prompts') + @classmethod def ensure_text_model(cls, v): if type(v) is dict: return PayPrompts.create_text_model(v) else: return v - @validator('voice') + @field_validator('voice') + @classmethod def ensure_voice_model(cls, v): if type(v) is dict: return PayPrompts.create_voice_model(v) @@ -227,9 +246,9 @@ def build_ncco(*args: Action, actions: List[Action] = None) -> str: ncco = [] if actions is not None: for action in actions: - ncco.append(action.dict(exclude_none=True)) + ncco.append(action.model_dump(exclude_none=True, by_alias=True)) for action in args: - ncco.append(action.dict(exclude_none=True)) + ncco.append(action.model_dump(exclude_none=True, by_alias=True)) return ncco @staticmethod diff --git a/src/vonage/ncco_builder/pay_prompts.py b/src/vonage/ncco_builder/pay_prompts.py index 11ecb404..116acd66 100644 --- a/src/vonage/ncco_builder/pay_prompts.py +++ b/src/vonage/ncco_builder/pay_prompts.py @@ -1,12 +1,12 @@ -from pydantic import BaseModel, validator -from typing import Optional, Dict +from pydantic import BaseModel, ValidationInfo, field_validator, validator +from typing import Dict from typing_extensions import Literal class PayPrompts: class VoicePrompt(BaseModel): - language: Optional[str] - style: Optional[int] + language: str = None + style: int = None class TextPrompt(BaseModel): type: Literal['CardNumber', 'ExpirationDate', 'SecurityCode'] @@ -22,8 +22,11 @@ class TextPrompt(BaseModel): Dict[Literal['text'], str], ] - @validator('errors') - def check_valid_error_format(cls, v, values): + @field_validator('errors') + @classmethod + def check_valid_error_format(cls, v, info: ValidationInfo): + values = info.data + if values['type'] == 'CardNumber': allowed_values = {'InvalidCardType', 'InvalidCardNumber', 'Timeout'} cls.check_allowed_values(v, allowed_values, values['type']) @@ -44,8 +47,8 @@ def check_allowed_values(errors, allowed_values, prompt_type): @classmethod def create_voice_model(cls, dict) -> VoicePrompt: - return cls.VoicePrompt.parse_obj(dict) + return cls.VoicePrompt.model_validate(dict) @classmethod def create_text_model(cls, dict) -> TextPrompt: - return cls.TextPrompt.parse_obj(dict) + return cls.TextPrompt.model_validate(dict) diff --git a/src/vonage/subaccounts.py b/src/vonage/subaccounts.py index bc88fa14..c731f100 100644 --- a/src/vonage/subaccounts.py +++ b/src/vonage/subaccounts.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Union from .errors import SubaccountsError @@ -28,8 +28,8 @@ def list_subaccounts(self): def create_subaccount( self, name: str, - secret: Optional[str] = None, - use_primary_account_balance: Optional[bool] = None, + secret: str = None, + use_primary_account_balance: bool = None, ): params = {'name': name, 'secret': secret} if self._is_boolean(use_primary_account_balance): @@ -52,9 +52,9 @@ def get_subaccount(self, subaccount_key: str): def modify_subaccount( self, subaccount_key: str, - suspended: Optional[bool] = None, - use_primary_account_balance: Optional[bool] = None, - name: Optional[str] = None, + suspended: bool = None, + use_primary_account_balance: bool = None, + name: str = None, ): params = {'name': name} if self._is_boolean(suspended): @@ -72,8 +72,8 @@ def modify_subaccount( def list_credit_transfers( self, start_date: str = default_start_date, - end_date: Optional[str] = None, - subaccount: Optional[str] = None, + end_date: str = None, + subaccount: str = None, ): params = { 'start_date': start_date, @@ -112,8 +112,8 @@ def transfer_credit( def list_balance_transfers( self, start_date: str = default_start_date, - end_date: Optional[str] = None, - subaccount: Optional[str] = None, + end_date: str = None, + subaccount: str = None, ): params = { 'start_date': start_date, diff --git a/src/vonage/verify2.py b/src/vonage/verify2.py index 33cffb44..cb13a353 100644 --- a/src/vonage/verify2.py +++ b/src/vonage/verify2.py @@ -1,11 +1,12 @@ from __future__ import annotations from typing import TYPE_CHECKING +from typing_extensions import Annotated if TYPE_CHECKING: from vonage import Client -from pydantic import BaseModel, ValidationError, validator, conint, constr -from typing import Optional, List +from pydantic import BaseModel, StringConstraints, ValidationError, field_validator, conint +from typing import List import copy import re @@ -32,7 +33,7 @@ def new_request(self, params: dict): self._remove_unnecessary_fraud_check(params) try: params_to_verify = copy.deepcopy(params) - Verify2.VerifyRequest.parse_obj(params_to_verify) + Verify2.VerifyRequest.model_validate(params_to_verify) except (ValidationError, Verify2Error) as err: raise err @@ -67,16 +68,26 @@ def _remove_unnecessary_fraud_check(self, params): class VerifyRequest(BaseModel): brand: str workflow: List[dict] - locale: Optional[str] - channel_timeout: Optional[conint(ge=60, le=900)] - client_ref: Optional[str] - code_length: Optional[conint(ge=4, le=10)] - fraud_check: Optional[bool] - code: Optional[ - constr(min_length=4, max_length=10, regex='^(?=[a-zA-Z0-9]{4,10}$)[a-zA-Z0-9]*$') - ] - - @validator('workflow') + locale: str = None + channel_timeout: conint(ge=60, le=900) = None + client_ref: str = None + code_length: conint(ge=4, le=10) = None + fraud_check: bool = None + code: Annotated[str, StringConstraints( + min_length=4, max_length=10 + )] = None + + @field_validator('code') + @classmethod + def regex_check(cls, c: str): + re_for_code: re.Pattern[str] = re.compile('^(?=[a-zA-Z0-9]{4,10}$)[a-zA-Z0-9]*$') + + if not re_for_code.match(c): + raise ValueError("string does not match regex") + return c + + @field_validator('workflow') + @classmethod def check_valid_workflow(cls, v): for workflow in v: Verify2._check_valid_channel(workflow) diff --git a/tests/test_ncco_builder/ncco_samples/ncco_builder_samples.py b/tests/test_ncco_builder/ncco_samples/ncco_builder_samples.py index 85177fc4..7e3c012a 100644 --- a/tests/test_ncco_builder/ncco_samples/ncco_builder_samples.py +++ b/tests/test_ncco_builder/ncco_samples/ncco_builder_samples.py @@ -1,3 +1,4 @@ +import pytest from vonage import Ncco, ConnectEndpoints, InputTypes, PayPrompts record = Ncco.Record(eventUrl='http://example.com/events') @@ -53,27 +54,34 @@ payload={"message": "world"}, eventUrl=["http://example.com"], eventMethod='PUT' ) -pay_voice_prompt = Ncco.Pay( - amount=99.99, - currency='gbp', - eventUrl='https://example.com/payment', - voice=PayPrompts.VoicePrompt(language='en-GB', style=1), -) -pay_text_prompt = Ncco.Pay( - amount=12.345, - currency='gbp', - eventUrl='https://example.com/payment', - prompts=PayPrompts.TextPrompt( - type='CardNumber', - text='Enter your card number.', - errors={ - 'InvalidCardType': { - 'text': 'The card you are trying to use is not valid for this purchase.' - } - }, - ), -) +def get_pay_voice_prompt(): + with pytest.deprecated_call(): + return Ncco.Pay( + amount=99.99, + currency='gbp', + eventUrl='https://example.com/payment', + voice=PayPrompts.VoicePrompt(language='en-GB', style=1), + ) + + +def get_pay_text_prompt(): + with pytest.deprecated_call(): + return Ncco.Pay( + amount=12.345, + currency='gbp', + eventUrl='https://example.com/payment', + prompts=PayPrompts.TextPrompt( + type='CardNumber', + text='Enter your card number.', + errors={ + 'InvalidCardType': { + 'text': 'The card you are trying to use is not valid for this purchase.' + } + }, + ), + ) + basic_ncco = [{"action": "talk", "text": "hello"}] diff --git a/tests/test_ncco_builder/test_connect_endpoints.py b/tests/test_ncco_builder/test_connect_endpoints.py index 44cc608a..6fa378e2 100644 --- a/tests/test_ncco_builder/test_connect_endpoints.py +++ b/tests/test_ncco_builder/test_connect_endpoints.py @@ -7,7 +7,7 @@ def _action_as_dict(action: Ncco.Action): - return action.dict(exclude_none=True) + return action.model_dump(exclude_none=True) def test_connect_all_endpoints_from_model(): diff --git a/tests/test_ncco_builder/test_input_types.py b/tests/test_ncco_builder/test_input_types.py index 7572ae1a..592e0518 100644 --- a/tests/test_ncco_builder/test_input_types.py +++ b/tests/test_ncco_builder/test_input_types.py @@ -4,14 +4,14 @@ def test_create_dtmf_model(): dtmf = InputTypes.Dtmf(timeOut=5, maxDigits=2, submitOnHash=True) assert type(dtmf) == InputTypes.Dtmf - assert dtmf.dict() == {'maxDigits': 2, 'submitOnHash': True, 'timeOut': 5} + assert dtmf.model_dump() == {'maxDigits': 2, 'submitOnHash': True, 'timeOut': 5} def test_create_dtmf_model_from_dict(): dtmf_dict = {'timeOut': 3, 'maxDigits': 4, 'submitOnHash': True} dtmf_model = InputTypes.create_dtmf_model(dtmf_dict) assert type(dtmf_model) == InputTypes.Dtmf - assert dtmf_model.dict() == {'maxDigits': 4, 'submitOnHash': True, 'timeOut': 3} + assert dtmf_model.model_dump() == {'maxDigits': 4, 'submitOnHash': True, 'timeOut': 3} def test_create_speech_model(): @@ -25,7 +25,7 @@ def test_create_speech_model(): saveAudio=True, ) assert type(speech) == InputTypes.Speech - assert speech.dict() == { + assert speech.model_dump() == { 'uuid': 'my-uuid', 'endOnSilence': 2.5, 'language': 'en-GB', @@ -40,7 +40,7 @@ def test_create_speech_model_from_dict(): speech_dict = {'uuid': 'my-uuid', 'endOnSilence': 2.5, 'maxDuration': 30} speech_model = InputTypes.create_speech_model(speech_dict) assert type(speech_model) == InputTypes.Speech - assert speech_model.dict(exclude_none=True) == { + assert speech_model.model_dump(exclude_none=True) == { 'uuid': 'my-uuid', 'endOnSilence': 2.5, 'maxDuration': 30, diff --git a/tests/test_ncco_builder/test_ncco_actions.py b/tests/test_ncco_builder/test_ncco_actions.py index b7bd028a..361e8bbd 100644 --- a/tests/test_ncco_builder/test_ncco_actions.py +++ b/tests/test_ncco_builder/test_ncco_actions.py @@ -7,7 +7,7 @@ def _action_as_dict(action: Ncco.Action): - return action.dict(exclude_none=True) + return action.model_dump(exclude_none=True, by_alias=True) def test_record_full(): @@ -261,17 +261,19 @@ def test_notify_validation_error(): def test_pay_voice_basic(): - pay = Ncco.Pay(amount='10.00') - assert type(pay) == Ncco.Pay - assert json.dumps(_action_as_dict(pay)) == nas.pay_basic + with pytest.deprecated_call(): + pay = Ncco.Pay(amount='10.00') + assert type(pay) == Ncco.Pay + assert json.dumps(_action_as_dict(pay)) == nas.pay_basic def test_pay_voice_full(): voice_settings = PayPrompts.VoicePrompt(language='en-GB', style=1) - pay = Ncco.Pay( - amount=99.99, currency='gbp', eventUrl='https://example.com/payment', voice=voice_settings - ) - assert json.dumps(_action_as_dict(pay)) == nas.pay_voice_full + with pytest.deprecated_call(): + pay = Ncco.Pay( + amount=99.99, currency='gbp', eventUrl='https://example.com/payment', voice=voice_settings + ) + assert json.dumps(_action_as_dict(pay)) == nas.pay_voice_full def test_pay_text(): @@ -284,10 +286,11 @@ def test_pay_text(): } }, ) - pay = Ncco.Pay( - amount=12.345, currency='gbp', eventUrl='https://example.com/payment', prompts=text_prompts - ) - assert json.dumps(_action_as_dict(pay)) == nas.pay_text + with pytest.deprecated_call(): + pay = Ncco.Pay( + amount=12.345, currency='gbp', eventUrl='https://example.com/payment', prompts=text_prompts + ) + assert json.dumps(_action_as_dict(pay)) == nas.pay_text def test_pay_text_multiple_prompts(): @@ -318,10 +321,12 @@ def test_pay_text_multiple_prompts(): ) text_prompts = [card_prompt, expiration_date_prompt, security_code_prompt] - pay = Ncco.Pay(amount=12, prompts=text_prompts) - assert json.dumps(_action_as_dict(pay)) == nas.pay_text_multiple_prompts + with pytest.deprecated_call(): + pay = Ncco.Pay(amount=12, prompts=text_prompts) + assert json.dumps(_action_as_dict(pay)) == nas.pay_text_multiple_prompts def test_pay_validation_error(): with pytest.raises(ValidationError): - Ncco.Pay(amount='not-valid') + with pytest.deprecated_call(): + Ncco.Pay(amount='not-valid') diff --git a/tests/test_ncco_builder/test_ncco_builder.py b/tests/test_ncco_builder/test_ncco_builder.py index fbabd42c..f3850b5d 100644 --- a/tests/test_ncco_builder/test_ncco_builder.py +++ b/tests/test_ncco_builder/test_ncco_builder.py @@ -33,8 +33,8 @@ def test_build_insane_ncco(): nbs.stream, nbs.input, nbs.notify, - nbs.pay_voice_prompt, - nbs.pay_text_prompt, + nbs.get_pay_voice_prompt(), + nbs.get_pay_text_prompt(), ] ncco = Ncco.build_ncco(actions=action_list) assert ncco == nbs.insane_ncco diff --git a/tests/test_verify2.py b/tests/test_verify2.py index 5a586b28..601a78ee 100644 --- a/tests/test_verify2.py +++ b/tests/test_verify2.py @@ -101,7 +101,7 @@ def test_new_request_sms_custom_code_length_error(): with raises(ValidationError) as err: verify2.new_request(params) - assert 'ensure this value has at least 4 characters' in str(err.value) + assert 'String should have at least 4 characters' in str(err.value) def test_new_request_sms_custom_code_character_error(): @@ -141,7 +141,7 @@ def test_new_request_code_length_error(): with raises(ValidationError) as err: verify2.new_request(params) - assert 'ensure this value is less than or equal to 10' in str(err.value) + assert 'Input should be less than or equal to 10' in str(err.value) def test_new_request_to_error(): diff --git a/tests/test_voice.py b/tests/test_voice.py index 22f26aab..cc6dda52 100644 --- a/tests/test_voice.py +++ b/tests/test_voice.py @@ -159,7 +159,6 @@ def test_user_provided_authorization(dummy_data): token = request_authorization().split()[1] token = jwt.decode(token, dummy_data.public_key, algorithms="RS256") - print(token) assert token["application_id"] == application_id assert token["nbf"] == nbf assert token["exp"] == exp