fraudnova 1.0.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,15 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ indent_style = space
7
+ indent_size = 4
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
10
+
11
+ [*.py]
12
+ indent_size = 4
13
+
14
+ [*.{yml,yaml,json,toml}]
15
+ indent_size = 2
@@ -0,0 +1,54 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ concurrency:
9
+ group: ci-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ jobs:
13
+ test:
14
+ runs-on: ubuntu-24.04
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
19
+ httpx: ['']
20
+ include:
21
+ # The oldest httpx the package allows (dependencies: httpx>=0.26).
22
+ - python-version: '3.9'
23
+ httpx: '0.26.*'
24
+ name: Python ${{ matrix.python-version }}${{ matrix.httpx && format(' (httpx {0})', matrix.httpx) || '' }}
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+
28
+ - uses: actions/setup-python@v5
29
+ with:
30
+ python-version: ${{ matrix.python-version }}
31
+ cache: pip
32
+
33
+ - run: pip install -e ".[dev]"
34
+ if: matrix.httpx == ''
35
+ - run: pip install -e ".[dev]" "httpx==${{ matrix.httpx }}"
36
+ if: matrix.httpx != ''
37
+ - run: ruff check .
38
+ - run: ruff format --check .
39
+ - run: mypy
40
+ - run: pytest -q
41
+
42
+ build:
43
+ runs-on: ubuntu-24.04
44
+ name: Build
45
+ steps:
46
+ - uses: actions/checkout@v4
47
+
48
+ - uses: actions/setup-python@v5
49
+ with:
50
+ python-version: '3.12'
51
+
52
+ - run: pip install build twine
53
+ - run: python -m build
54
+ - run: twine check dist/*
@@ -0,0 +1,53 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ['v*.*.*']
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: '3.12'
16
+
17
+ - run: pip install build
18
+ - run: python -m build
19
+
20
+ - uses: actions/upload-artifact@v4
21
+ with:
22
+ name: dist
23
+ path: dist/
24
+
25
+ publish:
26
+ needs: build
27
+ runs-on: ubuntu-latest
28
+ environment: pypi
29
+ permissions:
30
+ id-token: write
31
+ steps:
32
+ - uses: actions/download-artifact@v4
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+
37
+ - uses: pypa/gh-action-pypi-publish@release/v1
38
+
39
+ github-release:
40
+ needs: publish
41
+ runs-on: ubuntu-latest
42
+ permissions:
43
+ contents: write
44
+ steps:
45
+ - uses: actions/download-artifact@v4
46
+ with:
47
+ name: dist
48
+ path: dist/
49
+
50
+ - uses: softprops/action-gh-release@v2
51
+ with:
52
+ generate_release_notes: true
53
+ files: dist/*
@@ -0,0 +1,14 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ uv.lock
11
+ .superpowers/
12
+ .DS_Store
13
+ .idea/
14
+ .vscode/
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [1.0.0] - 2026-11-06
11
+
12
+ ### Added
13
+
14
+ - Client for every API endpoint: IP, email and phone checks, the three batch checks, balance and my IP.
15
+ - Typed results with the `flagged()` and `flag_reasons()` helpers.
16
+ - Typed exceptions for every API error status.
17
+ - Opt-in retry of HTTP 429 responses, honouring `Retry-After`.
18
+ - Batch helpers that chunk requests by 100 items.
19
+ - Bring-your-own `httpx.Client`.
20
+
21
+ [Unreleased]: https://github.com/dahleedam/fraudnova-python/compare/v1.0.0...HEAD
22
+ [1.0.0]: https://github.com/dahleedam/fraudnova-python/releases/tag/v1.0.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FraudNova
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,293 @@
1
+ Metadata-Version: 2.5
2
+ Name: fraudnova
3
+ Version: 1.0.0
4
+ Summary: Official Python client for the FraudNova fraud-detection API (IP, email and phone checks)
5
+ Project-URL: Homepage, https://fraudnova.com
6
+ Project-URL: Documentation, https://fraudnova.com/docs
7
+ Project-URL: Repository, https://github.com/dahleedam/fraudnova-python
8
+ Project-URL: Changelog, https://github.com/dahleedam/fraudnova-python/blob/main/CHANGELOG.md
9
+ Author-email: FraudNova <support@fraudnova.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api-client,email-validation,fraud,fraud-detection,ip,phone-validation,proxy,vpn
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: httpx<1.0,>=0.26
22
+ Provides-Extra: dev
23
+ Requires-Dist: mypy>=1.8; extra == 'dev'
24
+ Requires-Dist: pytest>=7; extra == 'dev'
25
+ Requires-Dist: ruff>=0.5; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # fraudnova
29
+
30
+ [FraudNova](https://fraudnova.com) is a fraud-detection API. It returns a fraud score and risk flags for any IP address, email address or phone number, so your application can decide whether to accept, review or block a signup, lead or order. This is the official Python client.
31
+
32
+ [![CI](https://github.com/dahleedam/fraudnova-python/actions/workflows/ci.yml/badge.svg)](https://github.com/dahleedam/fraudnova-python/actions/workflows/ci.yml)
33
+ [![PyPI version](https://img.shields.io/pypi/v/fraudnova)](https://pypi.org/project/fraudnova/)
34
+ [![Python versions](https://img.shields.io/pypi/pyversions/fraudnova)](https://pypi.org/project/fraudnova/)
35
+ [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
36
+
37
+ ## Requirements
38
+
39
+ - Python 3.9 or later
40
+ - [httpx](https://www.python-httpx.org/) 0.26 or later, installed automatically
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install fraudnova
46
+ ```
47
+
48
+ You need an API key. [Register](https://fraudnova.com/register) to get free trial credits, then create a key at [fraudnova.com/dashboard/api-keys](https://fraudnova.com/dashboard/api-keys). The key starts with `fn_` and is shown once, so copy it right away.
49
+
50
+ ## Quickstart
51
+
52
+ ```python
53
+ import fraudnova
54
+
55
+ client = fraudnova.Client("fn_your_api_key")
56
+ result = client.check_ip("8.8.8.8")
57
+ if result.flagged():
58
+ ... # review or block
59
+ print(result.fraud_score)
60
+ ```
61
+
62
+ ## Checking IP addresses, emails and phone numbers
63
+
64
+ ```python
65
+ import fraudnova
66
+
67
+ client = fraudnova.Client("fn_your_api_key")
68
+
69
+ ip = client.check_ip("8.8.8.8")
70
+ email = client.check_email("someone@example.com")
71
+ phone = client.check_phone("+1 817 679 1808")
72
+ phone = client.check_phone("8176791808", country="US") # optional two-letter country hint
73
+ ```
74
+
75
+ `check_ip()` takes optional keyword arguments. Leave one out to use the API default.
76
+
77
+ ```python
78
+ result = client.check_ip(
79
+ visitor_ip,
80
+ strictness=1, # 0 = lenient, 3 = strict
81
+ mobile=False, # the request comes from a mobile device
82
+ user_agent=request.headers.get("User-Agent"), # enables the device and browser fields
83
+ user_language=request.headers.get("Accept-Language", "").split(",")[0].strip() or None,
84
+ )
85
+ ```
86
+
87
+ `user_language` takes a short language tag such as `en-US`, not the whole `Accept-Language` header, so the example sends only the first tag.
88
+
89
+ An empty value, or a `strictness` that is not an integer from 0 to 3, raises `fraudnova.InvalidArgumentError` before any request is sent.
90
+
91
+ ## Batch checks
92
+
93
+ `check_ips()`, `check_emails()` and `check_phones()` score a list of values. The list can be any length: the client splits it into requests of 100 (`fraudnova.BATCH_MAX`) and joins the answers in input order.
94
+
95
+ ```python
96
+ batch = client.check_ips(ips)
97
+
98
+ for item in batch.successful():
99
+ print(item.subject, item.result.fraud_score)
100
+
101
+ for item in batch.failed():
102
+ print(item.subject, "failed:", item.error)
103
+
104
+ print("Credits left:", batch.credits_remaining)
105
+ ```
106
+
107
+ `check_ips()` takes the same keyword options as `check_ip()`, and `check_phones()` takes one `country` hint for the whole batch. `BatchResult` is iterable and supports `len()` and indexing; each `BatchItem` has `subject`, `success`, `result` (an `IpResult`, `EmailResult` or `PhoneResult`, or `None` on failure) and `error`. `has_failures()` tells you whether any item failed.
108
+
109
+ Batches are split into requests of 100. When a request reports zero credits remaining, the client stops and marks the remaining items `insufficient_credits` instead of sending a request that would fail. Any other error on a later request (a 429 with no retry policy or with the retries used up, a 503, or a `TransportError`) propagates as an exception, and the results of the earlier requests are discarded even though they were billed. If you need control over each request, slice the list to 100 yourself:
110
+
111
+ ```python
112
+ for start in range(0, len(ips), fraudnova.BATCH_MAX):
113
+ batch = client.check_ips(ips[start : start + fraudnova.BATCH_MAX])
114
+ ... # handle this request's result before sending the next
115
+ ```
116
+
117
+ Items must be strings or `None`; any other type raises `InvalidArgumentError` before the request is sent. One bad value never fails the batch, so an empty string or `None` comes back as a failed item. A failed item carries one of these error codes:
118
+
119
+ | `error` | Helper | Meaning |
120
+ | --- | --- | --- |
121
+ | `invalid_ip`, `invalid_email`, `invalid_phone` | `is_invalid()` | The value is not a valid IP address, email address or phone number. |
122
+ | `insufficient_credits` | `is_insufficient_credits()` | Your balance ran out partway through the batch. |
123
+ | `upstream_failed` | `is_upstream_failed()` | The lookup failed. The item was not charged. |
124
+
125
+ ## Results
126
+
127
+ `check_ip()`, `check_email()` and `check_phone()` return an `IpResult`, `EmailResult` or `PhoneResult`. Each has the API's fields as snake_case attributes, for example `fraud_score`, `recent_abuse` and `country_code`. `fraud_score` is always an integer (0 when the API does not return one); every other field is optional (`None`), because the API does not return every field for every check.
128
+
129
+ ```python
130
+ ip = client.check_ip("8.8.8.8")
131
+ ip.proxy # Optional[bool]
132
+ ip.vpn # Optional[bool]
133
+ ip.country_code # Optional[str], for example "US"
134
+ ip.isp # Optional[str] (API field ISP)
135
+ ip.asn # Optional[int] (API field ASN)
136
+
137
+ email = client.check_email("someone@example.com")
138
+ email.disposable # Optional[bool]
139
+ email.deliverability # Optional[str]
140
+ email.first_seen # Optional[DateInfo], also domain_age
141
+
142
+ phone = client.check_phone("+18176791808")
143
+ phone.voip # Optional[bool] (API field VOIP)
144
+ phone.line_type # Optional[str]
145
+ phone.carrier # Optional[str]
146
+ ```
147
+
148
+ The email dates `first_seen` and `domain_age` are `DateInfo` objects with `human`, `timestamp` and `iso`.
149
+
150
+ `to_dict()` returns the full API response for that check, with the original field names, including any field the client does not map to an attribute:
151
+
152
+ ```python
153
+ fields = client.check_ip("8.8.8.8").to_dict()
154
+ # {"fraud_score": 20, "proxy": False, "ISP": "Google", ...}
155
+ ```
156
+
157
+ `credits_remaining` holds your balance after the check. It is `None` on results inside a batch; read `BatchResult.credits_remaining` there instead.
158
+
159
+ ## The `flagged()` helper
160
+
161
+ `flagged()` returns `True` when the `fraud_score` is at or above a risk threshold (default 75), or when any of a set of fields is `True`. `flag_reasons()` lists which of those fired, for example `["fraud_score", "proxy"]`. Both take the same two optional arguments, `risk_threshold` and `flag_on`.
162
+
163
+ | Check | Fields checked by default |
164
+ | --- | --- |
165
+ | IP address | `proxy`, `vpn`, `tor` |
166
+ | Email address | `disposable` |
167
+ | Phone number | `VOIP`, `risky` |
168
+
169
+ Pass your own threshold and fields with the raw API field names (`VOIP`, not `voip`):
170
+
171
+ ```python
172
+ result = client.check_phone("+18176791808")
173
+
174
+ if result.flagged(risk_threshold=85, flag_on=["VOIP", "risky", "recent_abuse"]):
175
+ reasons = result.flag_reasons(85, ["VOIP", "risky", "recent_abuse"])
176
+ ...
177
+ ```
178
+
179
+ The defaults are available as `IpResult.DEFAULT_FLAG_ON`, `EmailResult.DEFAULT_FLAG_ON` and `PhoneResult.DEFAULT_FLAG_ON`.
180
+
181
+ ## Balance and your IP
182
+
183
+ ```python
184
+ credits = client.balance() # int, costs no credits
185
+ ip = client.my_ip() # str, the public IP your server calls from; no API key needed
186
+ ```
187
+
188
+ ## Errors
189
+
190
+ Every exception the client raises is a `fraudnova.FraudNovaError`, so one `except` handles them all:
191
+
192
+ ```python
193
+ from fraudnova import FraudNovaError, InsufficientCreditsError
194
+
195
+ try:
196
+ result = client.check_ip(visitor_ip)
197
+ except InsufficientCreditsError:
198
+ ... # buy a credit pack in the FraudNova dashboard
199
+ except FraudNovaError as error:
200
+ ... # log it and let the request through, or hold it for review
201
+ ```
202
+
203
+ | Exception | Status | When |
204
+ | --- | --- | --- |
205
+ | `AuthenticationError` | 401 | The API key is missing, invalid or revoked. |
206
+ | `InsufficientCreditsError` | 402 | Your balance is too low for the check. |
207
+ | `AccountSuspendedError` | 403 | The account is suspended. |
208
+ | `ValidationError` | 422 | The API rejected the request parameters. |
209
+ | `RateLimitError` | 429 | Too many requests, or too many checks in flight at once. `retry_after` and `is_concurrency_limit` tell you which. |
210
+ | `UpstreamError` | 502, 503, 504 | The lookup failed. On a 503 the credit is refunded automatically and `credit_refunded` is `True`. |
211
+ | `UnexpectedResponseError` | 2xx | The response could not be read as a FraudNova response. |
212
+ | `ApiError` | any other error status | Base class of every exception with a status. `status_code`, `api_message` and `body` are on all of them. A 401, 402, 403 or 422 whose body is not a FraudNova JSON response (for example a proxy error page) is a plain `ApiError`. |
213
+ | `TransportError` | none | The HTTP client failed, for example a timeout or DNS error. The original exception is `__cause__`. |
214
+ | `InvalidArgumentError` | none | A value was empty or out of range; no request was sent. It is also a `ValueError`. |
215
+
216
+ ## Retries
217
+
218
+ The client does not retry by default. To retry HTTP 429 responses, pass a `RetryPolicy`:
219
+
220
+ ```python
221
+ from fraudnova import Client, RetryPolicy
222
+
223
+ client = Client("fn_your_api_key", retry=RetryPolicy(max_attempts=3))
224
+ ```
225
+
226
+ `max_attempts` counts the first attempt, so `3` means up to two retries. Only 429 is retried; every other error is raised right away. The wait is the `Retry-After` header when the response has one, otherwise a short schedule for the concurrency limit and a longer one for the rate limit. When the attempts run out, the `RateLimitError` is raised. A `Retry-After` longer than `max_retry_after` (60 seconds by default) is not waited for; the `RateLimitError` is raised right away. Batch requests are safe to retry, because a 429 is returned before any item is checked.
227
+
228
+ Retries can make one call slow. With `RetryPolicy(max_attempts=3)` and the default `max_retry_after=60`, one call can sleep for up to about two minutes, plus three timeouts. Inside a web request, use a small `max_retry_after` (for example `2`) and a short `timeout`, and keep the full policy for background jobs.
229
+
230
+ ## Bring your own HTTP client
231
+
232
+ Inject your own `httpx.Client`. Timeouts, proxies and TLS settings belong to the HTTP client, so set them there:
233
+
234
+ ```python
235
+ import httpx
236
+ from fraudnova import Client
237
+
238
+ http = httpx.Client(timeout=5.0, proxy="http://proxy.internal:3128")
239
+ client = Client("fn_your_api_key", http_client=http)
240
+ ```
241
+
242
+ Without an injected client, the SDK creates its own `httpx.Client` with a 30 second timeout (change it with `Client(..., timeout=10.0)`).
243
+
244
+ Use the client as a context manager, or call `close()`, to release connections when you are done. `close()` only closes an `httpx.Client` the SDK created; one you inject stays yours to close.
245
+
246
+ ```python
247
+ with Client("fn_your_api_key") as client:
248
+ client.check_email("someone@example.com")
249
+ ```
250
+
251
+ ## Testing
252
+
253
+ Inject an `httpx.Client` backed by `httpx.MockTransport`, so your tests send no real request:
254
+
255
+ ```python
256
+ import httpx
257
+ from fraudnova import Client
258
+
259
+
260
+ def handler(request: httpx.Request) -> httpx.Response:
261
+ return httpx.Response(
262
+ 200,
263
+ json={
264
+ "success": True,
265
+ "data": {"fraud_score": 12, "proxy": False},
266
+ "credits_remaining": 99,
267
+ },
268
+ )
269
+
270
+
271
+ http = httpx.Client(transport=httpx.MockTransport(handler))
272
+ client = Client("fn_test", http_client=http)
273
+
274
+ assert client.check_ip("8.8.8.8").fraud_score == 12
275
+ ```
276
+
277
+ ## Credits and limits
278
+
279
+ One check costs one credit, whichever type it is. Failed checks are refunded. You get free trial credits when you verify your email address, and you buy more as credit packs, with no subscription. A batch costs one credit per successful item.
280
+
281
+ The API limits requests per minute for each key and checks in flight for each account. The current limits are in the [API documentation](https://fraudnova.com/docs).
282
+
283
+ ## Resources
284
+
285
+ * [FraudNova API documentation](https://fraudnova.com/docs)
286
+ * [Examples repository](https://github.com/dahleedam/fraudnova-examples)
287
+ * [Status](https://fraudnova.com/status)
288
+ * [Changelog](https://fraudnova.com/changelog)
289
+ * Support: [support@fraudnova.com](mailto:support@fraudnova.com)
290
+
291
+ ## Licence
292
+
293
+ Released under the MIT license. See [LICENSE](LICENSE).