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.
- fraudnova-1.0.0/.editorconfig +15 -0
- fraudnova-1.0.0/.github/workflows/ci.yml +54 -0
- fraudnova-1.0.0/.github/workflows/release.yml +53 -0
- fraudnova-1.0.0/.gitignore +14 -0
- fraudnova-1.0.0/CHANGELOG.md +22 -0
- fraudnova-1.0.0/LICENSE +21 -0
- fraudnova-1.0.0/PKG-INFO +293 -0
- fraudnova-1.0.0/README.md +266 -0
- fraudnova-1.0.0/docs/RELEASING.md +63 -0
- fraudnova-1.0.0/docs/superpowers/plans/2026-11-06-python-sdk.md +226 -0
- fraudnova-1.0.0/docs/superpowers/specs/2026-11-06-python-sdk-design.md +282 -0
- fraudnova-1.0.0/pyproject.toml +56 -0
- fraudnova-1.0.0/src/fraudnova/__init__.py +44 -0
- fraudnova-1.0.0/src/fraudnova/_transport.py +112 -0
- fraudnova-1.0.0/src/fraudnova/_version.py +1 -0
- fraudnova-1.0.0/src/fraudnova/client.py +203 -0
- fraudnova-1.0.0/src/fraudnova/exceptions.py +158 -0
- fraudnova-1.0.0/src/fraudnova/py.typed +0 -0
- fraudnova-1.0.0/src/fraudnova/results.py +342 -0
- fraudnova-1.0.0/src/fraudnova/retry.py +65 -0
- fraudnova-1.0.0/tests/__init__.py +0 -0
- fraudnova-1.0.0/tests/conftest.py +45 -0
- fraudnova-1.0.0/tests/test_client.py +255 -0
- fraudnova-1.0.0/tests/test_exceptions.py +161 -0
- fraudnova-1.0.0/tests/test_leak_guard.py +32 -0
- fraudnova-1.0.0/tests/test_readme.py +31 -0
- fraudnova-1.0.0/tests/test_release_consistency.py +11 -0
- fraudnova-1.0.0/tests/test_results.py +319 -0
- fraudnova-1.0.0/tests/test_retry.py +80 -0
- fraudnova-1.0.0/tests/test_transport.py +187 -0
|
@@ -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,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
|
fraudnova-1.0.0/LICENSE
ADDED
|
@@ -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.
|
fraudnova-1.0.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://github.com/dahleedam/fraudnova-python/actions/workflows/ci.yml)
|
|
33
|
+
[](https://pypi.org/project/fraudnova/)
|
|
34
|
+
[](https://pypi.org/project/fraudnova/)
|
|
35
|
+
[](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).
|