from __future__ import annotations import datetime from dataclasses import dataclass from functools import cache from typing import Annotated, Any import phonenumbers import pycountry from fansifter_common.legal_info.types import LegalEntity, PrivacyLink from pydantic import ( BeforeValidator, EmailStr, SerializerFunctionWrapHandler, WithJsonSchema, WrapSerializer, ) from pydantic_core import PydanticCustomError from typing_extensions import TypedDict from preference_center.profile.enums import Gender from preference_center.profile.utils import mask_email, mask_phone_number def serialize_email(value: str, _handler: SerializerFunctionWrapHandler) -> str: return mask_email(value) EmailMaskedStr = Annotated[str, WrapSerializer(serialize_email, when_used="json")] def validate_phone_number(value: str) -> str: try: numobj = phonenumbers.parse(value) except phonenumbers.NumberParseException as exc: raise PydanticCustomError( "invalid_phone_number", "value is not a valid phone number" ) from exc if not phonenumbers.is_valid_number(numobj): raise PydanticCustomError( "invalid_phone_number", "value is not a valid phone number" ) from None return phonenumbers.format_number(numobj, phonenumbers.PhoneNumberFormat.E164) PhoneNumberStr = Annotated[ str, BeforeValidator(validate_phone_number), WithJsonSchema( { "type": "string", "format": "phone", } ), ] def serialize_phone_number(value: str, _handler: SerializerFunctionWrapHandler) -> str: return mask_phone_number(value) PhoneNumberMaskedStr = Annotated[ str, WrapSerializer(serialize_phone_number, when_used="json") ] def validate_birthday(v: str) -> str: try: datetime.datetime.strptime(v, "%m/%d") except ValueError: raise PydanticCustomError( "invalid_birthday_format", "Birthday should match pattern '{pattern}'", {"pattern": "mm/dd"}, ) from None return v BirthdayStr = Annotated[ str, BeforeValidator(validate_birthday), WithJsonSchema( { "type": "string", "format": "birthday", } ), ] @cache def get_country_codes() -> list[str]: return [ getattr(country, "alpha_2") for country in pycountry.countries if hasattr(country, "alpha_2") ] def validate_country_code(value: str) -> str: value = value.upper() if value not in get_country_codes(): raise ValueError(f"{value} is not a valid country code") return value CountryCode = Annotated[str, BeforeValidator(validate_country_code)] def convert_any_value_to_str(value: Any) -> str: if isinstance(value, str): return value elif isinstance(value, datetime.date | datetime.datetime): return value.isoformat() elif value is None: return "" return str(value) AnyValueStr = Annotated[str, BeforeValidator(convert_any_value_to_str)] @dataclass(frozen=True) class ValueChange: old_value: Any new_value: Any class ProfileValues(TypedDict, total=False): email: EmailStr phone_number: PhoneNumberStr | None first_name: str | None last_name: str | None date_of_birth: datetime.date | None birthday: BirthdayStr | None gender: Gender | None country_code: CountryCode | None city: str | None address: str | None zip_code: str | None class SettingsDict(TypedDict): is_date_of_birth_allowed: bool privacy_links: dict[str, list[PrivacyLink]] legal_entity: LegalEntity