Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

chore(feat): Support OTP as a type #12

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions pydantic_extra_types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
CountryNumericCode,
CountryOfficialName,
CountryShortName,
OTPToken,
PaymentCardBrand,
PaymentCardNumber,
PhoneNumber,
Expand All @@ -25,4 +26,5 @@
'CountryNumericCode',
'CountryOfficialName',
'PhoneNumber',
'OTPToken',
)
2 changes: 2 additions & 0 deletions pydantic_extra_types/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
CountryOfficialName,
CountryShortName,
)
from pydantic_extra_types.types.otp import OTPToken
from pydantic_extra_types.types.payment import PaymentCardBrand, PaymentCardNumber
from pydantic_extra_types.types.phone_numbers import PhoneNumber
from pydantic_extra_types.types.routing_number import ABARoutingNumber
Expand All @@ -21,4 +22,5 @@
'CountryNumericCode',
'CountryOfficialName',
'PhoneNumber',
'OTPToken',
)
29 changes: 29 additions & 0 deletions pydantic_extra_types/types/otp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from typing import Any

import pyotp
from pydantic_core import PydanticCustomError


class OTPToken(str):
"""A one-time password token.

This is a custom type that can be used to validate a one-time password token
against a secret key. The secret key is passed in the context argument of
the model_validate method.

The type also has a custom JSON encoder that returns the current one-time
password token for the secret key.
"""

@staticmethod
def model_validate(value: Any, *, context: Any) -> Any:
if not pyotp.TOTP(context['otp_secret']).verify(value):
raise PydanticCustomError('Invalid one-time password', value)
return value

@classmethod
def __get_pydantic_core_schema__(cls) -> Any:
yield cls.model_validate
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not how pydantic V2 works, please look at the examples in pydantic.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, this should be breaking all tests, so something must be wrong in tests.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

almost 3 weeks didn't update it i will update it and also changes it, I guess this one would work fine #22


class Config:
json_encoders = {pyotp.TOTP: lambda v: v.now()}
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ classifiers = [
requires-python = '>=3.7'
dependencies = [
'pydantic@git+https://github.com/pydantic/pydantic.git@main',
'phonenumbers'
'phonenumbers',
'pyotp >=2.0.0,<2.8.0',
]
dynamic = ['version']

Expand Down
2 changes: 2 additions & 0 deletions requirements/pyproject.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@ pydantic @ git+https://github.com/pydantic/pydantic.git@main
# via pydantic-extra-types (pyproject.toml)
pydantic-core==0.38.0
# via pydantic
pyotp==2.7.0
# via pydantic-extra-types (pyproject.toml)
typing-extensions==4.6.3
# via pydantic
35 changes: 35 additions & 0 deletions tests/test_types_otp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import pyotp
import pytest
from pydantic_core import PydanticCustomError

from pydantic_extra_types import OTPToken


def test_model_validate():
secret = 'JBSWY3DPEHPK3PXP'
totp = pyotp.TOTP(secret)
value = totp.now()
context = {'otp_secret': secret}
# Test a valid OTP token
result = OTPToken.model_validate(value, context=context)
assert result == value

# Test an invalid OTP token
with pytest.raises(PydanticCustomError, match='Invalid one-time password'):
OTPToken.model_validate('Invalid one-time password', context=context)


def test_json_encoder():
secret = 'JBSWY3DPEHPK3PXP'
totp = pyotp.TOTP(secret)
value = totp.now()

# Test the custom JSON encoder
json_encoded_value = OTPToken.Config.json_encoders[pyotp.TOTP](totp)
assert json_encoded_value == value


def test___get_pydantic_core_schema__():
# Test the get_validators method
validators = list(OTPToken.__get_pydantic_core_schema__())
assert validators == [OTPToken.model_validate]