diff --git a/README.md b/README.md index 2adc764..30e1c25 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ from truelist import Truelist client = Truelist("your-api-key") result = client.email.validate("user@example.com") -print(result.state) # "valid" -print(result.sub_state) # "ok" -print(result.free_email) # True +print(result.state) # "ok" +print(result.sub_state) # "email_ok" +print(result.domain) # "example.com" print(result.is_valid) # True ``` @@ -120,20 +120,16 @@ Set `max_retries=0` to disable retries. ### Email Validation ```python -# Server-side validation result = client.email.validate("user@example.com") - -# Form/frontend validation (different rate limits) -result = client.email.form_validate("user@example.com") ``` ### Account Info ```python account = client.account.get() -print(account.email) # "you@company.com" -print(account.plan) # "pro" -print(account.credits) # 9500 +print(account.email) # "you@company.com" +print(account.name) # "Your Name" +print(account.payment_plan) # "pro" ``` ## Types Reference @@ -143,37 +139,38 @@ print(account.credits) # 9500 | Field | Type | Description | |-------|------|-------------| | `email` | `str` | The email address that was validated | -| `state` | `str` | One of: `valid`, `invalid`, `risky`, `unknown` | +| `domain` | `str` | The domain of the email address | +| `canonical` | `str \| None` | The canonical (local) part of the email | +| `mx_record` | `str \| None` | The MX record for the domain | +| `first_name` | `str \| None` | First name associated with the email, if available | +| `last_name` | `str \| None` | Last name associated with the email, if available | +| `state` | `str` | One of: `ok`, `email_invalid`, `risky`, `unknown`, `accept_all` | | `sub_state` | `str` | Detailed reason (see below) | -| `free_email` | `bool` | Whether the email is from a free provider | -| `role` | `bool` | Whether the email is a role address (e.g., info@) | -| `disposable` | `bool` | Whether the email is from a disposable provider | +| `verified_at` | `str \| None` | Timestamp of when the verification was performed | | `suggestion` | `str \| None` | Suggested correction, if any | -Convenience properties: `is_valid`, `is_invalid`, `is_risky`, `is_unknown` +Convenience properties: `is_valid`, `is_invalid`, `is_risky`, `is_unknown`, `is_disposable`, `is_role` #### Sub-states | Sub-state | Description | |-----------|-------------| -| `ok` | Email is valid | -| `accept_all` | Domain accepts all emails (risky) | -| `disposable_address` | Disposable email provider | -| `role_address` | Role-based address (info@, admin@, etc.) | -| `failed_mx_check` | Domain has no MX records | -| `failed_spam_trap` | Known spam trap | -| `failed_no_mailbox` | Mailbox does not exist | -| `failed_greylisted` | Server temporarily rejected (greylisting) | -| `failed_syntax_check` | Email syntax is invalid | -| `unknown` | Could not determine status | +| `email_ok` | Email is valid | +| `is_disposable` | Disposable email provider | +| `is_role` | Role-based address (info@, admin@, etc.) | +| `unknown_error` | Could not determine status | +| `failed_smtp_check` | SMTP check failed | ### `AccountInfo` | Field | Type | Description | |-------|------|-------------| | `email` | `str` | Account email address | -| `plan` | `str` | Current plan name | -| `credits` | `int` | Remaining validation credits | +| `name` | `str` | Account holder name | +| `uuid` | `str` | Account UUID | +| `time_zone` | `str \| None` | Account time zone | +| `is_admin_role` | `bool` | Whether the user has admin role | +| `payment_plan` | `str` | Current payment plan | ### Exceptions diff --git a/src/truelist/_http.py b/src/truelist/_http.py index d07e406..cb60211 100644 --- a/src/truelist/_http.py +++ b/src/truelist/_http.py @@ -97,12 +97,13 @@ def sync_request( *, max_retries: int, json: dict[str, Any] | None = None, + params: dict[str, str] | None = None, ) -> httpx.Response: last_exc: Exception | None = None for attempt in range(max_retries + 1): try: - response = client.request(method, url, json=json) + response = client.request(method, url, json=json, params=params) except httpx.ConnectError as exc: last_exc = exc if attempt < max_retries: @@ -140,6 +141,7 @@ async def async_request( *, max_retries: int, json: dict[str, Any] | None = None, + params: dict[str, str] | None = None, ) -> httpx.Response: import asyncio @@ -147,7 +149,7 @@ async def async_request( for attempt in range(max_retries + 1): try: - response = await client.request(method, url, json=json) + response = await client.request(method, url, json=json, params=params) except httpx.ConnectError as exc: last_exc = exc if attempt < max_retries: diff --git a/src/truelist/async_client.py b/src/truelist/async_client.py index 4e28e27..9eb732d 100644 --- a/src/truelist/async_client.py +++ b/src/truelist/async_client.py @@ -22,7 +22,7 @@ def __init__(self, client: httpx.AsyncClient, max_retries: int) -> None: self._max_retries = max_retries async def validate(self, email: str) -> ValidationResult: - """Validate an email address using server-side verification. + """Validate an email address. Args: email: The email address to validate. @@ -33,29 +33,9 @@ async def validate(self, email: str) -> ValidationResult: response = await async_request( self._client, "POST", - "/api/v1/verify", + "/api/v1/verify_inline", max_retries=self._max_retries, - json={"email": email}, - ) - return _parse_validation_result(response.json()) - - async def form_validate(self, email: str) -> ValidationResult: - """Validate an email address using form/frontend verification. - - This endpoint has different rate limits suited for frontend form validation. - - Args: - email: The email address to validate. - - Returns: - A ValidationResult with the verification details. - """ - response = await async_request( - self._client, - "POST", - "/api/v1/form_verify", - max_retries=self._max_retries, - json={"email": email}, + params={"email": email}, ) return _parse_validation_result(response.json()) @@ -71,19 +51,22 @@ async def get(self) -> AccountInfo: """Get account information for the authenticated user. Returns: - An AccountInfo with email, plan, and credits. + An AccountInfo with account details. """ response = await async_request( self._client, "GET", - "/api/v1/account", + "/me", max_retries=self._max_retries, ) data: dict[str, Any] = response.json() return AccountInfo( email=data["email"], - plan=data["plan"], - credits=data["credits"], + name=data["name"], + uuid=data["uuid"], + time_zone=data.get("time_zone"), + is_admin_role=data.get("is_admin_role", False), + payment_plan=data["account"]["payment_plan"], ) @@ -144,12 +127,17 @@ async def __aexit__(self, *args: object) -> None: def _parse_validation_result(data: dict[str, Any]) -> ValidationResult: + emails = data["emails"] + email_data = emails[0] return ValidationResult( - email=data["email"], - state=data["state"], - sub_state=data["sub_state"], - free_email=data["free_email"], - role=data["role"], - disposable=data["disposable"], - suggestion=data.get("suggestion"), + email=email_data["address"], + domain=email_data["domain"], + canonical=email_data.get("canonical"), + mx_record=email_data.get("mx_record"), + first_name=email_data.get("first_name"), + last_name=email_data.get("last_name"), + state=email_data["email_state"], + sub_state=email_data["email_sub_state"], + verified_at=email_data.get("verified_at"), + suggestion=email_data.get("did_you_mean"), ) diff --git a/src/truelist/client.py b/src/truelist/client.py index 1c11cf6..8cadc74 100644 --- a/src/truelist/client.py +++ b/src/truelist/client.py @@ -22,7 +22,7 @@ def __init__(self, client: httpx.Client, max_retries: int) -> None: self._max_retries = max_retries def validate(self, email: str) -> ValidationResult: - """Validate an email address using server-side verification. + """Validate an email address. Args: email: The email address to validate. @@ -33,29 +33,9 @@ def validate(self, email: str) -> ValidationResult: response = sync_request( self._client, "POST", - "/api/v1/verify", + "/api/v1/verify_inline", max_retries=self._max_retries, - json={"email": email}, - ) - return _parse_validation_result(response.json()) - - def form_validate(self, email: str) -> ValidationResult: - """Validate an email address using form/frontend verification. - - This endpoint has different rate limits suited for frontend form validation. - - Args: - email: The email address to validate. - - Returns: - A ValidationResult with the verification details. - """ - response = sync_request( - self._client, - "POST", - "/api/v1/form_verify", - max_retries=self._max_retries, - json={"email": email}, + params={"email": email}, ) return _parse_validation_result(response.json()) @@ -71,19 +51,22 @@ def get(self) -> AccountInfo: """Get account information for the authenticated user. Returns: - An AccountInfo with email, plan, and credits. + An AccountInfo with account details. """ response = sync_request( self._client, "GET", - "/api/v1/account", + "/me", max_retries=self._max_retries, ) data: dict[str, Any] = response.json() return AccountInfo( email=data["email"], - plan=data["plan"], - credits=data["credits"], + name=data["name"], + uuid=data["uuid"], + time_zone=data.get("time_zone"), + is_admin_role=data.get("is_admin_role", False), + payment_plan=data["account"]["payment_plan"], ) @@ -143,12 +126,17 @@ def __exit__(self, *args: object) -> None: def _parse_validation_result(data: dict[str, Any]) -> ValidationResult: + emails = data["emails"] + email_data = emails[0] return ValidationResult( - email=data["email"], - state=data["state"], - sub_state=data["sub_state"], - free_email=data["free_email"], - role=data["role"], - disposable=data["disposable"], - suggestion=data.get("suggestion"), + email=email_data["address"], + domain=email_data["domain"], + canonical=email_data.get("canonical"), + mx_record=email_data.get("mx_record"), + first_name=email_data.get("first_name"), + last_name=email_data.get("last_name"), + state=email_data["email_state"], + sub_state=email_data["email_sub_state"], + verified_at=email_data.get("verified_at"), + suggestion=email_data.get("did_you_mean"), ) diff --git a/src/truelist/types.py b/src/truelist/types.py index 6ab9e39..77b3909 100644 --- a/src/truelist/types.py +++ b/src/truelist/types.py @@ -8,22 +8,25 @@ class ValidationResult: """Result of an email validation request.""" email: str + domain: str + canonical: str | None + mx_record: str | None + first_name: str | None + last_name: str | None state: str sub_state: str - free_email: bool - role: bool - disposable: bool + verified_at: str | None suggestion: str | None @property def is_valid(self) -> bool: - """Whether the email state is 'valid'.""" - return self.state == "valid" + """Whether the email state is 'ok'.""" + return self.state == "ok" @property def is_invalid(self) -> bool: - """Whether the email state is 'invalid'.""" - return self.state == "invalid" + """Whether the email state is 'email_invalid'.""" + return self.state == "email_invalid" @property def is_risky(self) -> bool: @@ -35,11 +38,24 @@ def is_unknown(self) -> bool: """Whether the email state is 'unknown'.""" return self.state == "unknown" + @property + def is_disposable(self) -> bool: + """Whether the email sub_state is 'is_disposable'.""" + return self.sub_state == "is_disposable" + + @property + def is_role(self) -> bool: + """Whether the email sub_state is 'is_role'.""" + return self.sub_state == "is_role" + @dataclass(frozen=True) class AccountInfo: """Account information for the authenticated user.""" email: str - plan: str - credits: int + name: str + uuid: str + time_zone: str | None + is_admin_role: bool + payment_plan: str diff --git a/tests/conftest.py b/tests/conftest.py index e7baa70..ce84252 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,81 +10,132 @@ def valid_email_response() -> dict[str, Any]: return { - "email": "user@example.com", - "state": "valid", - "sub_state": "ok", - "free_email": True, - "role": False, - "disposable": False, - "suggestion": None, + "emails": [ + { + "address": "user@example.com", + "domain": "example.com", + "canonical": "user", + "mx_record": None, + "first_name": None, + "last_name": None, + "email_state": "ok", + "email_sub_state": "email_ok", + "verified_at": "2026-02-21T10:00:00.000Z", + "did_you_mean": None, + } + ] } def invalid_email_response() -> dict[str, Any]: return { - "email": "bad@invalid.com", - "state": "invalid", - "sub_state": "failed_no_mailbox", - "free_email": False, - "role": False, - "disposable": False, - "suggestion": None, + "emails": [ + { + "address": "bad@invalid.com", + "domain": "invalid.com", + "canonical": "bad", + "mx_record": None, + "first_name": None, + "last_name": None, + "email_state": "email_invalid", + "email_sub_state": "unknown_error", + "verified_at": "2026-02-21T10:00:00.000Z", + "did_you_mean": None, + } + ] } def risky_email_response() -> dict[str, Any]: return { - "email": "info@company.com", - "state": "risky", - "sub_state": "accept_all", - "free_email": False, - "role": True, - "disposable": False, - "suggestion": None, + "emails": [ + { + "address": "info@company.com", + "domain": "company.com", + "canonical": "info", + "mx_record": "mx.company.com", + "first_name": None, + "last_name": None, + "email_state": "risky", + "email_sub_state": "is_role", + "verified_at": "2026-02-21T10:00:00.000Z", + "did_you_mean": None, + } + ] } def unknown_email_response() -> dict[str, Any]: return { - "email": "mystery@timeout.com", - "state": "unknown", - "sub_state": "failed_greylisted", - "free_email": False, - "role": False, - "disposable": False, - "suggestion": None, + "emails": [ + { + "address": "mystery@timeout.com", + "domain": "timeout.com", + "canonical": "mystery", + "mx_record": None, + "first_name": None, + "last_name": None, + "email_state": "unknown", + "email_sub_state": "failed_smtp_check", + "verified_at": "2026-02-21T10:00:00.000Z", + "did_you_mean": None, + } + ] } def disposable_email_response() -> dict[str, Any]: return { - "email": "temp@mailinator.com", - "state": "invalid", - "sub_state": "disposable_address", - "free_email": True, - "role": False, - "disposable": True, - "suggestion": None, + "emails": [ + { + "address": "temp@mailinator.com", + "domain": "mailinator.com", + "canonical": "temp", + "mx_record": None, + "first_name": None, + "last_name": None, + "email_state": "email_invalid", + "email_sub_state": "is_disposable", + "verified_at": "2026-02-21T10:00:00.000Z", + "did_you_mean": None, + } + ] } def suggestion_email_response() -> dict[str, Any]: return { - "email": "user@gmial.com", - "state": "invalid", - "sub_state": "failed_no_mailbox", - "free_email": False, - "role": False, - "disposable": False, - "suggestion": "user@gmail.com", + "emails": [ + { + "address": "user@gmial.com", + "domain": "gmial.com", + "canonical": "user", + "mx_record": None, + "first_name": None, + "last_name": None, + "email_state": "email_invalid", + "email_sub_state": "unknown_error", + "verified_at": "2026-02-21T10:00:00.000Z", + "did_you_mean": "user@gmail.com", + } + ] } def account_response() -> dict[str, Any]: return { - "email": "owner@truelist.io", - "plan": "pro", - "credits": 9500, + "email": "team@company.com", + "name": "Team Lead", + "uuid": "a3828d19-1234-5678-9abc-def012345678", + "time_zone": "America/New_York", + "is_admin_role": True, + "token": "test_token", + "api_keys": [], + "account": { + "name": "Company Inc", + "payment_plan": "pro", + "users": [], + }, } diff --git a/tests/test_async_client.py b/tests/test_async_client.py index 722b914..e341a36 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -32,7 +32,7 @@ class TestAsyncEmailValidate: @respx.mock async def test_valid_email(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=valid_email_response()) ) async with AsyncTruelist(API_KEY) as client: @@ -40,11 +40,11 @@ async def test_valid_email(self) -> None: assert isinstance(result, ValidationResult) assert result.email == "user@example.com" - assert result.state == "valid" - assert result.sub_state == "ok" - assert result.free_email is True - assert result.role is False - assert result.disposable is False + assert result.domain == "example.com" + assert result.canonical == "user" + assert result.state == "ok" + assert result.sub_state == "email_ok" + assert result.verified_at == "2026-02-21T10:00:00.000Z" assert result.suggestion is None assert result.is_valid is True assert result.is_invalid is False @@ -53,18 +53,18 @@ async def test_valid_email(self) -> None: @respx.mock async def test_invalid_email(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=invalid_email_response()) ) async with AsyncTruelist(API_KEY) as client: result = await client.email.validate("bad@invalid.com") - assert result.state == "invalid" + assert result.state == "email_invalid" assert result.is_invalid is True @respx.mock async def test_risky_email(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=risky_email_response()) ) async with AsyncTruelist(API_KEY) as client: @@ -72,10 +72,11 @@ async def test_risky_email(self) -> None: assert result.state == "risky" assert result.is_risky is True + assert result.is_role is True @respx.mock async def test_unknown_email(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=unknown_email_response()) ) async with AsyncTruelist(API_KEY) as client: @@ -86,17 +87,17 @@ async def test_unknown_email(self) -> None: @respx.mock async def test_disposable_email(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=disposable_email_response()) ) async with AsyncTruelist(API_KEY) as client: result = await client.email.validate("temp@mailinator.com") - assert result.disposable is True + assert result.is_disposable is True @respx.mock async def test_email_with_suggestion(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=suggestion_email_response()) ) async with AsyncTruelist(API_KEY) as client: @@ -105,34 +106,23 @@ async def test_email_with_suggestion(self) -> None: assert result.suggestion == "user@gmail.com" -class TestAsyncEmailFormValidate: - """Tests for async client.email.form_validate().""" - - @respx.mock - async def test_form_validate(self) -> None: - respx.post(f"{BASE_URL}/api/v1/form_verify").mock( - return_value=httpx.Response(200, json=valid_email_response()) - ) - async with AsyncTruelist(API_KEY) as client: - result = await client.email.form_validate("user@example.com") - - assert result.is_valid is True - - class TestAsyncAccountGet: """Tests for async client.account.get().""" @respx.mock async def test_get_account(self) -> None: - respx.get(f"{BASE_URL}/api/v1/account").mock( + respx.get(f"{BASE_URL}/me").mock( return_value=httpx.Response(200, json=account_response()) ) async with AsyncTruelist(API_KEY) as client: account = await client.account.get() - assert account.email == "owner@truelist.io" - assert account.plan == "pro" - assert account.credits == 9500 + assert account.email == "team@company.com" + assert account.name == "Team Lead" + assert account.uuid == "a3828d19-1234-5678-9abc-def012345678" + assert account.time_zone == "America/New_York" + assert account.is_admin_role is True + assert account.payment_plan == "pro" class TestAsyncErrorHandling: @@ -140,7 +130,7 @@ class TestAsyncErrorHandling: @respx.mock async def test_authentication_error(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(401, text="Unauthorized") ) async with AsyncTruelist(API_KEY, max_retries=0) as client: @@ -151,7 +141,7 @@ async def test_authentication_error(self) -> None: @respx.mock async def test_rate_limit_error(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response( 429, text="Too Many Requests", @@ -166,7 +156,7 @@ async def test_rate_limit_error(self) -> None: @respx.mock async def test_server_error(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(500, text="Internal Server Error") ) async with AsyncTruelist(API_KEY, max_retries=0) as client: @@ -177,7 +167,7 @@ async def test_server_error(self) -> None: @respx.mock async def test_timeout_error(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( side_effect=httpx.ReadTimeout("Read timed out") ) async with AsyncTruelist(API_KEY, max_retries=0) as client: @@ -186,7 +176,7 @@ async def test_timeout_error(self) -> None: @respx.mock async def test_connection_error(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( side_effect=httpx.ConnectError("Connection refused") ) async with AsyncTruelist(API_KEY, max_retries=0) as client: @@ -199,7 +189,7 @@ class TestAsyncRetries: @respx.mock async def test_retry_on_500_then_success(self) -> None: - route = respx.post(f"{BASE_URL}/api/v1/verify") + route = respx.post(f"{BASE_URL}/api/v1/verify_inline") route.side_effect = [ httpx.Response(500, text="Error"), httpx.Response(200, json=valid_email_response()), @@ -212,7 +202,7 @@ async def test_retry_on_500_then_success(self) -> None: @respx.mock async def test_retry_on_429_then_success(self) -> None: - route = respx.post(f"{BASE_URL}/api/v1/verify") + route = respx.post(f"{BASE_URL}/api/v1/verify_inline") route.side_effect = [ httpx.Response(429, text="Rate Limited", headers={"Retry-After": "0"}), httpx.Response(200, json=valid_email_response()), @@ -225,7 +215,7 @@ async def test_retry_on_429_then_success(self) -> None: @respx.mock async def test_retry_exhausted_raises(self) -> None: - route = respx.post(f"{BASE_URL}/api/v1/verify") + route = respx.post(f"{BASE_URL}/api/v1/verify_inline") route.side_effect = [ httpx.Response(502, text="Bad Gateway"), httpx.Response(502, text="Bad Gateway"), @@ -240,7 +230,7 @@ async def test_retry_exhausted_raises(self) -> None: @respx.mock async def test_no_retry_on_auth_error(self) -> None: - route = respx.post(f"{BASE_URL}/api/v1/verify") + route = respx.post(f"{BASE_URL}/api/v1/verify_inline") route.mock(return_value=httpx.Response(401, text="Unauthorized")) async with AsyncTruelist(API_KEY, max_retries=2) as client: with pytest.raises(AuthenticationError): @@ -250,7 +240,7 @@ async def test_no_retry_on_auth_error(self) -> None: @respx.mock async def test_retry_on_connection_error_then_success(self) -> None: - route = respx.post(f"{BASE_URL}/api/v1/verify") + route = respx.post(f"{BASE_URL}/api/v1/verify_inline") route.side_effect = [ httpx.ConnectError("Connection refused"), httpx.Response(200, json=valid_email_response()), @@ -267,7 +257,7 @@ class TestAsyncClientConfiguration: @respx.mock async def test_user_agent_header(self) -> None: - route = respx.post(f"{BASE_URL}/api/v1/verify").mock( + route = respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=valid_email_response()) ) async with AsyncTruelist(API_KEY) as client: @@ -278,7 +268,7 @@ async def test_user_agent_header(self) -> None: @respx.mock async def test_authorization_header(self) -> None: - route = respx.post(f"{BASE_URL}/api/v1/verify").mock( + route = respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=valid_email_response()) ) async with AsyncTruelist(API_KEY) as client: diff --git a/tests/test_client.py b/tests/test_client.py index de8e371..4fc403c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -32,7 +32,7 @@ class TestEmailValidate: @respx.mock def test_valid_email(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=valid_email_response()) ) with Truelist(API_KEY) as client: @@ -40,11 +40,11 @@ def test_valid_email(self) -> None: assert isinstance(result, ValidationResult) assert result.email == "user@example.com" - assert result.state == "valid" - assert result.sub_state == "ok" - assert result.free_email is True - assert result.role is False - assert result.disposable is False + assert result.domain == "example.com" + assert result.canonical == "user" + assert result.state == "ok" + assert result.sub_state == "email_ok" + assert result.verified_at == "2026-02-21T10:00:00.000Z" assert result.suggestion is None assert result.is_valid is True assert result.is_invalid is False @@ -53,55 +53,56 @@ def test_valid_email(self) -> None: @respx.mock def test_invalid_email(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=invalid_email_response()) ) with Truelist(API_KEY) as client: result = client.email.validate("bad@invalid.com") - assert result.state == "invalid" - assert result.sub_state == "failed_no_mailbox" + assert result.state == "email_invalid" + assert result.sub_state == "unknown_error" assert result.is_valid is False assert result.is_invalid is True @respx.mock def test_risky_email(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=risky_email_response()) ) with Truelist(API_KEY) as client: result = client.email.validate("info@company.com") assert result.state == "risky" - assert result.sub_state == "accept_all" - assert result.role is True + assert result.sub_state == "is_role" + assert result.is_role is True assert result.is_risky is True @respx.mock def test_unknown_email(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=unknown_email_response()) ) with Truelist(API_KEY) as client: result = client.email.validate("mystery@timeout.com") assert result.state == "unknown" - assert result.sub_state == "failed_greylisted" + assert result.sub_state == "failed_smtp_check" assert result.is_unknown is True @respx.mock def test_disposable_email(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=disposable_email_response()) ) with Truelist(API_KEY) as client: result = client.email.validate("temp@mailinator.com") - assert result.disposable is True + assert result.is_disposable is True + assert result.sub_state == "is_disposable" @respx.mock def test_email_with_suggestion(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=suggestion_email_response()) ) with Truelist(API_KEY) as client: @@ -111,29 +112,14 @@ def test_email_with_suggestion(self) -> None: @respx.mock def test_validation_result_is_frozen(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=valid_email_response()) ) with Truelist(API_KEY) as client: result = client.email.validate("user@example.com") with pytest.raises(AttributeError): - result.state = "invalid" # type: ignore[misc] - - -class TestEmailFormValidate: - """Tests for client.email.form_validate().""" - - @respx.mock - def test_form_validate(self) -> None: - respx.post(f"{BASE_URL}/api/v1/form_verify").mock( - return_value=httpx.Response(200, json=valid_email_response()) - ) - with Truelist(API_KEY) as client: - result = client.email.form_validate("user@example.com") - - assert result.is_valid is True - assert result.email == "user@example.com" + result.state = "email_invalid" # type: ignore[misc] class TestAccountGet: @@ -141,15 +127,18 @@ class TestAccountGet: @respx.mock def test_get_account(self) -> None: - respx.get(f"{BASE_URL}/api/v1/account").mock( + respx.get(f"{BASE_URL}/me").mock( return_value=httpx.Response(200, json=account_response()) ) with Truelist(API_KEY) as client: account = client.account.get() - assert account.email == "owner@truelist.io" - assert account.plan == "pro" - assert account.credits == 9500 + assert account.email == "team@company.com" + assert account.name == "Team Lead" + assert account.uuid == "a3828d19-1234-5678-9abc-def012345678" + assert account.time_zone == "America/New_York" + assert account.is_admin_role is True + assert account.payment_plan == "pro" class TestErrorHandling: @@ -157,7 +146,7 @@ class TestErrorHandling: @respx.mock def test_authentication_error_401(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(401, text="Unauthorized") ) with ( @@ -171,7 +160,7 @@ def test_authentication_error_401(self) -> None: @respx.mock def test_authentication_error_403(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(403, text="Forbidden") ) with ( @@ -184,7 +173,7 @@ def test_authentication_error_403(self) -> None: @respx.mock def test_rate_limit_error(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response( 429, text="Too Many Requests", @@ -199,7 +188,7 @@ def test_rate_limit_error(self) -> None: @respx.mock def test_server_error_500(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(500, text="Internal Server Error") ) with Truelist(API_KEY, max_retries=0) as client, pytest.raises(ApiError) as exc_info: @@ -209,7 +198,7 @@ def test_server_error_500(self) -> None: @respx.mock def test_api_error_422(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(422, text="Unprocessable Entity") ) with Truelist(API_KEY, max_retries=0) as client, pytest.raises(ApiError) as exc_info: @@ -219,7 +208,7 @@ def test_api_error_422(self) -> None: @respx.mock def test_timeout_error(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( side_effect=httpx.ReadTimeout("Read timed out") ) with Truelist(API_KEY, max_retries=0) as client, pytest.raises(TimeoutError): @@ -227,7 +216,7 @@ def test_timeout_error(self) -> None: @respx.mock def test_connection_error(self) -> None: - respx.post(f"{BASE_URL}/api/v1/verify").mock( + respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( side_effect=httpx.ConnectError("Connection refused") ) with Truelist(API_KEY, max_retries=0) as client, pytest.raises(ConnectionError): @@ -239,7 +228,7 @@ class TestRetries: @respx.mock def test_retry_on_500_then_success(self) -> None: - route = respx.post(f"{BASE_URL}/api/v1/verify") + route = respx.post(f"{BASE_URL}/api/v1/verify_inline") route.side_effect = [ httpx.Response(500, text="Internal Server Error"), httpx.Response(200, json=valid_email_response()), @@ -252,7 +241,7 @@ def test_retry_on_500_then_success(self) -> None: @respx.mock def test_retry_on_429_then_success(self) -> None: - route = respx.post(f"{BASE_URL}/api/v1/verify") + route = respx.post(f"{BASE_URL}/api/v1/verify_inline") route.side_effect = [ httpx.Response(429, text="Rate Limited", headers={"Retry-After": "0"}), httpx.Response(200, json=valid_email_response()), @@ -265,7 +254,7 @@ def test_retry_on_429_then_success(self) -> None: @respx.mock def test_retry_exhausted_raises(self) -> None: - route = respx.post(f"{BASE_URL}/api/v1/verify") + route = respx.post(f"{BASE_URL}/api/v1/verify_inline") route.side_effect = [ httpx.Response(500, text="Error"), httpx.Response(500, text="Error"), @@ -279,7 +268,7 @@ def test_retry_exhausted_raises(self) -> None: @respx.mock def test_retry_on_connection_error_then_success(self) -> None: - route = respx.post(f"{BASE_URL}/api/v1/verify") + route = respx.post(f"{BASE_URL}/api/v1/verify_inline") route.side_effect = [ httpx.ConnectError("Connection refused"), httpx.Response(200, json=valid_email_response()), @@ -292,7 +281,7 @@ def test_retry_on_connection_error_then_success(self) -> None: @respx.mock def test_retry_on_timeout_then_success(self) -> None: - route = respx.post(f"{BASE_URL}/api/v1/verify") + route = respx.post(f"{BASE_URL}/api/v1/verify_inline") route.side_effect = [ httpx.ReadTimeout("Timeout"), httpx.Response(200, json=valid_email_response()), @@ -305,7 +294,7 @@ def test_retry_on_timeout_then_success(self) -> None: @respx.mock def test_no_retry_on_401(self) -> None: - route = respx.post(f"{BASE_URL}/api/v1/verify") + route = respx.post(f"{BASE_URL}/api/v1/verify_inline") route.mock(return_value=httpx.Response(401, text="Unauthorized")) with Truelist(API_KEY, max_retries=2) as client, pytest.raises(AuthenticationError): client.email.validate("user@example.com") @@ -314,7 +303,7 @@ def test_no_retry_on_401(self) -> None: @respx.mock def test_no_retry_on_422(self) -> None: - route = respx.post(f"{BASE_URL}/api/v1/verify") + route = respx.post(f"{BASE_URL}/api/v1/verify_inline") route.mock(return_value=httpx.Response(422, text="Bad Request")) with Truelist(API_KEY, max_retries=2) as client, pytest.raises(ApiError): client.email.validate("user@example.com") @@ -328,7 +317,7 @@ class TestClientConfiguration: @respx.mock def test_custom_base_url(self) -> None: custom_url = "https://custom.api.truelist.io" - respx.post(f"{custom_url}/api/v1/verify").mock( + respx.post(f"{custom_url}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=valid_email_response()) ) with Truelist(API_KEY, base_url=custom_url) as client: @@ -338,7 +327,7 @@ def test_custom_base_url(self) -> None: @respx.mock def test_user_agent_header(self) -> None: - route = respx.post(f"{BASE_URL}/api/v1/verify").mock( + route = respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=valid_email_response()) ) with Truelist(API_KEY) as client: @@ -349,7 +338,7 @@ def test_user_agent_header(self) -> None: @respx.mock def test_authorization_header(self) -> None: - route = respx.post(f"{BASE_URL}/api/v1/verify").mock( + route = respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=valid_email_response()) ) with Truelist(API_KEY) as client: @@ -359,18 +348,15 @@ def test_authorization_header(self) -> None: assert request.headers["Authorization"] == f"Bearer {API_KEY}" @respx.mock - def test_request_body(self) -> None: - route = respx.post(f"{BASE_URL}/api/v1/verify").mock( + def test_email_sent_as_query_param(self) -> None: + route = respx.post(f"{BASE_URL}/api/v1/verify_inline").mock( return_value=httpx.Response(200, json=valid_email_response()) ) with Truelist(API_KEY) as client: client.email.validate("user@example.com") request = route.calls[0].request - import json - - body = json.loads(request.content) - assert body == {"email": "user@example.com"} + assert "email=user%40example.com" in str(request.url) def test_context_manager(self) -> None: with Truelist(API_KEY) as client: