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.
@@ -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
+ [![PyPI 0.2.0](https://img.shields.io/badge/PyPI-0.2.0-blue.svg)](https://pypi.org/project/checkmax-phone-utils/)
36
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
37
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
38
+ [![CheckMaxApp](https://img.shields.io/badge/Powered_by-CheckMaxApp-orange.svg)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.