checkharbor 0.1.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.
- checkharbor-0.1.0/PKG-INFO +73 -0
- checkharbor-0.1.0/README.md +60 -0
- checkharbor-0.1.0/checkharbor/__init__.py +214 -0
- checkharbor-0.1.0/checkharbor.egg-info/PKG-INFO +73 -0
- checkharbor-0.1.0/checkharbor.egg-info/SOURCES.txt +9 -0
- checkharbor-0.1.0/checkharbor.egg-info/dependency_links.txt +1 -0
- checkharbor-0.1.0/checkharbor.egg-info/requires.txt +1 -0
- checkharbor-0.1.0/checkharbor.egg-info/top_level.txt +1 -0
- checkharbor-0.1.0/pyproject.toml +25 -0
- checkharbor-0.1.0/setup.cfg +4 -0
- checkharbor-0.1.0/tests/test_checkharbor.py +266 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: checkharbor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Checkharbor Python SDK — email/phone/IP validation
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://checkharbor.com
|
|
7
|
+
Project-URL: Documentation, https://docs.checkharbor.com
|
|
8
|
+
Project-URL: Repository, https://github.com/checkharbor/checkharbor-python
|
|
9
|
+
Keywords: checkharbor,email-validation,phone-validation,ip-intelligence
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: requests>=2.28
|
|
13
|
+
|
|
14
|
+
# checkharbor
|
|
15
|
+
|
|
16
|
+
Official Checkharbor Python SDK.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install checkharbor
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from checkharbor import Checkharbor
|
|
28
|
+
|
|
29
|
+
client = Checkharbor(api_key="chk_live_...")
|
|
30
|
+
|
|
31
|
+
# Validate email
|
|
32
|
+
email = client.validate_email("john@example.com")
|
|
33
|
+
print(email["score"], email["deliverability"])
|
|
34
|
+
|
|
35
|
+
# Validate phone
|
|
36
|
+
phone = client.validate_phone("+905321234567", country_hint="TR")
|
|
37
|
+
|
|
38
|
+
# IP intelligence
|
|
39
|
+
ip = client.ip_intel("84.17.45.10")
|
|
40
|
+
|
|
41
|
+
# Unified fraud check
|
|
42
|
+
fraud = client.verify(email="john@example.com", ip="84.17.45.10")
|
|
43
|
+
print(fraud["fraud_score"], fraud["risk"])
|
|
44
|
+
|
|
45
|
+
# Batch
|
|
46
|
+
job = client.batch.create(
|
|
47
|
+
type="email",
|
|
48
|
+
rows=["a@example.com", "b@example.com"],
|
|
49
|
+
webhook_url="https://myapp.com/webhook",
|
|
50
|
+
)
|
|
51
|
+
done = client.batch.wait_until_done(job["id"])
|
|
52
|
+
csv = client.batch.download_result(done)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Error handling
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from checkharbor import CheckharborError
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
result = client.validate_email("bad@test.com")
|
|
62
|
+
except CheckharborError as e:
|
|
63
|
+
print(e.code, e.status, str(e))
|
|
64
|
+
# e.g. "insufficient_credits" 402 "Not enough credits"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Development
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
71
|
+
pip install -e ".[dev]"
|
|
72
|
+
pytest
|
|
73
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# checkharbor
|
|
2
|
+
|
|
3
|
+
Official Checkharbor Python SDK.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install checkharbor
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from checkharbor import Checkharbor
|
|
15
|
+
|
|
16
|
+
client = Checkharbor(api_key="chk_live_...")
|
|
17
|
+
|
|
18
|
+
# Validate email
|
|
19
|
+
email = client.validate_email("john@example.com")
|
|
20
|
+
print(email["score"], email["deliverability"])
|
|
21
|
+
|
|
22
|
+
# Validate phone
|
|
23
|
+
phone = client.validate_phone("+905321234567", country_hint="TR")
|
|
24
|
+
|
|
25
|
+
# IP intelligence
|
|
26
|
+
ip = client.ip_intel("84.17.45.10")
|
|
27
|
+
|
|
28
|
+
# Unified fraud check
|
|
29
|
+
fraud = client.verify(email="john@example.com", ip="84.17.45.10")
|
|
30
|
+
print(fraud["fraud_score"], fraud["risk"])
|
|
31
|
+
|
|
32
|
+
# Batch
|
|
33
|
+
job = client.batch.create(
|
|
34
|
+
type="email",
|
|
35
|
+
rows=["a@example.com", "b@example.com"],
|
|
36
|
+
webhook_url="https://myapp.com/webhook",
|
|
37
|
+
)
|
|
38
|
+
done = client.batch.wait_until_done(job["id"])
|
|
39
|
+
csv = client.batch.download_result(done)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Error handling
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from checkharbor import CheckharborError
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
result = client.validate_email("bad@test.com")
|
|
49
|
+
except CheckharborError as e:
|
|
50
|
+
print(e.code, e.status, str(e))
|
|
51
|
+
# e.g. "insufficient_credits" 402 "Not enough credits"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Development
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
58
|
+
pip install -e ".[dev]"
|
|
59
|
+
pytest
|
|
60
|
+
```
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Checkharbor Python SDK — single-file, zero-dependency beyond `requests`.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from checkharbor import Checkharbor, CheckharborError
|
|
6
|
+
|
|
7
|
+
client = Checkharbor(api_key="chk_live_...")
|
|
8
|
+
result = client.validate_email("john@example.com")
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import csv
|
|
14
|
+
import io
|
|
15
|
+
import time
|
|
16
|
+
from typing import Any, Optional, Union
|
|
17
|
+
|
|
18
|
+
import requests
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
__all__ = ["Checkharbor", "CheckharborError"]
|
|
22
|
+
|
|
23
|
+
DEFAULT_BASE_URL = "https://api.checkharbor.com"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CheckharborError(Exception):
|
|
27
|
+
"""Raised for any non-2xx response from the Checkharbor API."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, code: str, message: str, status: int) -> None:
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
self.code = code
|
|
32
|
+
self.status = status
|
|
33
|
+
|
|
34
|
+
def __repr__(self) -> str:
|
|
35
|
+
return f"CheckharborError(code={self.code!r}, status={self.status}, message={str(self)!r})"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _BatchNamespace:
|
|
39
|
+
def __init__(self, client: "Checkharbor") -> None:
|
|
40
|
+
self._client = client
|
|
41
|
+
|
|
42
|
+
def create(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
type: str,
|
|
46
|
+
rows: Optional[list[str]] = None,
|
|
47
|
+
csv_string: Optional[str] = None,
|
|
48
|
+
webhook_url: Optional[str] = None,
|
|
49
|
+
) -> dict[str, Any]:
|
|
50
|
+
"""
|
|
51
|
+
Create a batch job.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
type: "email", "phone", or "ip"
|
|
55
|
+
rows: list of values (will be joined as CSV)
|
|
56
|
+
csv_string: raw CSV content (alternative to rows)
|
|
57
|
+
webhook_url: optional callback URL when job completes
|
|
58
|
+
"""
|
|
59
|
+
if csv_string is None and not rows:
|
|
60
|
+
raise ValueError("batch.create() requires either rows or csv_string")
|
|
61
|
+
|
|
62
|
+
content = csv_string if csv_string is not None else "\n".join(rows) # type: ignore[arg-type]
|
|
63
|
+
|
|
64
|
+
files = {"file": ("data.csv", io.BytesIO(content.encode()), "text/csv")}
|
|
65
|
+
data: dict[str, str] = {"type": type}
|
|
66
|
+
if webhook_url:
|
|
67
|
+
data["webhook_url"] = webhook_url
|
|
68
|
+
|
|
69
|
+
return self._client._request("POST", "/v1/batch", data=data, files=files)
|
|
70
|
+
|
|
71
|
+
def get(self, job_id: str) -> dict[str, Any]:
|
|
72
|
+
"""Fetch the status of a batch job."""
|
|
73
|
+
return self._client._request("GET", f"/v1/batch/{job_id}")
|
|
74
|
+
|
|
75
|
+
def wait_until_done(
|
|
76
|
+
self,
|
|
77
|
+
job_id: str,
|
|
78
|
+
*,
|
|
79
|
+
poll_seconds: float = 2.0,
|
|
80
|
+
timeout_seconds: float = 300.0,
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
"""
|
|
83
|
+
Block until the batch job is done or failed.
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
CheckharborError: if job fails or timeout is exceeded.
|
|
87
|
+
"""
|
|
88
|
+
deadline = time.monotonic() + timeout_seconds
|
|
89
|
+
while True:
|
|
90
|
+
job = self.get(job_id)
|
|
91
|
+
status = job.get("status")
|
|
92
|
+
if status == "done":
|
|
93
|
+
return job
|
|
94
|
+
if status == "failed":
|
|
95
|
+
raise CheckharborError("batch_failed", f"Batch job {job_id} failed", 200)
|
|
96
|
+
if time.monotonic() >= deadline:
|
|
97
|
+
raise CheckharborError(
|
|
98
|
+
"timeout",
|
|
99
|
+
f"Batch job {job_id} timed out after {timeout_seconds}s",
|
|
100
|
+
0,
|
|
101
|
+
)
|
|
102
|
+
time.sleep(poll_seconds)
|
|
103
|
+
|
|
104
|
+
def download_result(self, job: dict[str, Any]) -> str:
|
|
105
|
+
"""
|
|
106
|
+
Download result CSV from a completed batch job.
|
|
107
|
+
The result_url is a signed URL — no auth header needed.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
CSV content as a string.
|
|
111
|
+
"""
|
|
112
|
+
if job.get("status") != "done":
|
|
113
|
+
raise CheckharborError("not_done", "Job is not done yet", 0)
|
|
114
|
+
result_url = job.get("result_url")
|
|
115
|
+
if not result_url:
|
|
116
|
+
raise CheckharborError("no_result_url", "Job has no result_url", 0)
|
|
117
|
+
resp = requests.get(result_url, timeout=30)
|
|
118
|
+
if not resp.ok:
|
|
119
|
+
raise CheckharborError("download_failed", f"Download failed: HTTP {resp.status_code}", resp.status_code)
|
|
120
|
+
return resp.text
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class Checkharbor:
|
|
124
|
+
"""
|
|
125
|
+
Checkharbor API client.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
api_key: Your Checkharbor API key (X-Api-Key header).
|
|
129
|
+
base_url: Override the default API base URL.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def __init__(
|
|
133
|
+
self,
|
|
134
|
+
api_key: str,
|
|
135
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
136
|
+
) -> None:
|
|
137
|
+
if not api_key:
|
|
138
|
+
raise ValueError("api_key is required")
|
|
139
|
+
self._api_key = api_key
|
|
140
|
+
self._base_url = base_url.rstrip("/")
|
|
141
|
+
self._session = requests.Session()
|
|
142
|
+
self._session.headers["X-Api-Key"] = api_key
|
|
143
|
+
self.batch = _BatchNamespace(self)
|
|
144
|
+
|
|
145
|
+
def _request(
|
|
146
|
+
self,
|
|
147
|
+
method: str,
|
|
148
|
+
path: str,
|
|
149
|
+
json: Optional[dict[str, Any]] = None,
|
|
150
|
+
data: Optional[dict[str, str]] = None,
|
|
151
|
+
files: Optional[Any] = None,
|
|
152
|
+
) -> dict[str, Any]:
|
|
153
|
+
url = f"{self._base_url}{path}"
|
|
154
|
+
resp = self._session.request(method, url, json=json, data=data, files=files, timeout=30)
|
|
155
|
+
if not resp.ok:
|
|
156
|
+
code = "api_error"
|
|
157
|
+
message = f"HTTP {resp.status_code}"
|
|
158
|
+
try:
|
|
159
|
+
body = resp.json()
|
|
160
|
+
code = body.get("error", {}).get("code", code)
|
|
161
|
+
message = body.get("error", {}).get("message", message)
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
raise CheckharborError(code, message, resp.status_code)
|
|
165
|
+
return resp.json() # type: ignore[return-value]
|
|
166
|
+
|
|
167
|
+
def ping(self) -> dict[str, Any]:
|
|
168
|
+
"""Health check — returns ok + credits_remaining."""
|
|
169
|
+
return self._request("GET", "/v1/ping")
|
|
170
|
+
|
|
171
|
+
def validate_email(self, email: str) -> dict[str, Any]:
|
|
172
|
+
"""Validate a single email address (1 credit)."""
|
|
173
|
+
return self._request("POST", "/v1/email/validate", json={"email": email})
|
|
174
|
+
|
|
175
|
+
def validate_phone(
|
|
176
|
+
self,
|
|
177
|
+
phone: str,
|
|
178
|
+
*,
|
|
179
|
+
country_hint: Optional[str] = None,
|
|
180
|
+
hlr: Optional[bool] = None,
|
|
181
|
+
) -> dict[str, Any]:
|
|
182
|
+
"""Validate a phone number (1 credit offline, 5 with HLR)."""
|
|
183
|
+
payload: dict[str, Any] = {"phone": phone}
|
|
184
|
+
if country_hint is not None:
|
|
185
|
+
payload["country_hint"] = country_hint
|
|
186
|
+
if hlr is not None:
|
|
187
|
+
payload["hlr"] = hlr
|
|
188
|
+
return self._request("POST", "/v1/phone/validate", json=payload)
|
|
189
|
+
|
|
190
|
+
def ip_intel(self, ip: str) -> dict[str, Any]:
|
|
191
|
+
"""IP intelligence lookup (1 credit)."""
|
|
192
|
+
return self._request("POST", "/v1/ip/intel", json={"ip": ip})
|
|
193
|
+
|
|
194
|
+
def verify(
|
|
195
|
+
self,
|
|
196
|
+
*,
|
|
197
|
+
email: Optional[str] = None,
|
|
198
|
+
phone: Optional[str] = None,
|
|
199
|
+
ip: Optional[str] = None,
|
|
200
|
+
) -> dict[str, Any]:
|
|
201
|
+
"""
|
|
202
|
+
Unified fraud scoring (3 credits).
|
|
203
|
+
At least one of email, phone, ip is required.
|
|
204
|
+
"""
|
|
205
|
+
if not any([email, phone, ip]):
|
|
206
|
+
raise ValueError("verify() requires at least one of: email, phone, ip")
|
|
207
|
+
payload: dict[str, Any] = {}
|
|
208
|
+
if email:
|
|
209
|
+
payload["email"] = email
|
|
210
|
+
if phone:
|
|
211
|
+
payload["phone"] = phone
|
|
212
|
+
if ip:
|
|
213
|
+
payload["ip"] = ip
|
|
214
|
+
return self._request("POST", "/v1/verify", json=payload)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: checkharbor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Checkharbor Python SDK — email/phone/IP validation
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://checkharbor.com
|
|
7
|
+
Project-URL: Documentation, https://docs.checkharbor.com
|
|
8
|
+
Project-URL: Repository, https://github.com/checkharbor/checkharbor-python
|
|
9
|
+
Keywords: checkharbor,email-validation,phone-validation,ip-intelligence
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: requests>=2.28
|
|
13
|
+
|
|
14
|
+
# checkharbor
|
|
15
|
+
|
|
16
|
+
Official Checkharbor Python SDK.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install checkharbor
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from checkharbor import Checkharbor
|
|
28
|
+
|
|
29
|
+
client = Checkharbor(api_key="chk_live_...")
|
|
30
|
+
|
|
31
|
+
# Validate email
|
|
32
|
+
email = client.validate_email("john@example.com")
|
|
33
|
+
print(email["score"], email["deliverability"])
|
|
34
|
+
|
|
35
|
+
# Validate phone
|
|
36
|
+
phone = client.validate_phone("+905321234567", country_hint="TR")
|
|
37
|
+
|
|
38
|
+
# IP intelligence
|
|
39
|
+
ip = client.ip_intel("84.17.45.10")
|
|
40
|
+
|
|
41
|
+
# Unified fraud check
|
|
42
|
+
fraud = client.verify(email="john@example.com", ip="84.17.45.10")
|
|
43
|
+
print(fraud["fraud_score"], fraud["risk"])
|
|
44
|
+
|
|
45
|
+
# Batch
|
|
46
|
+
job = client.batch.create(
|
|
47
|
+
type="email",
|
|
48
|
+
rows=["a@example.com", "b@example.com"],
|
|
49
|
+
webhook_url="https://myapp.com/webhook",
|
|
50
|
+
)
|
|
51
|
+
done = client.batch.wait_until_done(job["id"])
|
|
52
|
+
csv = client.batch.download_result(done)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Error handling
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from checkharbor import CheckharborError
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
result = client.validate_email("bad@test.com")
|
|
62
|
+
except CheckharborError as e:
|
|
63
|
+
print(e.code, e.status, str(e))
|
|
64
|
+
# e.g. "insufficient_credits" 402 "Not enough credits"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Development
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
71
|
+
pip install -e ".[dev]"
|
|
72
|
+
pytest
|
|
73
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
checkharbor/__init__.py
|
|
4
|
+
checkharbor.egg-info/PKG-INFO
|
|
5
|
+
checkharbor.egg-info/SOURCES.txt
|
|
6
|
+
checkharbor.egg-info/dependency_links.txt
|
|
7
|
+
checkharbor.egg-info/requires.txt
|
|
8
|
+
checkharbor.egg-info/top_level.txt
|
|
9
|
+
tests/test_checkharbor.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.28
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
checkharbor
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "checkharbor"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Checkharbor Python SDK — email/phone/IP validation"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["checkharbor", "email-validation", "phone-validation", "ip-intelligence"]
|
|
13
|
+
dependencies = ["requests>=2.28"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://checkharbor.com"
|
|
17
|
+
Documentation = "https://docs.checkharbor.com"
|
|
18
|
+
Repository = "https://github.com/checkharbor/checkharbor-python"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
where = ["."]
|
|
22
|
+
include = ["checkharbor*"]
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Minimal pytest tests for the Checkharbor Python SDK using unittest.mock."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import time
|
|
7
|
+
from unittest.mock import MagicMock, patch, call
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from checkharbor import Checkharbor, CheckharborError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ── fixtures ──────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
EMAIL_OK = {
|
|
16
|
+
"input": "test@example.com",
|
|
17
|
+
"valid_syntax": True,
|
|
18
|
+
"score": 90,
|
|
19
|
+
"credits_used": 1,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
PHONE_OK = {
|
|
23
|
+
"input": "+905321234567",
|
|
24
|
+
"valid": True,
|
|
25
|
+
"e164": "+905321234567",
|
|
26
|
+
"country": "TR",
|
|
27
|
+
"line_type": "mobile",
|
|
28
|
+
"credits_used": 1,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
IP_OK = {
|
|
32
|
+
"input": "84.17.45.10",
|
|
33
|
+
"vpn": False,
|
|
34
|
+
"proxy": False,
|
|
35
|
+
"tor": False,
|
|
36
|
+
"datacenter": False,
|
|
37
|
+
"abuse_score": 5,
|
|
38
|
+
"credits_used": 1,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
VERIFY_OK = {
|
|
42
|
+
"fraud_score": 20,
|
|
43
|
+
"risk": "low",
|
|
44
|
+
"credits_used": 3,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
BATCH_QUEUED = {
|
|
48
|
+
"id": "job_123",
|
|
49
|
+
"status": "queued",
|
|
50
|
+
"type": "email",
|
|
51
|
+
"total_rows": 3,
|
|
52
|
+
"processed_rows": 0,
|
|
53
|
+
"result_url": None,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
BATCH_DONE = {
|
|
57
|
+
**BATCH_QUEUED,
|
|
58
|
+
"status": "done",
|
|
59
|
+
"processed_rows": 3,
|
|
60
|
+
"result_url": "https://cdn.checkharbor.com/results/job_123.csv?sig=abc",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def make_response(body: dict, status: int = 200) -> MagicMock:
|
|
65
|
+
resp = MagicMock()
|
|
66
|
+
resp.ok = 200 <= status < 300
|
|
67
|
+
resp.status_code = status
|
|
68
|
+
resp.json.return_value = body
|
|
69
|
+
resp.text = "csv,data\na,b"
|
|
70
|
+
return resp
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.fixture
|
|
74
|
+
def client() -> Checkharbor:
|
|
75
|
+
return Checkharbor(api_key="test-key", base_url="http://localhost:4000")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ── constructor ───────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
def test_constructor_requires_api_key():
|
|
81
|
+
with pytest.raises(ValueError, match="api_key is required"):
|
|
82
|
+
Checkharbor(api_key="")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_constructor_strips_trailing_slash():
|
|
86
|
+
c = Checkharbor(api_key="k", base_url="http://localhost:4000/")
|
|
87
|
+
assert c._base_url == "http://localhost:4000"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ── ping ──────────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
def test_ping(client: Checkharbor):
|
|
93
|
+
with patch.object(client._session, "request", return_value=make_response({"ok": True, "credits_remaining": 500})) as mock:
|
|
94
|
+
result = client.ping()
|
|
95
|
+
assert result["ok"] is True
|
|
96
|
+
assert result["credits_remaining"] == 500
|
|
97
|
+
mock.assert_called_once_with("GET", "http://localhost:4000/v1/ping", json=None, data=None, files=None, timeout=30)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ── validate_email ────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
def test_validate_email(client: Checkharbor):
|
|
103
|
+
with patch.object(client._session, "request", return_value=make_response(EMAIL_OK)) as mock:
|
|
104
|
+
result = client.validate_email("test@example.com")
|
|
105
|
+
assert result["valid_syntax"] is True
|
|
106
|
+
assert result["score"] == 90
|
|
107
|
+
mock.assert_called_once_with(
|
|
108
|
+
"POST", "http://localhost:4000/v1/email/validate",
|
|
109
|
+
json={"email": "test@example.com"}, data=None, files=None, timeout=30
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ── validate_phone ────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
def test_validate_phone_with_options(client: Checkharbor):
|
|
116
|
+
with patch.object(client._session, "request", return_value=make_response(PHONE_OK)) as mock:
|
|
117
|
+
result = client.validate_phone("+905321234567", country_hint="TR", hlr=False)
|
|
118
|
+
assert result["valid"] is True
|
|
119
|
+
mock.assert_called_once_with(
|
|
120
|
+
"POST", "http://localhost:4000/v1/phone/validate",
|
|
121
|
+
json={"phone": "+905321234567", "country_hint": "TR", "hlr": False},
|
|
122
|
+
data=None, files=None, timeout=30
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_validate_phone_minimal(client: Checkharbor):
|
|
127
|
+
with patch.object(client._session, "request", return_value=make_response(PHONE_OK)) as mock:
|
|
128
|
+
client.validate_phone("+905321234567")
|
|
129
|
+
called_json = mock.call_args.kwargs["json"]
|
|
130
|
+
assert called_json == {"phone": "+905321234567"}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ── ip_intel ──────────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
def test_ip_intel(client: Checkharbor):
|
|
136
|
+
with patch.object(client._session, "request", return_value=make_response(IP_OK)):
|
|
137
|
+
result = client.ip_intel("84.17.45.10")
|
|
138
|
+
assert result["input"] == "84.17.45.10"
|
|
139
|
+
assert result["credits_used"] == 1
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ── verify ────────────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
def test_verify(client: Checkharbor):
|
|
145
|
+
with patch.object(client._session, "request", return_value=make_response(VERIFY_OK)) as mock:
|
|
146
|
+
result = client.verify(email="test@example.com", ip="1.2.3.4")
|
|
147
|
+
assert result["fraud_score"] == 20
|
|
148
|
+
assert result["risk"] == "low"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_verify_requires_at_least_one_field(client: Checkharbor):
|
|
152
|
+
with pytest.raises(ValueError, match="at least one of"):
|
|
153
|
+
client.verify()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ── batch ─────────────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
def test_batch_create_with_rows(client: Checkharbor):
|
|
159
|
+
with patch.object(client._session, "request", return_value=make_response(BATCH_QUEUED, 202)) as mock:
|
|
160
|
+
job = client.batch.create(
|
|
161
|
+
type="email",
|
|
162
|
+
rows=["a@example.com", "b@example.com", "c@example.com"],
|
|
163
|
+
)
|
|
164
|
+
assert job["id"] == "job_123"
|
|
165
|
+
assert job["status"] == "queued"
|
|
166
|
+
# Confirm multipart call (data + files, no json)
|
|
167
|
+
call_kwargs = mock.call_args.kwargs
|
|
168
|
+
assert call_kwargs["json"] is None
|
|
169
|
+
assert call_kwargs["data"] == {"type": "email"}
|
|
170
|
+
assert "file" in call_kwargs["files"]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_batch_create_with_csv_string(client: Checkharbor):
|
|
174
|
+
with patch.object(client._session, "request", return_value=make_response(BATCH_QUEUED, 202)) as mock:
|
|
175
|
+
client.batch.create(type="email", csv_string="a@b.com\nc@d.com")
|
|
176
|
+
call_kwargs = mock.call_args.kwargs
|
|
177
|
+
assert call_kwargs["data"] == {"type": "email"}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_batch_create_raises_without_data(client: Checkharbor):
|
|
181
|
+
with pytest.raises(ValueError, match="rows or csv_string"):
|
|
182
|
+
client.batch.create(type="email")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_batch_get(client: Checkharbor):
|
|
186
|
+
with patch.object(client._session, "request", return_value=make_response(BATCH_DONE)) as mock:
|
|
187
|
+
job = client.batch.get("job_123")
|
|
188
|
+
assert job["status"] == "done"
|
|
189
|
+
mock.assert_called_once_with(
|
|
190
|
+
"GET", "http://localhost:4000/v1/batch/job_123",
|
|
191
|
+
json=None, data=None, files=None, timeout=30
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_batch_wait_until_done_polls(client: Checkharbor):
|
|
196
|
+
responses = [
|
|
197
|
+
make_response(BATCH_QUEUED),
|
|
198
|
+
make_response({**BATCH_QUEUED, "status": "processing"}),
|
|
199
|
+
make_response(BATCH_DONE),
|
|
200
|
+
]
|
|
201
|
+
with patch.object(client._session, "request", side_effect=responses):
|
|
202
|
+
with patch("time.sleep"): # don't actually sleep
|
|
203
|
+
job = client.batch.wait_until_done("job_123", poll_seconds=0.001)
|
|
204
|
+
assert job["status"] == "done"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_batch_wait_until_done_fails(client: Checkharbor):
|
|
208
|
+
failed = {**BATCH_QUEUED, "status": "failed"}
|
|
209
|
+
with patch.object(client._session, "request", return_value=make_response(failed)):
|
|
210
|
+
with pytest.raises(CheckharborError) as exc_info:
|
|
211
|
+
client.batch.wait_until_done("job_123", poll_seconds=0.001)
|
|
212
|
+
assert exc_info.value.code == "batch_failed"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_batch_wait_until_done_timeout(client: Checkharbor):
|
|
216
|
+
with patch.object(client._session, "request", return_value=make_response(BATCH_QUEUED)):
|
|
217
|
+
with patch("time.sleep"):
|
|
218
|
+
with patch("time.monotonic", side_effect=[0.0, 0.0, 1000.0]):
|
|
219
|
+
with pytest.raises(CheckharborError) as exc_info:
|
|
220
|
+
client.batch.wait_until_done("job_123", poll_seconds=0.001, timeout_seconds=1.0)
|
|
221
|
+
assert exc_info.value.code == "timeout"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_batch_download_result(client: Checkharbor):
|
|
225
|
+
csv_content = "email,valid\na@b.com,true"
|
|
226
|
+
download_resp = MagicMock()
|
|
227
|
+
download_resp.ok = True
|
|
228
|
+
download_resp.status_code = 200
|
|
229
|
+
download_resp.text = csv_content
|
|
230
|
+
|
|
231
|
+
with patch("requests.get", return_value=download_resp) as mock_get:
|
|
232
|
+
result = client.batch.download_result(BATCH_DONE)
|
|
233
|
+
assert result == csv_content
|
|
234
|
+
mock_get.assert_called_once_with(BATCH_DONE["result_url"], timeout=30)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_batch_download_result_raises_if_not_done(client: Checkharbor):
|
|
238
|
+
with pytest.raises(CheckharborError) as exc_info:
|
|
239
|
+
client.batch.download_result(BATCH_QUEUED)
|
|
240
|
+
assert exc_info.value.code == "not_done"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ── error handling ────────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
def test_raises_checkharbor_error_on_402(client: Checkharbor):
|
|
246
|
+
err_resp = make_response(
|
|
247
|
+
{"error": {"code": "insufficient_credits", "message": "Not enough credits"}},
|
|
248
|
+
status=402,
|
|
249
|
+
)
|
|
250
|
+
with patch.object(client._session, "request", return_value=err_resp):
|
|
251
|
+
with pytest.raises(CheckharborError) as exc_info:
|
|
252
|
+
client.validate_email("x@y.com")
|
|
253
|
+
assert exc_info.value.code == "insufficient_credits"
|
|
254
|
+
assert exc_info.value.status == 402
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_raises_checkharbor_error_on_429(client: Checkharbor):
|
|
258
|
+
err_resp = make_response(
|
|
259
|
+
{"error": {"code": "rate_limited", "message": "Too many requests"}},
|
|
260
|
+
status=429,
|
|
261
|
+
)
|
|
262
|
+
with patch.object(client._session, "request", return_value=err_resp):
|
|
263
|
+
with pytest.raises(CheckharborError) as exc_info:
|
|
264
|
+
client.ip_intel("1.2.3.4")
|
|
265
|
+
assert exc_info.value.code == "rate_limited"
|
|
266
|
+
assert exc_info.value.status == 429
|