checkmax-phone-utils 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checkmax_phone_utils/__init__.py +56 -0
- checkmax_phone_utils/client.py +146 -0
- checkmax_phone_utils/exceptions.py +65 -0
- checkmax_phone_utils/normalizer.py +95 -0
- checkmax_phone_utils/validator.py +124 -0
- checkmax_phone_utils-0.2.0.dist-info/METADATA +174 -0
- checkmax_phone_utils-0.2.0.dist-info/RECORD +9 -0
- checkmax_phone_utils-0.2.0.dist-info/WHEEL +4 -0
- checkmax_phone_utils-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""checkmax-phone-utils — Phone validation utilities + CheckMaxApp API client.
|
|
2
|
+
|
|
3
|
+
Companion library to CheckMaxApp (https://checkmaxapp.com), a phone
|
|
4
|
+
validation service for the MAX messenger. Format-validate numbers locally
|
|
5
|
+
(E.164, libphonenumber), and verify messenger registration via the official
|
|
6
|
+
REST API.
|
|
7
|
+
|
|
8
|
+
Public API:
|
|
9
|
+
validate_e164 — format-validate a raw phone string and return E.164.
|
|
10
|
+
is_mobile — check whether an E.164 number is mobile.
|
|
11
|
+
detect_region — guess the ISO region of a raw phone string.
|
|
12
|
+
normalize — convert any input to canonical E.164 form or None.
|
|
13
|
+
clean — strip non-digit characters (preserving leading +).
|
|
14
|
+
CheckMaxClient — client for the CheckMaxApp REST API (registration check).
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
>>> from checkmax_phone_utils import normalize, validate_e164
|
|
18
|
+
>>> normalize("+7 (916) 123-45-67")
|
|
19
|
+
'+79161234567'
|
|
20
|
+
>>> validate_e164("89161234567", region="RU")
|
|
21
|
+
(True, '+79161234567')
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from checkmax_phone_utils.client import CheckMaxClient
|
|
25
|
+
from checkmax_phone_utils.exceptions import (
|
|
26
|
+
CheckMaxError,
|
|
27
|
+
InvalidPhoneNumberError,
|
|
28
|
+
APIError,
|
|
29
|
+
AuthError,
|
|
30
|
+
InsufficientBalanceError,
|
|
31
|
+
APINotAvailableError,
|
|
32
|
+
)
|
|
33
|
+
from checkmax_phone_utils.normalizer import clean, normalize
|
|
34
|
+
from checkmax_phone_utils.validator import (
|
|
35
|
+
detect_region,
|
|
36
|
+
is_mobile,
|
|
37
|
+
validate_e164,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__version__ = "0.2.0"
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"__version__",
|
|
44
|
+
"validate_e164",
|
|
45
|
+
"is_mobile",
|
|
46
|
+
"detect_region",
|
|
47
|
+
"normalize",
|
|
48
|
+
"clean",
|
|
49
|
+
"CheckMaxClient",
|
|
50
|
+
"CheckMaxError",
|
|
51
|
+
"InvalidPhoneNumberError",
|
|
52
|
+
"APIError",
|
|
53
|
+
"AuthError",
|
|
54
|
+
"InsufficientBalanceError",
|
|
55
|
+
"APINotAvailableError",
|
|
56
|
+
]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Client for the CheckMaxApp REST API.
|
|
2
|
+
|
|
3
|
+
A small, dependency-free (stdlib ``urllib``) client for the CheckMaxApp
|
|
4
|
+
phone-validation API — verify whether a phone number is registered on the
|
|
5
|
+
MAX messenger and retrieve the public profile name, for anti-fraud,
|
|
6
|
+
list hygiene and CRM enrichment.
|
|
7
|
+
|
|
8
|
+
Get an API key from the CheckMaxApp Telegram bot, then:
|
|
9
|
+
|
|
10
|
+
>>> from checkmax_phone_utils import CheckMaxClient
|
|
11
|
+
>>> client = CheckMaxClient(api_key="mxk_...")
|
|
12
|
+
>>> client.check(["79001234567"]) # doctest: +SKIP
|
|
13
|
+
[{'phone': '79001234567', 'status': 'registered',
|
|
14
|
+
'first_name': 'Ivan', 'last_name': 'Petrov'}]
|
|
15
|
+
|
|
16
|
+
Full docs: https://checkmaxapp.com/api
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import urllib.error
|
|
23
|
+
import urllib.request
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from checkmax_phone_utils.exceptions import (
|
|
27
|
+
APIError,
|
|
28
|
+
AuthError,
|
|
29
|
+
InsufficientBalanceError,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = ["CheckMaxClient"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CheckMaxClient:
|
|
36
|
+
"""Client for the CheckMaxApp phone-validation REST API.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
api_key: Personal API key (``mxk_`` + 64 hex chars). Obtain and
|
|
40
|
+
rotate it via the CheckMaxApp Telegram bot. Sent only in the
|
|
41
|
+
``X-API-Key`` header — never in the query string.
|
|
42
|
+
base_url: API root. Defaults to the public endpoint.
|
|
43
|
+
timeout: Per-request HTTP timeout in seconds.
|
|
44
|
+
|
|
45
|
+
See https://checkmaxapp.com/api for the full reference.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
DEFAULT_BASE_URL = "https://api.maxcheck.online/v1"
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
api_key: str,
|
|
53
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
54
|
+
timeout: float = 30.0,
|
|
55
|
+
) -> None:
|
|
56
|
+
self.api_key = api_key
|
|
57
|
+
self.base_url = base_url.rstrip("/")
|
|
58
|
+
self.timeout = timeout
|
|
59
|
+
|
|
60
|
+
# ---- HTTP plumbing -------------------------------------------------
|
|
61
|
+
|
|
62
|
+
def _request(
|
|
63
|
+
self,
|
|
64
|
+
method: str,
|
|
65
|
+
path: str,
|
|
66
|
+
body: dict[str, Any] | None = None,
|
|
67
|
+
auth: bool = True,
|
|
68
|
+
) -> Any:
|
|
69
|
+
url = f"{self.base_url}{path}"
|
|
70
|
+
data = json.dumps(body).encode("utf-8") if body is not None else None
|
|
71
|
+
headers = {"Accept": "application/json"}
|
|
72
|
+
if data is not None:
|
|
73
|
+
headers["Content-Type"] = "application/json"
|
|
74
|
+
if auth:
|
|
75
|
+
headers["X-API-Key"] = self.api_key
|
|
76
|
+
|
|
77
|
+
req = urllib.request.Request(url, data=data, method=method, headers=headers)
|
|
78
|
+
try:
|
|
79
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
80
|
+
raw = resp.read().decode("utf-8")
|
|
81
|
+
return json.loads(raw) if raw else None
|
|
82
|
+
except urllib.error.HTTPError as exc:
|
|
83
|
+
detail = exc.read().decode("utf-8", "ignore")
|
|
84
|
+
if exc.code == 401:
|
|
85
|
+
raise AuthError(detail or "invalid or missing API key") from exc
|
|
86
|
+
if exc.code == 402:
|
|
87
|
+
raise InsufficientBalanceError(detail or "insufficient balance") from exc
|
|
88
|
+
raise APIError(f"HTTP {exc.code}: {detail[:300]}", status_code=exc.code) from exc
|
|
89
|
+
except urllib.error.URLError as exc:
|
|
90
|
+
raise APIError(f"network error: {exc.reason}") from exc
|
|
91
|
+
|
|
92
|
+
# ---- Endpoints -----------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def health(self) -> dict[str, Any]:
|
|
95
|
+
"""``GET /health`` — service liveness for monitoring (no auth)."""
|
|
96
|
+
return self._request("GET", "/health", auth=False)
|
|
97
|
+
|
|
98
|
+
def balance(self) -> dict[str, Any]:
|
|
99
|
+
"""``GET /balance`` — current balance, tier discount and effective price."""
|
|
100
|
+
return self._request("GET", "/balance")
|
|
101
|
+
|
|
102
|
+
def usage(self) -> dict[str, Any]:
|
|
103
|
+
"""``GET /usage`` — counters of successfully billed checks per window."""
|
|
104
|
+
return self._request("GET", "/usage")
|
|
105
|
+
|
|
106
|
+
def check(self, phones: list[str]) -> list[dict[str, Any]]:
|
|
107
|
+
"""``POST /check`` — synchronously verify a list of phone numbers.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
phones: Phone numbers (digits, e.g. ``"79001234567"``). Best for
|
|
111
|
+
up to ~1000 numbers; for more, use :meth:`batch_create`.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
One result per input number, each with ``phone``, ``status``
|
|
115
|
+
(``registered`` / ``not_found`` / ``error``) and, when
|
|
116
|
+
registered, ``first_name`` / ``last_name``.
|
|
117
|
+
"""
|
|
118
|
+
return self._request("POST", "/check", {"phones": phones})
|
|
119
|
+
|
|
120
|
+
def batch_create(self, phones: list[str]) -> dict[str, Any]:
|
|
121
|
+
"""``POST /batch`` — submit an async batch job for large lists.
|
|
122
|
+
|
|
123
|
+
Returns a job descriptor including its ``id``; poll
|
|
124
|
+
:meth:`batch_status` and fetch results with :meth:`batch_download`.
|
|
125
|
+
"""
|
|
126
|
+
return self._request("POST", "/batch", {"phones": phones})
|
|
127
|
+
|
|
128
|
+
def batch_status(self, job_id: str) -> dict[str, Any]:
|
|
129
|
+
"""``GET /batch/{id}`` — status/progress of an async batch job."""
|
|
130
|
+
return self._request("GET", f"/batch/{job_id}")
|
|
131
|
+
|
|
132
|
+
def batch_download(self, job_id: str) -> bytes:
|
|
133
|
+
"""``GET /batch/{id}/download`` — fetch finished batch results (CSV bytes)."""
|
|
134
|
+
url = f"{self.base_url}/batch/{job_id}/download"
|
|
135
|
+
req = urllib.request.Request(
|
|
136
|
+
url, method="GET", headers={"X-API-Key": self.api_key}
|
|
137
|
+
)
|
|
138
|
+
try:
|
|
139
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
140
|
+
return resp.read()
|
|
141
|
+
except urllib.error.HTTPError as exc:
|
|
142
|
+
if exc.code == 401:
|
|
143
|
+
raise AuthError("invalid or missing API key") from exc
|
|
144
|
+
raise APIError(f"HTTP {exc.code}", status_code=exc.code) from exc
|
|
145
|
+
except urllib.error.URLError as exc:
|
|
146
|
+
raise APIError(f"network error: {exc.reason}") from exc
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Custom exceptions for checkmax-phone-utils."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CheckMaxError(Exception):
|
|
7
|
+
"""Base exception for all checkmax-phone-utils errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InvalidPhoneNumberError(CheckMaxError):
|
|
11
|
+
"""Raised when a phone number cannot be parsed or is invalid.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
raw: The original input string that failed to parse.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, raw: str, message: str | None = None) -> None:
|
|
18
|
+
self.raw = raw
|
|
19
|
+
super().__init__(message or f"Cannot parse phone number: {raw!r}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class APIError(CheckMaxError):
|
|
23
|
+
"""Raised on a non-success response from the CheckMaxApp REST API.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
status_code: HTTP status code, when available (else ``None``).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, message: str, status_code: int | None = None) -> None:
|
|
30
|
+
self.status_code = status_code
|
|
31
|
+
super().__init__(message)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AuthError(APIError):
|
|
35
|
+
"""HTTP 401 — API key missing, malformed, or revoked.
|
|
36
|
+
|
|
37
|
+
Get or rotate your key via the CheckMaxApp Telegram bot. See
|
|
38
|
+
https://checkmaxapp.com/api.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, message: str | None = None) -> None:
|
|
42
|
+
super().__init__(message or "invalid or missing API key", status_code=401)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class InsufficientBalanceError(APIError):
|
|
46
|
+
"""HTTP 402 — key is valid but the balance cannot cover the request.
|
|
47
|
+
|
|
48
|
+
Nothing is charged on a 402. Top up and retry. See
|
|
49
|
+
https://checkmaxapp.com/pricing.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, message: str | None = None) -> None:
|
|
53
|
+
super().__init__(message or "insufficient balance", status_code=402)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class APINotAvailableError(APIError):
|
|
57
|
+
"""Deprecated. Kept for backwards compatibility with 0.1.x.
|
|
58
|
+
|
|
59
|
+
The public API is live; this is no longer raised by the client.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, message: str | None = None) -> None:
|
|
63
|
+
super().__init__(
|
|
64
|
+
message or "CheckMaxApp API. See https://checkmaxapp.com/api"
|
|
65
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Normalize raw phone strings to canonical E.164 form."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import phonenumbers
|
|
9
|
+
from phonenumbers import NumberParseException
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_NON_DIGIT_OR_PLUS_RE = re.compile(r"[^\d+]")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def clean(raw: str) -> str:
|
|
16
|
+
"""Strip every character that is not a digit or a leading ``+``.
|
|
17
|
+
|
|
18
|
+
A ``+`` is preserved only when it appears at the very start; any
|
|
19
|
+
other ``+`` characters are removed. The result is a pure digit
|
|
20
|
+
string (optionally with a single leading ``+``).
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
raw: Any string that might contain a phone number.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Cleaned string, possibly empty.
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
>>> clean("+7 (916) 123-45-67")
|
|
30
|
+
'+79161234567'
|
|
31
|
+
>>> clean("8 916 123-45-67")
|
|
32
|
+
'89161234567'
|
|
33
|
+
>>> clean("phone: +1-415-555-2671 ext 99")
|
|
34
|
+
'+14155552671'
|
|
35
|
+
>>> clean("")
|
|
36
|
+
''
|
|
37
|
+
"""
|
|
38
|
+
if not raw or not isinstance(raw, str):
|
|
39
|
+
return ""
|
|
40
|
+
|
|
41
|
+
stripped = raw.strip()
|
|
42
|
+
has_plus = stripped.startswith("+")
|
|
43
|
+
digits = _NON_DIGIT_OR_PLUS_RE.sub("", stripped)
|
|
44
|
+
digits = digits.replace("+", "")
|
|
45
|
+
return ("+" + digits) if has_plus else digits
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def normalize(raw: str, default_region: str = "RU") -> Optional[str]:
|
|
49
|
+
"""Convert any phone-shaped string to canonical E.164 form.
|
|
50
|
+
|
|
51
|
+
This is the safe high-level entry point: it cleans the input,
|
|
52
|
+
attempts a parse against ``default_region`` if no country code is
|
|
53
|
+
present, validates the result, and returns the canonical
|
|
54
|
+
``+CCNNNNNNNNNNN`` form.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
raw: User input, e.g. ``"+7 (916) 123-45-67"`` or
|
|
58
|
+
``"8 916 123-45-67"``.
|
|
59
|
+
default_region: ISO 3166-1 alpha-2 region used when ``raw`` has
|
|
60
|
+
no international prefix. Defaults to ``"RU"``.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Canonical E.164 string, or ``None`` if the input cannot be
|
|
64
|
+
parsed or is not a valid phone number.
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
>>> normalize("+7 (916) 123-45-67")
|
|
68
|
+
'+79161234567'
|
|
69
|
+
>>> normalize("8 916 123-45-67", default_region="RU")
|
|
70
|
+
'+79161234567'
|
|
71
|
+
>>> normalize("+1 415 555 2671")
|
|
72
|
+
'+14155552671'
|
|
73
|
+
>>> normalize("garbage") is None
|
|
74
|
+
True
|
|
75
|
+
>>> normalize("") is None
|
|
76
|
+
True
|
|
77
|
+
"""
|
|
78
|
+
if not raw or not isinstance(raw, str):
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
cleaned = clean(raw)
|
|
82
|
+
if not cleaned:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
parsed = phonenumbers.parse(cleaned, default_region)
|
|
87
|
+
except NumberParseException:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
if not phonenumbers.is_valid_number(parsed):
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
return phonenumbers.format_number(
|
|
94
|
+
parsed, phonenumbers.PhoneNumberFormat.E164
|
|
95
|
+
)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Format validation for phone numbers.
|
|
2
|
+
|
|
3
|
+
Wraps Google's libphonenumber (Python port `phonenumbers`) and exposes
|
|
4
|
+
ergonomic helpers that return plain types instead of library objects.
|
|
5
|
+
|
|
6
|
+
These functions perform *format* validation only — they verify the input
|
|
7
|
+
has a structurally valid E.164 shape for the given region. They do NOT
|
|
8
|
+
check whether the number is in service or registered on MAX messenger;
|
|
9
|
+
for that, use :class:`checkmax_phone_utils.CheckMaxClient`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
import phonenumbers
|
|
17
|
+
from phonenumbers import NumberParseException, PhoneNumberType
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def validate_e164(raw: str, region: str = "RU") -> tuple[bool, Optional[str]]:
|
|
21
|
+
"""Validate a raw phone string and return the canonical E.164 form.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
raw: User-supplied phone number, e.g. ``"+7 (916) 123-45-67"``.
|
|
25
|
+
region: ISO 3166-1 alpha-2 region used when the input has no
|
|
26
|
+
international prefix. Defaults to ``"RU"``.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
A tuple ``(is_valid, e164)``. When valid, ``e164`` is a string
|
|
30
|
+
like ``"+79161234567"``. When invalid, ``e164`` is ``None``.
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
>>> validate_e164("+7 (916) 123-45-67")
|
|
34
|
+
(True, '+79161234567')
|
|
35
|
+
>>> validate_e164("89161234567", region="RU")
|
|
36
|
+
(True, '+79161234567')
|
|
37
|
+
>>> validate_e164("not a phone")
|
|
38
|
+
(False, None)
|
|
39
|
+
>>> validate_e164("")
|
|
40
|
+
(False, None)
|
|
41
|
+
"""
|
|
42
|
+
if not raw or not isinstance(raw, str):
|
|
43
|
+
return False, None
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
parsed = phonenumbers.parse(raw, region)
|
|
47
|
+
except NumberParseException:
|
|
48
|
+
return False, None
|
|
49
|
+
|
|
50
|
+
if not phonenumbers.is_valid_number(parsed):
|
|
51
|
+
return False, None
|
|
52
|
+
|
|
53
|
+
return True, phonenumbers.format_number(
|
|
54
|
+
parsed, phonenumbers.PhoneNumberFormat.E164
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_mobile(e164: str) -> bool:
|
|
59
|
+
"""Return True if the given E.164 number is a mobile line.
|
|
60
|
+
|
|
61
|
+
Some carriers in some countries use overlapping ranges for mobile
|
|
62
|
+
and fixed lines; in those cases this returns ``True`` for
|
|
63
|
+
``FIXED_LINE_OR_MOBILE`` as well.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
e164: Phone number in E.164 form, e.g. ``"+79161234567"``.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
True if the number is mobile (or mobile-or-fixed), False
|
|
70
|
+
otherwise — including the case where the input cannot be parsed.
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
>>> is_mobile("+79161234567")
|
|
74
|
+
True
|
|
75
|
+
>>> is_mobile("garbage")
|
|
76
|
+
False
|
|
77
|
+
"""
|
|
78
|
+
if not e164 or not isinstance(e164, str):
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
parsed = phonenumbers.parse(e164, None)
|
|
83
|
+
except NumberParseException:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
number_type = phonenumbers.number_type(parsed)
|
|
87
|
+
return number_type in (
|
|
88
|
+
PhoneNumberType.MOBILE,
|
|
89
|
+
PhoneNumberType.FIXED_LINE_OR_MOBILE,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def detect_region(raw: str) -> Optional[str]:
|
|
94
|
+
"""Detect the ISO 3166-1 alpha-2 region of a raw phone number.
|
|
95
|
+
|
|
96
|
+
The input must contain a country prefix (``+`` or ``00``) for this
|
|
97
|
+
to succeed; otherwise we cannot disambiguate the region.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
raw: A phone string with international prefix, e.g.
|
|
101
|
+
``"+79161234567"``.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Two-letter ISO region (e.g. ``"RU"``) or ``None`` if the number
|
|
105
|
+
cannot be parsed or its region cannot be determined.
|
|
106
|
+
|
|
107
|
+
Examples:
|
|
108
|
+
>>> detect_region("+79161234567")
|
|
109
|
+
'RU'
|
|
110
|
+
>>> detect_region("+14155552671")
|
|
111
|
+
'US'
|
|
112
|
+
>>> detect_region("89161234567") # no country prefix
|
|
113
|
+
>>> detect_region("")
|
|
114
|
+
"""
|
|
115
|
+
if not raw or not isinstance(raw, str):
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
parsed = phonenumbers.parse(raw, None)
|
|
120
|
+
except NumberParseException:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
region = phonenumbers.region_code_for_number(parsed)
|
|
124
|
+
return region if region and region != "ZZ" else None
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: checkmax-phone-utils
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Phone validation utilities (E.164 + libphonenumber) and the official REST client for the CheckMaxApp phone-verification API.
|
|
5
|
+
Project-URL: Homepage, https://checkmaxapp.com
|
|
6
|
+
Project-URL: Documentation, https://checkmaxapp.com/api
|
|
7
|
+
Project-URL: Repository, https://github.com/abragimbaliev/checkmax-phone-utils
|
|
8
|
+
Project-URL: Issues, https://github.com/abragimbaliev/checkmax-phone-utils/issues
|
|
9
|
+
Author-email: CheckMax Team <dev@checkmaxapp.com>
|
|
10
|
+
Maintainer-email: CheckMax Team <dev@checkmaxapp.com>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: checkmaxapp,e164,libphonenumber,max-messenger,phone,validation
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Communications :: Telephony
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Requires-Dist: phonenumbers>=8.13
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# checkmax-phone-utils
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/checkmax-phone-utils/)
|
|
36
|
+
[](https://www.python.org/downloads/)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
[](https://checkmaxapp.com)
|
|
39
|
+
|
|
40
|
+
A small, focused Python toolkit for phone-number **format validation**,
|
|
41
|
+
**E.164 normalization**, and an **optional API client** for the
|
|
42
|
+
[CheckMaxApp](https://checkmaxapp.com) service. Built on top of Google's
|
|
43
|
+
[libphonenumber](https://github.com/google/libphonenumber) (Python port).
|
|
44
|
+
Use it as a drop-in helper in your own code, or as the official client
|
|
45
|
+
library for the live CheckMaxApp REST API.
|
|
46
|
+
|
|
47
|
+
> **Powered by [CheckMaxApp](https://checkmaxapp.com) — phone validation
|
|
48
|
+
> service for the MAX messenger.** This library handles *format* and
|
|
49
|
+
> *region* validation. To check whether a number is actually registered
|
|
50
|
+
> on MAX, use the hosted CheckMaxApp service.
|
|
51
|
+
|
|
52
|
+
## Quickstart
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install checkmax-phone-utils
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from checkmax_phone_utils import normalize, validate_e164, is_mobile
|
|
60
|
+
|
|
61
|
+
normalize("+7 (916) 123-45-67") # '+79161234567'
|
|
62
|
+
normalize("8 916 123-45-67", "RU") # '+79161234567'
|
|
63
|
+
validate_e164("+1 415 555 2671") # (True, '+14155552671')
|
|
64
|
+
is_mobile("+79161234567") # True
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## What it does
|
|
68
|
+
|
|
69
|
+
| Function | Purpose |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `normalize(raw, default_region)` | Convert any phone-shaped input to canonical E.164, or `None`. |
|
|
72
|
+
| `validate_e164(raw, region)` | Return `(is_valid, e164_or_none)` for a raw input. |
|
|
73
|
+
| `is_mobile(e164)` | True if the number is mobile (or mobile-or-fixed). |
|
|
74
|
+
| `detect_region(raw)` | Return ISO 3166-1 alpha-2 region of an international number. |
|
|
75
|
+
| `clean(raw)` | Strip everything that is not a digit or leading `+`. |
|
|
76
|
+
| `CheckMaxClient(api_key).check([...])` | Verify numbers via the REST API — registration status + public name. |
|
|
77
|
+
|
|
78
|
+
Full reference: see the docstrings — every public function has examples
|
|
79
|
+
and edge-case notes.
|
|
80
|
+
|
|
81
|
+
## API reference
|
|
82
|
+
|
|
83
|
+
### `normalize(raw: str, default_region: str = "RU") -> str | None`
|
|
84
|
+
|
|
85
|
+
Safe high-level entry point. Cleans, parses, validates, and returns the
|
|
86
|
+
canonical `+CCNNNNNNNNNNN` form, or `None` if the input is not a valid
|
|
87
|
+
phone number.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
>>> normalize("+7 (916) 123-45-67")
|
|
91
|
+
'+79161234567'
|
|
92
|
+
>>> normalize("garbage") is None
|
|
93
|
+
True
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### `validate_e164(raw: str, region: str = "RU") -> tuple[bool, str | None]`
|
|
97
|
+
|
|
98
|
+
Returns `(is_valid, e164)`. When valid, `e164` is the canonical form;
|
|
99
|
+
otherwise it is `None`. Useful when you want to keep both signals.
|
|
100
|
+
|
|
101
|
+
### `is_mobile(e164: str) -> bool`
|
|
102
|
+
|
|
103
|
+
True for `MOBILE` and `FIXED_LINE_OR_MOBILE` numbers; False otherwise,
|
|
104
|
+
including when the input cannot be parsed.
|
|
105
|
+
|
|
106
|
+
### `detect_region(raw: str) -> str | None`
|
|
107
|
+
|
|
108
|
+
Detect the ISO region from an international-format number (with `+` or
|
|
109
|
+
`00` prefix). Returns `None` when the input lacks a country prefix.
|
|
110
|
+
|
|
111
|
+
### `CheckMaxClient(api_key: str)`
|
|
112
|
+
|
|
113
|
+
Official client for the [CheckMaxApp REST API](https://checkmaxapp.com/api).
|
|
114
|
+
Get an API key from the CheckMaxApp Telegram bot. Methods: `health()`,
|
|
115
|
+
`balance()`, `usage()`, `check(phones)`, `batch_create(phones)`,
|
|
116
|
+
`batch_status(id)`, `batch_download(id)`. Raises `AuthError` (401) and
|
|
117
|
+
`InsufficientBalanceError` (402).
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from checkmax_phone_utils import CheckMaxClient
|
|
121
|
+
|
|
122
|
+
client = CheckMaxClient(api_key="mxk_...")
|
|
123
|
+
client.check(["79001234567"])
|
|
124
|
+
# [{'phone': '79001234567', 'status': 'registered',
|
|
125
|
+
# 'first_name': 'Ivan', 'last_name': 'Petrov'}]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The full machine-readable schema lives at
|
|
129
|
+
[`openapi/checkmax-api.openapi.yaml`](openapi/checkmax-api.openapi.yaml)
|
|
130
|
+
(OpenAPI 3.0) — also published to the API directories.
|
|
131
|
+
|
|
132
|
+
## Examples
|
|
133
|
+
|
|
134
|
+
The [`examples/`](examples) directory contains runnable scripts:
|
|
135
|
+
|
|
136
|
+
- `examples/basic_validation.py` — single-number validation against a
|
|
137
|
+
small sample list.
|
|
138
|
+
- `examples/bulk_normalize.py` — normalize a CSV file of phones in
|
|
139
|
+
bulk, emitting a CSV with `e164` and `valid` columns.
|
|
140
|
+
- `examples/api_client_demo.py` — intended surface of the
|
|
141
|
+
CheckMaxApp client.
|
|
142
|
+
|
|
143
|
+
## Roadmap
|
|
144
|
+
|
|
145
|
+
- v0.1 — format validation, E.164 normalization, stub client.
|
|
146
|
+
- v0.2 — live REST client (check / batch / balance / usage), structured
|
|
147
|
+
errors, OpenAPI 3.0 spec (current).
|
|
148
|
+
- v0.3 — async client, retries, optional caching.
|
|
149
|
+
- v0.4 — type stubs on PyPI, CLI entry point (`checkmax phone <num>`).
|
|
150
|
+
|
|
151
|
+
## Development
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
git clone https://github.com/abragimbaliev/checkmax-phone-utils.git
|
|
155
|
+
cd checkmax-phone-utils
|
|
156
|
+
pip install -e ".[dev]"
|
|
157
|
+
pytest
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
CI runs `pytest` against Python 3.10, 3.11, and 3.12 on every push.
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
[MIT](LICENSE). Copyright (c) 2026 CheckMax Team.
|
|
165
|
+
|
|
166
|
+
## Authors
|
|
167
|
+
|
|
168
|
+
CheckMax Team — `dev@checkmaxapp.com`
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
Looking for the hosted service?
|
|
173
|
+
**[CheckMaxApp](https://checkmaxapp.com)** — phone validation for the
|
|
174
|
+
MAX messenger.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
checkmax_phone_utils/__init__.py,sha256=b0LQRu_uzeIW3bsCzHlV3Pa4_KTNENQkBJHvSFSriGk,1686
|
|
2
|
+
checkmax_phone_utils/client.py,sha256=efuBisxcs3LY8rUktlbkxFpnlEHncg0C0sNzPN1Zu4M,5622
|
|
3
|
+
checkmax_phone_utils/exceptions.py,sha256=zkWQzOAPOIZ9zZX-a7d8f_lZskFWKyehKxDhMb9Wr14,1953
|
|
4
|
+
checkmax_phone_utils/normalizer.py,sha256=6BBodjG6gvbtA99rREydykH0Inzh1h3dFUinaJ1Cpx0,2672
|
|
5
|
+
checkmax_phone_utils/validator.py,sha256=37vKkXG9xV0g17fWthKgfoTiL38y-XiRay9Elrus3o0,3779
|
|
6
|
+
checkmax_phone_utils-0.2.0.dist-info/METADATA,sha256=Oo8nouUXb4sdCQRQUeVMAvJf6la60igfRCBMaFKJcTQ,6531
|
|
7
|
+
checkmax_phone_utils-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
8
|
+
checkmax_phone_utils-0.2.0.dist-info/licenses/LICENSE,sha256=-doPs4yonX8rLlFA3O2OMLvdZxScth0tQ9y_JG08R2g,1070
|
|
9
|
+
checkmax_phone_utils-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CheckMax Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|