checkmax-phone-utils 0.2.0__tar.gz

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,35 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+ branches: [main, master]
8
+
9
+ jobs:
10
+ test:
11
+ name: pytest (Python ${{ matrix.python-version }})
12
+ runs-on: ubuntu-latest
13
+
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ python-version: ["3.10", "3.11", "3.12"]
18
+
19
+ steps:
20
+ - name: Checkout
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Set up Python ${{ matrix.python-version }}
24
+ uses: actions/setup-python@v5
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+ cache: pip
28
+
29
+ - name: Install dependencies
30
+ run: |
31
+ python -m pip install --upgrade pip
32
+ pip install -e ".[dev]"
33
+
34
+ - name: Run pytest
35
+ run: pytest -ra
@@ -0,0 +1,99 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ *.manifest
31
+ *.spec
32
+
33
+ # Installer logs
34
+ pip-log.txt
35
+ pip-delete-this-directory.txt
36
+
37
+ # Unit test / coverage reports
38
+ htmlcov/
39
+ .tox/
40
+ .nox/
41
+ .coverage
42
+ .coverage.*
43
+ .cache
44
+ nosetests.xml
45
+ coverage.xml
46
+ *.cover
47
+ *.py,cover
48
+ .hypothesis/
49
+ .pytest_cache/
50
+ cover/
51
+
52
+ # Translations
53
+ *.mo
54
+ *.pot
55
+
56
+ # Jupyter Notebook
57
+ .ipynb_checkpoints
58
+
59
+ # IPython
60
+ profile_default/
61
+ ipython_config.py
62
+
63
+ # pyenv
64
+ .python-version
65
+
66
+ # pipenv / Poetry / PDM
67
+ Pipfile.lock
68
+ poetry.lock
69
+ pdm.lock
70
+ __pypackages__/
71
+
72
+ # Environments
73
+ .env
74
+ .env.*
75
+ .venv
76
+ env/
77
+ venv/
78
+ ENV/
79
+ env.bak/
80
+ venv.bak/
81
+
82
+ # mypy / pytype / ruff
83
+ .mypy_cache/
84
+ .dmypy.json
85
+ dmypy.json
86
+ .pytype/
87
+ .ruff_cache/
88
+
89
+ # IDE / editor
90
+ .idea/
91
+ .vscode/
92
+ *.swp
93
+ *.swo
94
+ *~
95
+
96
+ # OS
97
+ .DS_Store
98
+ Thumbs.db
99
+ desktop.ini
@@ -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.
@@ -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,142 @@
1
+ # checkmax-phone-utils
2
+
3
+ [![PyPI 0.2.0](https://img.shields.io/badge/PyPI-0.2.0-blue.svg)](https://pypi.org/project/checkmax-phone-utils/)
4
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
6
+ [![CheckMaxApp](https://img.shields.io/badge/Powered_by-CheckMaxApp-orange.svg)](https://checkmaxapp.com)
7
+
8
+ A small, focused Python toolkit for phone-number **format validation**,
9
+ **E.164 normalization**, and an **optional API client** for the
10
+ [CheckMaxApp](https://checkmaxapp.com) service. Built on top of Google's
11
+ [libphonenumber](https://github.com/google/libphonenumber) (Python port).
12
+ Use it as a drop-in helper in your own code, or as the official client
13
+ library for the live CheckMaxApp REST API.
14
+
15
+ > **Powered by [CheckMaxApp](https://checkmaxapp.com) — phone validation
16
+ > service for the MAX messenger.** This library handles *format* and
17
+ > *region* validation. To check whether a number is actually registered
18
+ > on MAX, use the hosted CheckMaxApp service.
19
+
20
+ ## Quickstart
21
+
22
+ ```bash
23
+ pip install checkmax-phone-utils
24
+ ```
25
+
26
+ ```python
27
+ from checkmax_phone_utils import normalize, validate_e164, is_mobile
28
+
29
+ normalize("+7 (916) 123-45-67") # '+79161234567'
30
+ normalize("8 916 123-45-67", "RU") # '+79161234567'
31
+ validate_e164("+1 415 555 2671") # (True, '+14155552671')
32
+ is_mobile("+79161234567") # True
33
+ ```
34
+
35
+ ## What it does
36
+
37
+ | Function | Purpose |
38
+ | --- | --- |
39
+ | `normalize(raw, default_region)` | Convert any phone-shaped input to canonical E.164, or `None`. |
40
+ | `validate_e164(raw, region)` | Return `(is_valid, e164_or_none)` for a raw input. |
41
+ | `is_mobile(e164)` | True if the number is mobile (or mobile-or-fixed). |
42
+ | `detect_region(raw)` | Return ISO 3166-1 alpha-2 region of an international number. |
43
+ | `clean(raw)` | Strip everything that is not a digit or leading `+`. |
44
+ | `CheckMaxClient(api_key).check([...])` | Verify numbers via the REST API — registration status + public name. |
45
+
46
+ Full reference: see the docstrings — every public function has examples
47
+ and edge-case notes.
48
+
49
+ ## API reference
50
+
51
+ ### `normalize(raw: str, default_region: str = "RU") -> str | None`
52
+
53
+ Safe high-level entry point. Cleans, parses, validates, and returns the
54
+ canonical `+CCNNNNNNNNNNN` form, or `None` if the input is not a valid
55
+ phone number.
56
+
57
+ ```python
58
+ >>> normalize("+7 (916) 123-45-67")
59
+ '+79161234567'
60
+ >>> normalize("garbage") is None
61
+ True
62
+ ```
63
+
64
+ ### `validate_e164(raw: str, region: str = "RU") -> tuple[bool, str | None]`
65
+
66
+ Returns `(is_valid, e164)`. When valid, `e164` is the canonical form;
67
+ otherwise it is `None`. Useful when you want to keep both signals.
68
+
69
+ ### `is_mobile(e164: str) -> bool`
70
+
71
+ True for `MOBILE` and `FIXED_LINE_OR_MOBILE` numbers; False otherwise,
72
+ including when the input cannot be parsed.
73
+
74
+ ### `detect_region(raw: str) -> str | None`
75
+
76
+ Detect the ISO region from an international-format number (with `+` or
77
+ `00` prefix). Returns `None` when the input lacks a country prefix.
78
+
79
+ ### `CheckMaxClient(api_key: str)`
80
+
81
+ Official client for the [CheckMaxApp REST API](https://checkmaxapp.com/api).
82
+ Get an API key from the CheckMaxApp Telegram bot. Methods: `health()`,
83
+ `balance()`, `usage()`, `check(phones)`, `batch_create(phones)`,
84
+ `batch_status(id)`, `batch_download(id)`. Raises `AuthError` (401) and
85
+ `InsufficientBalanceError` (402).
86
+
87
+ ```python
88
+ from checkmax_phone_utils import CheckMaxClient
89
+
90
+ client = CheckMaxClient(api_key="mxk_...")
91
+ client.check(["79001234567"])
92
+ # [{'phone': '79001234567', 'status': 'registered',
93
+ # 'first_name': 'Ivan', 'last_name': 'Petrov'}]
94
+ ```
95
+
96
+ The full machine-readable schema lives at
97
+ [`openapi/checkmax-api.openapi.yaml`](openapi/checkmax-api.openapi.yaml)
98
+ (OpenAPI 3.0) — also published to the API directories.
99
+
100
+ ## Examples
101
+
102
+ The [`examples/`](examples) directory contains runnable scripts:
103
+
104
+ - `examples/basic_validation.py` — single-number validation against a
105
+ small sample list.
106
+ - `examples/bulk_normalize.py` — normalize a CSV file of phones in
107
+ bulk, emitting a CSV with `e164` and `valid` columns.
108
+ - `examples/api_client_demo.py` — intended surface of the
109
+ CheckMaxApp client.
110
+
111
+ ## Roadmap
112
+
113
+ - v0.1 — format validation, E.164 normalization, stub client.
114
+ - v0.2 — live REST client (check / batch / balance / usage), structured
115
+ errors, OpenAPI 3.0 spec (current).
116
+ - v0.3 — async client, retries, optional caching.
117
+ - v0.4 — type stubs on PyPI, CLI entry point (`checkmax phone <num>`).
118
+
119
+ ## Development
120
+
121
+ ```bash
122
+ git clone https://github.com/abragimbaliev/checkmax-phone-utils.git
123
+ cd checkmax-phone-utils
124
+ pip install -e ".[dev]"
125
+ pytest
126
+ ```
127
+
128
+ CI runs `pytest` against Python 3.10, 3.11, and 3.12 on every push.
129
+
130
+ ## License
131
+
132
+ [MIT](LICENSE). Copyright (c) 2026 CheckMax Team.
133
+
134
+ ## Authors
135
+
136
+ CheckMax Team — `dev@checkmaxapp.com`
137
+
138
+ ---
139
+
140
+ Looking for the hosted service?
141
+ **[CheckMaxApp](https://checkmaxapp.com)** — phone validation for the
142
+ MAX messenger.
@@ -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