veqen 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.
veqen-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Keyveve, Inc.
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.
veqen-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: veqen
3
+ Version: 0.1.0
4
+ Summary: Official Python client for the Veqen signup-check API: disposable emails, datacenter IPs, VoIP numbers and more.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://veqen-app.com
7
+ Project-URL: Documentation, https://veqen-app.com/docs
8
+ Project-URL: Source, https://github.com/Keyveve/veqen-sdks/tree/main/python
9
+ Project-URL: Issues, https://github.com/Keyveve/veqen-sdks/issues
10
+ Keywords: email validation,disposable email,fraud,signup,ip lookup,phone validation,vpn detection
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Topic :: Security
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: httpx>=0.25
18
+ Requires-Dist: certifi>=2026.10.19
19
+ Dynamic: license-file
20
+
21
+ # veqen (Python)
22
+
23
+ Official Python client for [Veqen](https://veqen-app.com): catch disposable emails, typos, datacenter and Tor IPs, and VoIP numbers with one API call.
24
+
25
+ ```bash
26
+ pip install https://veqen-app.com/static/sdk/veqen-0.1.0-py3-none-any.whl
27
+ ```
28
+
29
+ ```python
30
+ from veqen import Veqen, VeqenError
31
+
32
+ client = Veqen() # reads VEQEN_API_KEY from the environment
33
+
34
+ result = client.signup(email="jane@mailinator.com", ip="185.220.101.1")
35
+ print(result["risk"]["verdict"]) # "refuse"
36
+ for reason in result["risk"]["reasons"]:
37
+ print(reason["code"], reason["message"])
38
+ ```
39
+
40
+ | Method | Endpoint | Credits |
41
+ |---|---|---|
42
+ | `email(address, verify_mailbox=False)` | `/v1/email/check` | 1 (+5) |
43
+ | `emails(list)` | `/v1/email/batch` (chunks of 100) | 1 each |
44
+ | `ip(address)` | `/v1/ip/check` | 1 |
45
+ | `phone(number, country=None)` | `/v1/phone/check` | 1 |
46
+ | `domain(name)` | `/v1/domain/check` | 1 |
47
+ | `signup(email=, ip=, phone=, phone_country=)` | `/v1/signup/check` | 3 |
48
+ | `account()`, `usage(days=30)` | `/v1/account`, `/v1/usage` | 0 |
49
+
50
+ Errors raise `VeqenError` with `.status`, `.type` (e.g. `insufficient_credits`) and `.message`. Requests are retried on 429 and 5xx. Docs: https://veqen-app.com/docs
veqen-0.1.0/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # veqen (Python)
2
+
3
+ Official Python client for [Veqen](https://veqen-app.com): catch disposable emails, typos, datacenter and Tor IPs, and VoIP numbers with one API call.
4
+
5
+ ```bash
6
+ pip install https://veqen-app.com/static/sdk/veqen-0.1.0-py3-none-any.whl
7
+ ```
8
+
9
+ ```python
10
+ from veqen import Veqen, VeqenError
11
+
12
+ client = Veqen() # reads VEQEN_API_KEY from the environment
13
+
14
+ result = client.signup(email="jane@mailinator.com", ip="185.220.101.1")
15
+ print(result["risk"]["verdict"]) # "refuse"
16
+ for reason in result["risk"]["reasons"]:
17
+ print(reason["code"], reason["message"])
18
+ ```
19
+
20
+ | Method | Endpoint | Credits |
21
+ |---|---|---|
22
+ | `email(address, verify_mailbox=False)` | `/v1/email/check` | 1 (+5) |
23
+ | `emails(list)` | `/v1/email/batch` (chunks of 100) | 1 each |
24
+ | `ip(address)` | `/v1/ip/check` | 1 |
25
+ | `phone(number, country=None)` | `/v1/phone/check` | 1 |
26
+ | `domain(name)` | `/v1/domain/check` | 1 |
27
+ | `signup(email=, ip=, phone=, phone_country=)` | `/v1/signup/check` | 3 |
28
+ | `account()`, `usage(days=30)` | `/v1/account`, `/v1/usage` | 0 |
29
+
30
+ Errors raise `VeqenError` with `.status`, `.type` (e.g. `insufficient_credits`) and `.message`. Requests are retried on 429 and 5xx. Docs: https://veqen-app.com/docs
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "veqen"
3
+ version = "0.1.0"
4
+ description = "Official Python client for the Veqen signup-check API: disposable emails, datacenter IPs, VoIP numbers and more."
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ dependencies = ["httpx>=0.25", "certifi>=2026.10.19"]
10
+ keywords = ["email validation", "disposable email", "fraud", "signup", "ip lookup", "phone validation", "vpn detection"]
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3",
13
+ "Intended Audience :: Developers",
14
+ "Topic :: Security",
15
+ ]
16
+
17
+ [project.urls]
18
+ Homepage = "https://veqen-app.com"
19
+ Documentation = "https://veqen-app.com/docs"
20
+ Source = "https://github.com/Keyveve/veqen-sdks/tree/main/python"
21
+ Issues = "https://github.com/Keyveve/veqen-sdks/issues"
22
+
23
+ [build-system]
24
+ requires = ["setuptools>=77"]
25
+ build-backend = "setuptools.build_meta"
26
+
27
+ [tool.setuptools]
28
+ packages = ["veqen"]
veqen-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,90 @@
1
+ import json
2
+
3
+ import httpx
4
+ import pytest
5
+
6
+ from veqen import Veqen, VeqenError
7
+
8
+
9
+ def make(handler, **kwargs):
10
+ return Veqen("vq_live_test", base_url="https://api.test", transport=httpx.MockTransport(handler), **kwargs)
11
+
12
+
13
+ def test_signup_sends_auth_and_drops_empty_fields():
14
+ seen = {}
15
+
16
+ def handler(request):
17
+ seen["auth"] = request.headers["Authorization"]
18
+ seen["path"] = request.url.path
19
+ seen["body"] = json.loads(request.content)
20
+ return httpx.Response(200, json={"risk": {"verdict": "refuse"}})
21
+
22
+ result = make(handler).signup(email="a@b.com", ip="1.2.3.4")
23
+ assert result["risk"]["verdict"] == "refuse"
24
+ assert seen == {"auth": "Bearer vq_live_test", "path": "/v1/signup/check", "body": {"email": "a@b.com", "ip": "1.2.3.4", "verify_mailbox": False}}
25
+
26
+
27
+ def test_errors_raise_with_type():
28
+ def handler(request):
29
+ return httpx.Response(402, json={"error": {"type": "insufficient_credits", "message": "Buy credits"}}, headers={"X-Request-Id": "r1"})
30
+
31
+ with pytest.raises(VeqenError) as info:
32
+ make(handler).email("a@b.com")
33
+ assert info.value.status == 402 and info.value.type == "insufficient_credits" and info.value.request_id == "r1"
34
+
35
+
36
+ def test_retries_on_429(monkeypatch):
37
+ monkeypatch.setattr("time.sleep", lambda s: None)
38
+ calls = []
39
+
40
+ def handler(request):
41
+ calls.append(1)
42
+ if len(calls) < 2:
43
+ return httpx.Response(429, json={"error": {"type": "rate_limited", "message": "slow"}}, headers={"Retry-After": "1"})
44
+ return httpx.Response(200, json={"ok": True})
45
+
46
+ assert make(handler).ip("1.2.3.4") == {"ok": True}
47
+ assert len(calls) == 2
48
+
49
+
50
+ def test_batch_chunks_by_100():
51
+ sizes = []
52
+
53
+ def handler(request):
54
+ emails = json.loads(request.content)["emails"]
55
+ sizes.append(len(emails))
56
+ return httpx.Response(200, json={"results": [{"email": e} for e in emails], "count": len(emails)})
57
+
58
+ results = make(handler).emails([f"u{i}@a.com" for i in range(250)])
59
+ assert sizes == [100, 100, 50] and len(results) == 250
60
+
61
+
62
+ def test_idempotency_header():
63
+ def handler(request):
64
+ assert request.headers["Idempotency-Key"] == "k1"
65
+ return httpx.Response(200, json={})
66
+
67
+ make(handler).phone("+447400123456", idempotency_key="k1")
68
+
69
+
70
+ def test_requires_key(monkeypatch):
71
+ monkeypatch.delenv("VEQEN_API_KEY", raising=False)
72
+ with pytest.raises(ValueError):
73
+ Veqen()
74
+
75
+
76
+ def test_retry_preserves_automatic_idempotency_key(monkeypatch):
77
+ monkeypatch.setattr("time.sleep", lambda s: None)
78
+ keys = []
79
+
80
+ def handler(request):
81
+ keys.append(request.headers["Idempotency-Key"])
82
+ if len(keys) == 1:
83
+ raise httpx.ReadTimeout("Response lost after server charged", request=request)
84
+ return httpx.Response(200, json={})
85
+
86
+ client = make(handler)
87
+ client.ip("1.2.3.4")
88
+ client.ip("1.2.3.4")
89
+ assert keys[0] == keys[1]
90
+ assert keys[1] != keys[2]
@@ -0,0 +1,143 @@
1
+ """Official Python client for the Veqen signup-check API.
2
+
3
+ from veqen import Veqen
4
+
5
+ client = Veqen() # reads VEQEN_API_KEY
6
+ result = client.signup(email="jane@mailinator.com", ip="185.220.101.1")
7
+ print(result["risk"]["verdict"])
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import time
14
+ import uuid
15
+ from typing import Any
16
+
17
+ import httpx
18
+
19
+ __all__ = ["Veqen", "VeqenError"]
20
+ __version__ = "0.1.0"
21
+
22
+ DEFAULT_BASE_URL = "https://veqen-app.com"
23
+ RETRY_STATUSES = {429, 500, 502, 503, 504}
24
+
25
+
26
+ class VeqenError(Exception):
27
+ """Raised for any non-2xx response. ``type`` matches the API's error.type."""
28
+
29
+ def __init__(self, status: int, type_: str, message: str, request_id: str | None = None):
30
+ super().__init__(f"{status} {type_}: {message}")
31
+ self.status = status
32
+ self.type = type_
33
+ self.message = message
34
+ self.request_id = request_id
35
+
36
+
37
+ class Veqen:
38
+ def __init__(
39
+ self,
40
+ api_key: str | None = None,
41
+ *,
42
+ base_url: str | None = None,
43
+ timeout: float = 10.0,
44
+ max_retries: int = 2,
45
+ transport: httpx.BaseTransport | None = None,
46
+ ) -> None:
47
+ key = api_key or os.environ.get("VEQEN_API_KEY")
48
+ if not key:
49
+ raise ValueError("Pass api_key or set VEQEN_API_KEY.")
50
+ self.max_retries = max_retries
51
+ self._client = httpx.Client(
52
+ base_url=(base_url or os.environ.get("VEQEN_BASE_URL") or DEFAULT_BASE_URL).rstrip("/"),
53
+ headers={"Authorization": f"Bearer {key}", "User-Agent": f"veqen-python/{__version__}"},
54
+ timeout=timeout,
55
+ transport=transport,
56
+ )
57
+
58
+ # --- checks ----------------------------------------------------------------------------------
59
+
60
+ def email(self, email: str, *, verify_mailbox: bool = False, idempotency_key: str | None = None) -> dict:
61
+ return self._post("/v1/email/check", {"email": email, "verify_mailbox": verify_mailbox}, idempotency_key)
62
+
63
+ def emails(self, emails: list[str], *, verify_mailbox: bool = False) -> list[dict]:
64
+ results: list[dict] = []
65
+ for start in range(0, len(emails), 100):
66
+ chunk = emails[start : start + 100]
67
+ results.extend(self._post("/v1/email/batch", {"emails": chunk, "verify_mailbox": verify_mailbox})["results"])
68
+ return results
69
+
70
+ def ip(self, ip: str, *, idempotency_key: str | None = None) -> dict:
71
+ return self._post("/v1/ip/check", {"ip": ip}, idempotency_key)
72
+
73
+ def phone(self, phone: str, country: str | None = None, *, idempotency_key: str | None = None) -> dict:
74
+ return self._post("/v1/phone/check", {"phone": phone, "country": country}, idempotency_key)
75
+
76
+ def domain(self, domain: str, *, idempotency_key: str | None = None) -> dict:
77
+ return self._post("/v1/domain/check", {"domain": domain}, idempotency_key)
78
+
79
+ def signup(
80
+ self,
81
+ *,
82
+ email: str | None = None,
83
+ ip: str | None = None,
84
+ phone: str | None = None,
85
+ phone_country: str | None = None,
86
+ verify_mailbox: bool = False,
87
+ idempotency_key: str | None = None,
88
+ ) -> dict:
89
+ body = {"email": email, "ip": ip, "phone": phone, "phone_country": phone_country, "verify_mailbox": verify_mailbox}
90
+ return self._post("/v1/signup/check", {k: v for k, v in body.items() if v not in (None, "")}, idempotency_key)
91
+
92
+ # --- account ---------------------------------------------------------------------------------
93
+
94
+ def account(self) -> dict:
95
+ return self._request("GET", "/v1/account")
96
+
97
+ def usage(self, days: int = 30) -> dict:
98
+ return self._request("GET", "/v1/usage", params={"days": days})
99
+
100
+ # --- plumbing --------------------------------------------------------------------------------
101
+
102
+ def _post(self, path: str, body: dict, idempotency_key: str | None = None) -> Any:
103
+ # Reuse one key across transport/5xx retries, but generate a fresh key per logical call.
104
+ headers = {"Idempotency-Key": idempotency_key or str(uuid.uuid4())}
105
+ return self._request("POST", path, json=body, headers=headers)
106
+
107
+ def _request(self, method: str, path: str, **kwargs: Any) -> Any:
108
+ attempt = 0
109
+ while True:
110
+ try:
111
+ response = self._client.request(method, path, **kwargs)
112
+ except httpx.TransportError:
113
+ if attempt >= self.max_retries:
114
+ raise
115
+ attempt += 1
116
+ time.sleep(min(2**attempt * 0.25, 4))
117
+ continue
118
+ if response.status_code in RETRY_STATUSES and attempt < self.max_retries:
119
+ attempt += 1
120
+ retry_after = response.headers.get("Retry-After")
121
+ time.sleep(float(retry_after) if retry_after and retry_after.isdigit() else min(2**attempt * 0.25, 4))
122
+ continue
123
+ if response.status_code >= 400:
124
+ try:
125
+ error = response.json().get("error", {})
126
+ except ValueError:
127
+ error = {}
128
+ raise VeqenError(
129
+ response.status_code,
130
+ error.get("type", "http_error"),
131
+ error.get("message", response.text[:200]),
132
+ response.headers.get("X-Request-Id"),
133
+ )
134
+ return response.json()
135
+
136
+ def close(self) -> None:
137
+ self._client.close()
138
+
139
+ def __enter__(self) -> Veqen:
140
+ return self
141
+
142
+ def __exit__(self, *exc: object) -> None:
143
+ self.close()
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: veqen
3
+ Version: 0.1.0
4
+ Summary: Official Python client for the Veqen signup-check API: disposable emails, datacenter IPs, VoIP numbers and more.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://veqen-app.com
7
+ Project-URL: Documentation, https://veqen-app.com/docs
8
+ Project-URL: Source, https://github.com/Keyveve/veqen-sdks/tree/main/python
9
+ Project-URL: Issues, https://github.com/Keyveve/veqen-sdks/issues
10
+ Keywords: email validation,disposable email,fraud,signup,ip lookup,phone validation,vpn detection
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Topic :: Security
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: httpx>=0.25
18
+ Requires-Dist: certifi>=2026.10.19
19
+ Dynamic: license-file
20
+
21
+ # veqen (Python)
22
+
23
+ Official Python client for [Veqen](https://veqen-app.com): catch disposable emails, typos, datacenter and Tor IPs, and VoIP numbers with one API call.
24
+
25
+ ```bash
26
+ pip install https://veqen-app.com/static/sdk/veqen-0.1.0-py3-none-any.whl
27
+ ```
28
+
29
+ ```python
30
+ from veqen import Veqen, VeqenError
31
+
32
+ client = Veqen() # reads VEQEN_API_KEY from the environment
33
+
34
+ result = client.signup(email="jane@mailinator.com", ip="185.220.101.1")
35
+ print(result["risk"]["verdict"]) # "refuse"
36
+ for reason in result["risk"]["reasons"]:
37
+ print(reason["code"], reason["message"])
38
+ ```
39
+
40
+ | Method | Endpoint | Credits |
41
+ |---|---|---|
42
+ | `email(address, verify_mailbox=False)` | `/v1/email/check` | 1 (+5) |
43
+ | `emails(list)` | `/v1/email/batch` (chunks of 100) | 1 each |
44
+ | `ip(address)` | `/v1/ip/check` | 1 |
45
+ | `phone(number, country=None)` | `/v1/phone/check` | 1 |
46
+ | `domain(name)` | `/v1/domain/check` | 1 |
47
+ | `signup(email=, ip=, phone=, phone_country=)` | `/v1/signup/check` | 3 |
48
+ | `account()`, `usage(days=30)` | `/v1/account`, `/v1/usage` | 0 |
49
+
50
+ Errors raise `VeqenError` with `.status`, `.type` (e.g. `insufficient_credits`) and `.message`. Requests are retried on 429 and 5xx. Docs: https://veqen-app.com/docs
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ tests/test_client.py
5
+ veqen/__init__.py
6
+ veqen.egg-info/PKG-INFO
7
+ veqen.egg-info/SOURCES.txt
8
+ veqen.egg-info/dependency_links.txt
9
+ veqen.egg-info/requires.txt
10
+ veqen.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ httpx>=0.25
2
+ certifi>=2026.10.19
@@ -0,0 +1 @@
1
+ veqen