diff --git a/.gitignore b/.gitignore index b2bbe971..b2ab9fe2 100644 --- a/.gitignore +++ b/.gitignore @@ -65,4 +65,5 @@ docs/build/ .DS_Store # AI tools -.claude/ \ No newline at end of file +.claude/ +error.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..f9ec9129 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +--- +repos: + - repo: local + hooks: + - id: ruff + name: ruff + entry: ruff check --force-exclude + language: system + types_or: [python, pyi] + require_serial: true + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-merge-conflict + - repo: local + hooks: + - id: ggshield + name: ggshield + entry: ggshield secret scan pre-commit + language: system + stages: [pre-commit] diff --git a/src/auth0/__init__.py b/src/auth0/__init__.py index b3bd7c8a..c751927c 100644 --- a/src/auth0/__init__.py +++ b/src/auth0/__init__.py @@ -1,7 +1,7 @@ # This file was auto-generated by Fern from our API Definition. -from . import management -from . import authentication +from . import authentication, management + from auth0.management.version import __version__ __all__ = ["management", "authentication", "__version__"] diff --git a/src/auth0/authentication/back_channel_login.py b/src/auth0/authentication/back_channel_login.py index d1885f79..b6f8a80f 100644 --- a/src/auth0/authentication/back_channel_login.py +++ b/src/auth0/authentication/back_channel_login.py @@ -1,9 +1,8 @@ -from typing import Any, Optional, Union, List, Dict +import json +from typing import Any, Dict, List, Optional, Union from .base import AuthenticationBase -import json - class BackChannelLogin(AuthenticationBase): """Back-Channel Login endpoint""" diff --git a/src/auth0/authentication/pushed_authorization_requests.py b/src/auth0/authentication/pushed_authorization_requests.py index 12c4fc97..0368b531 100644 --- a/src/auth0/authentication/pushed_authorization_requests.py +++ b/src/auth0/authentication/pushed_authorization_requests.py @@ -3,7 +3,6 @@ from .base import AuthenticationBase - class PushedAuthorizationRequests(AuthenticationBase): """Pushed Authorization Request (PAR) endpoint""" diff --git a/src/auth0/authentication/rest_async.py b/src/auth0/authentication/rest_async.py index 28f572ba..62484a17 100644 --- a/src/auth0/authentication/rest_async.py +++ b/src/auth0/authentication/rest_async.py @@ -4,11 +4,9 @@ from typing import Any import aiohttp - from .exceptions import RateLimitError -from .types import RequestData - from .rest import EmptyResponse, JsonResponse, PlainResponse, Response, RestClient +from .types import RequestData def _clean_params(params: dict[Any, Any] | None) -> dict[Any, Any] | None: diff --git a/src/auth0/authentication/token_verifier.py b/src/auth0/authentication/token_verifier.py index 4c0864ec..1742eebe 100644 --- a/src/auth0/authentication/token_verifier.py +++ b/src/auth0/authentication/token_verifier.py @@ -7,7 +7,6 @@ import jwt import requests - from .exceptions import TokenValidationError if TYPE_CHECKING: diff --git a/src/auth0/management/types/user_response_schema.py b/src/auth0/management/types/user_response_schema.py index 5d1e5842..51d3d881 100644 --- a/src/auth0/management/types/user_response_schema.py +++ b/src/auth0/management/types/user_response_schema.py @@ -1,6 +1,7 @@ # This file was auto-generated by Fern from our API Definition. import typing +from typing import Annotated import pydantic from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel @@ -9,6 +10,16 @@ from .user_identity_schema import UserIdentitySchema from .user_metadata_schema import UserMetadataSchema +StrBool = Annotated[ + bool, + pydantic.BeforeValidator( + lambda x: bool(x) if isinstance(x, bool) else bool(x and str(x).strip()) + ), # Coerces input values (like "", "email_address") into a bool + pydantic.PlainSerializer( + lambda x: 1 if x else 0, return_type=int + ), # Serializes bool to 0 or 1 +] + class UserResponseSchema(UniversalBaseModel): user_id: typing.Optional[str] = pydantic.Field(default=None) @@ -21,7 +32,7 @@ class UserResponseSchema(UniversalBaseModel): Email address of this user. """ - email_verified: typing.Optional[bool] = pydantic.Field(default=None) + email_verified: typing.Optional[StrBool] = pydantic.Field(default=None) """ Whether this email address is verified (true) or unverified (false). """ @@ -43,7 +54,9 @@ class UserResponseSchema(UniversalBaseModel): created_at: typing.Optional[UserDateSchema] = None updated_at: typing.Optional[UserDateSchema] = None - identities: typing.Optional[typing.List[UserIdentitySchema]] = pydantic.Field(default=None) + identities: typing.Optional[typing.List[UserIdentitySchema]] = pydantic.Field( + default=None + ) """ Array of user identity objects when accounts are linked. """ diff --git a/tests/authentication/test_back_channel_login.py b/tests/authentication/test_back_channel_login.py index 7a675966..deca2291 100644 --- a/tests/authentication/test_back_channel_login.py +++ b/tests/authentication/test_back_channel_login.py @@ -1,12 +1,13 @@ +import json import unittest from unittest import mock -import json import requests -from auth0.authentication.exceptions import Auth0Error, RateLimitError from auth0.authentication.back_channel_login import BackChannelLogin +from auth0.authentication.exceptions import Auth0Error + class TestBackChannelLogin(unittest.TestCase): @mock.patch("auth0.authentication.rest.RestClient.post") diff --git a/tests/authentication/test_get_token.py b/tests/authentication/test_get_token.py index 494e542f..301def1e 100644 --- a/tests/authentication/test_get_token.py +++ b/tests/authentication/test_get_token.py @@ -1,9 +1,9 @@ import unittest -import requests from fnmatch import fnmatch from unittest import mock from unittest.mock import ANY +import requests from cryptography.hazmat.primitives import asymmetric, serialization from auth0.authentication.exceptions import RateLimitError diff --git a/tests/authentication/test_pushed_authorization_requests.py b/tests/authentication/test_pushed_authorization_requests.py index e062dabf..7946996d 100644 --- a/tests/authentication/test_pushed_authorization_requests.py +++ b/tests/authentication/test_pushed_authorization_requests.py @@ -1,5 +1,5 @@ -import unittest import json +import unittest from unittest import mock from auth0.authentication.pushed_authorization_requests import PushedAuthorizationRequests diff --git a/tests/authentication/test_token_verifier.py b/tests/authentication/test_token_verifier.py index 783b248c..ebfba8c3 100644 --- a/tests/authentication/test_token_verifier.py +++ b/tests/authentication/test_token_verifier.py @@ -5,6 +5,7 @@ import jwt +from auth0.authentication.exceptions import TokenValidationError from auth0.authentication.token_verifier import ( AsymmetricSignatureVerifier, JwksFetcher, @@ -12,7 +13,6 @@ SymmetricSignatureVerifier, TokenVerifier, ) -from auth0.authentication.exceptions import TokenValidationError RSA_PUB_KEY_1_PEM = b"""-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuGbXWiK3dQTyCbX5xdE4\nyCuYp0AF2d15Qq1JSXT/lx8CEcXb9RbDddl8jGDv+spi5qPa8qEHiK7FwV2KpRE9\n83wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVs\nWXI9C+yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT\n69s7of9+I9l5lsJ9cozf1rxrXX4V1u/SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8\nAziMCxS+VrRPDM+zfvpIJg3JljAh3PJHDiLu902v9w+Iplu1WyoB2aPfitxEhRN0\nYwIDAQAB\n-----END PUBLIC KEY-----\n""" RSA_PUB_KEY_2_PEM = b"""-----BEGIN PUBLIC KEY-----\nMDowDQYJKoZIhvcNAQEBBQADKQAwJgIfAI7TBUCn8e1hj/fNpb5dqMf8Jj6Ja6qN\npqyeOGYEzAIDAQAB\n-----END PUBLIC KEY-----\n""" diff --git a/tests/management/test_user_response_schema.py b/tests/management/test_user_response_schema.py new file mode 100644 index 00000000..570d2ce7 --- /dev/null +++ b/tests/management/test_user_response_schema.py @@ -0,0 +1,53 @@ +from auth0.management.types.user_response_schema import UserResponseSchema + + +class TestEmailVerifiedStrBool: + """Tests that email_verified accepts both bool and string values.""" + + def test_bool_true(self): + user = UserResponseSchema(email_verified=True) + assert user.email_verified is True + + def test_bool_false(self): + user = UserResponseSchema(email_verified=False) + assert user.email_verified is False + + def test_none(self): + user = UserResponseSchema(email_verified=None) + assert user.email_verified is None + + def test_default_none(self): + user = UserResponseSchema() + assert user.email_verified is None + + def test_nonempty_string_is_true(self): + user = UserResponseSchema(email_verified="user@example.com") + assert user.email_verified is True + + def test_empty_string_is_false(self): + user = UserResponseSchema(email_verified="") + assert user.email_verified is False + + def test_whitespace_only_string_is_false(self): + user = UserResponseSchema(email_verified=" ") + assert user.email_verified is False + + def test_serialization_true_to_int(self): + user = UserResponseSchema(email_verified=True) + data = user.dict() + assert data["email_verified"] == 1 + + def test_serialization_false_to_int(self): + user = UserResponseSchema(email_verified=False) + data = user.dict() + assert data["email_verified"] == 0 + + def test_serialization_string_true_to_int(self): + user = UserResponseSchema(email_verified="verified") + data = user.dict() + assert data["email_verified"] == 1 + + def test_serialization_empty_string_to_int(self): + user = UserResponseSchema(email_verified="") + data = user.dict() + assert data["email_verified"] == 0