layercall 1.2.4__tar.gz → 1.2.6__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.
- {layercall-1.2.4 → layercall-1.2.6}/PKG-INFO +60 -5
- {layercall-1.2.4 → layercall-1.2.6}/README.md +59 -4
- {layercall-1.2.4 → layercall-1.2.6}/layercall/__init__.py +124 -9
- {layercall-1.2.4 → layercall-1.2.6}/layercall.egg-info/PKG-INFO +60 -5
- {layercall-1.2.4 → layercall-1.2.6}/pyproject.toml +1 -1
- {layercall-1.2.4 → layercall-1.2.6}/layercall.egg-info/SOURCES.txt +0 -0
- {layercall-1.2.4 → layercall-1.2.6}/layercall.egg-info/dependency_links.txt +0 -0
- {layercall-1.2.4 → layercall-1.2.6}/layercall.egg-info/top_level.txt +0 -0
- {layercall-1.2.4 → layercall-1.2.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: layercall
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.6
|
|
4
4
|
Summary: Official LayerCall client — score an IP, email, phone, domain or a whole signup for fraud in one call. Zero dependencies.
|
|
5
5
|
Author: LayerCall
|
|
6
6
|
License: MIT
|
|
@@ -72,13 +72,68 @@ lc.verify_email("someone@mailinator.com")
|
|
|
72
72
|
lc.lookup_phone("+14155552671")
|
|
73
73
|
lc.lookup_phone("4155552671", country="US")
|
|
74
74
|
lc.score_domain("example.com")
|
|
75
|
+
lc.score_device(device_id, ip=ip, signals=signals) # fingerprint from /fp.js
|
|
75
76
|
lc.score_user(ip=ip, email=email, phone=phone, device_id=device_id)
|
|
76
|
-
lc.batch("email", ["a@x.com", "b@y.com"])
|
|
77
|
+
lc.batch("email", ["a@x.com", "b@y.com"]) # up to 500
|
|
78
|
+
|
|
79
|
+
# AI agents — proof of identity, then your policy applied to it
|
|
80
|
+
lc.verify_agent(url=url, headers=headers) # who is this?
|
|
81
|
+
lc.authorize_agent(method=method, url=url, headers=headers) # may they, here?
|
|
82
|
+
lc.get_agent_policy()
|
|
83
|
+
lc.set_agent_policy([{"trigger": "crawler", "path": "/api", "action": "deny"}])
|
|
84
|
+
|
|
85
|
+
# Tell us whether a score was right. Free, and the only thing that improves it.
|
|
86
|
+
lc.report_outcome(request_id=r["request_id"], outcome="fraud")
|
|
87
|
+
|
|
88
|
+
# Report confirmed fraud to the shared reputation network. Live keys only.
|
|
89
|
+
lc.report("ip", "185.220.101.1", reason="carding")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Custom rules - your lists always win
|
|
93
|
+
|
|
94
|
+
A rule overrides the computed score for that value. The kind (`ip`, `cidr`,
|
|
95
|
+
`email`, `domain`, `phone`, `asn`) is detected from the value unless you force
|
|
96
|
+
it.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
lc.add_rules("block", ["185.220.101.1", "mailinator.com"])
|
|
100
|
+
lc.add_rules("allow", "10.0.0.0/8")
|
|
101
|
+
lc.add_rules("block", ["AS14061"], kind="asn")
|
|
102
|
+
|
|
103
|
+
listed = lc.list_rules()
|
|
104
|
+
lc.delete_rule(listed["rules"][0]["id"])
|
|
105
|
+
|
|
106
|
+
# Paste a list - one value per line, or CSV.
|
|
107
|
+
lc.import_rules("block", "1.2.3.4\n5.6.7.8\nspam.example")
|
|
108
|
+
|
|
109
|
+
# Removes EVERY rule on the account. confirm is required, deliberately.
|
|
110
|
+
lc.clear_rules(confirm=True)
|
|
77
111
|
```
|
|
78
112
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
113
|
+
### A pending mailbox
|
|
114
|
+
|
|
115
|
+
`verify_email` returns `mailbox_status` `"pending"` when the SMTP probe has not
|
|
116
|
+
finished - it is queued and the answer is there on the next lookup. If you
|
|
117
|
+
would rather wait for it, say so. It costs 2-10 seconds on a cache miss, which
|
|
118
|
+
is why it is opt-in:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
r = lc.verify_email("someone@example.com", wait_for_mailbox=True)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
That is all thirteen. This block used to list seven, with no hint there were
|
|
125
|
+
more — so a Python developer reasonably concluded that device scoring, agent
|
|
126
|
+
authorization and outcome feedback were Node-only features. They were not;
|
|
127
|
+
they were shipped, working and undocumented.
|
|
128
|
+
|
|
129
|
+
Every **scoring** method takes `strictness` (`0` lenient → `3` paranoid) —
|
|
130
|
+
`score_ip`, `verify_email`, `lookup_phone`, `score_domain`, `score_device`,
|
|
131
|
+
`score_user` and `batch`. It moves the verdict thresholds only; the risk score
|
|
132
|
+
never changes, so you can re-tune without re-scoring anything.
|
|
133
|
+
|
|
134
|
+
The other methods report or configure rather than score, and take no
|
|
135
|
+
strictness: `report`, `report_outcome`, `verify_agent`, `authorize_agent`,
|
|
136
|
+
`get_agent_policy`, `set_agent_policy`.
|
|
82
137
|
|
|
83
138
|
## None means unknown, never "no"
|
|
84
139
|
|
|
@@ -53,13 +53,68 @@ lc.verify_email("someone@mailinator.com")
|
|
|
53
53
|
lc.lookup_phone("+14155552671")
|
|
54
54
|
lc.lookup_phone("4155552671", country="US")
|
|
55
55
|
lc.score_domain("example.com")
|
|
56
|
+
lc.score_device(device_id, ip=ip, signals=signals) # fingerprint from /fp.js
|
|
56
57
|
lc.score_user(ip=ip, email=email, phone=phone, device_id=device_id)
|
|
57
|
-
lc.batch("email", ["a@x.com", "b@y.com"])
|
|
58
|
+
lc.batch("email", ["a@x.com", "b@y.com"]) # up to 500
|
|
59
|
+
|
|
60
|
+
# AI agents — proof of identity, then your policy applied to it
|
|
61
|
+
lc.verify_agent(url=url, headers=headers) # who is this?
|
|
62
|
+
lc.authorize_agent(method=method, url=url, headers=headers) # may they, here?
|
|
63
|
+
lc.get_agent_policy()
|
|
64
|
+
lc.set_agent_policy([{"trigger": "crawler", "path": "/api", "action": "deny"}])
|
|
65
|
+
|
|
66
|
+
# Tell us whether a score was right. Free, and the only thing that improves it.
|
|
67
|
+
lc.report_outcome(request_id=r["request_id"], outcome="fraud")
|
|
68
|
+
|
|
69
|
+
# Report confirmed fraud to the shared reputation network. Live keys only.
|
|
70
|
+
lc.report("ip", "185.220.101.1", reason="carding")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Custom rules - your lists always win
|
|
74
|
+
|
|
75
|
+
A rule overrides the computed score for that value. The kind (`ip`, `cidr`,
|
|
76
|
+
`email`, `domain`, `phone`, `asn`) is detected from the value unless you force
|
|
77
|
+
it.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
lc.add_rules("block", ["185.220.101.1", "mailinator.com"])
|
|
81
|
+
lc.add_rules("allow", "10.0.0.0/8")
|
|
82
|
+
lc.add_rules("block", ["AS14061"], kind="asn")
|
|
83
|
+
|
|
84
|
+
listed = lc.list_rules()
|
|
85
|
+
lc.delete_rule(listed["rules"][0]["id"])
|
|
86
|
+
|
|
87
|
+
# Paste a list - one value per line, or CSV.
|
|
88
|
+
lc.import_rules("block", "1.2.3.4\n5.6.7.8\nspam.example")
|
|
89
|
+
|
|
90
|
+
# Removes EVERY rule on the account. confirm is required, deliberately.
|
|
91
|
+
lc.clear_rules(confirm=True)
|
|
58
92
|
```
|
|
59
93
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
94
|
+
### A pending mailbox
|
|
95
|
+
|
|
96
|
+
`verify_email` returns `mailbox_status` `"pending"` when the SMTP probe has not
|
|
97
|
+
finished - it is queued and the answer is there on the next lookup. If you
|
|
98
|
+
would rather wait for it, say so. It costs 2-10 seconds on a cache miss, which
|
|
99
|
+
is why it is opt-in:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
r = lc.verify_email("someone@example.com", wait_for_mailbox=True)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
That is all thirteen. This block used to list seven, with no hint there were
|
|
106
|
+
more — so a Python developer reasonably concluded that device scoring, agent
|
|
107
|
+
authorization and outcome feedback were Node-only features. They were not;
|
|
108
|
+
they were shipped, working and undocumented.
|
|
109
|
+
|
|
110
|
+
Every **scoring** method takes `strictness` (`0` lenient → `3` paranoid) —
|
|
111
|
+
`score_ip`, `verify_email`, `lookup_phone`, `score_domain`, `score_device`,
|
|
112
|
+
`score_user` and `batch`. It moves the verdict thresholds only; the risk score
|
|
113
|
+
never changes, so you can re-tune without re-scoring anything.
|
|
114
|
+
|
|
115
|
+
The other methods report or configure rather than score, and take no
|
|
116
|
+
strictness: `report`, `report_outcome`, `verify_agent`, `authorize_agent`,
|
|
117
|
+
`get_agent_policy`, `set_agent_policy`.
|
|
63
118
|
|
|
64
119
|
## None means unknown, never "no"
|
|
65
120
|
|
|
@@ -32,7 +32,7 @@ import urllib.parse
|
|
|
32
32
|
import urllib.request
|
|
33
33
|
from typing import Any, Literal, Mapping, Sequence
|
|
34
34
|
|
|
35
|
-
__version__ = "1.2.
|
|
35
|
+
__version__ = "1.2.6"
|
|
36
36
|
__all__ = ["LayerCall", "LayerCallError"]
|
|
37
37
|
|
|
38
38
|
Verdict = Literal["allow", "review", "block"]
|
|
@@ -87,7 +87,7 @@ class LayerCall:
|
|
|
87
87
|
api_key: str,
|
|
88
88
|
*,
|
|
89
89
|
base_url: str = _DEFAULT_BASE,
|
|
90
|
-
timeout: float =
|
|
90
|
+
timeout: float = 30.0,
|
|
91
91
|
retries: int = 2,
|
|
92
92
|
) -> None:
|
|
93
93
|
if not api_key:
|
|
@@ -95,6 +95,9 @@ class LayerCall:
|
|
|
95
95
|
self._key = api_key
|
|
96
96
|
self._base = base_url.rstrip("/")
|
|
97
97
|
self._timeout = timeout
|
|
98
|
+
# /v1/batch alone budgets 300s server-side, so the normal default would
|
|
99
|
+
# abandon a large batch mid-flight and bill every item in it.
|
|
100
|
+
self._batch_timeout = max(timeout, 300.0)
|
|
98
101
|
self._retries = retries
|
|
99
102
|
|
|
100
103
|
# -- transport --------------------------------------------------------
|
|
@@ -143,7 +146,10 @@ class LayerCall:
|
|
|
143
146
|
},
|
|
144
147
|
)
|
|
145
148
|
try:
|
|
146
|
-
|
|
149
|
+
timeout = (
|
|
150
|
+
self._batch_timeout if path == "/v1/batch" else self._timeout
|
|
151
|
+
)
|
|
152
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
147
153
|
return json.loads(resp.read().decode() or "{}")
|
|
148
154
|
except urllib.error.HTTPError as exc:
|
|
149
155
|
try:
|
|
@@ -162,6 +168,22 @@ class LayerCall:
|
|
|
162
168
|
raise err from None
|
|
163
169
|
last_exc = err
|
|
164
170
|
except Exception as exc: # timeouts, DNS, connection resets
|
|
171
|
+
# A TIMEOUT IS NOT EVIDENCE THAT THE SERVER FAILED.
|
|
172
|
+
#
|
|
173
|
+
# It only says we stopped waiting. The request very likely
|
|
174
|
+
# arrived and is still being processed - and it will finish,
|
|
175
|
+
# meter a billable lookup, and return to nobody. Retrying does
|
|
176
|
+
# not recover that lookup, it buys a second one. With the old
|
|
177
|
+
# 5s default against a 25-second endpoint budget, one logical
|
|
178
|
+
# call became three billable ones and the caller still got an
|
|
179
|
+
# error.
|
|
180
|
+
#
|
|
181
|
+
# Refused, reset or DNS means the request never reached us, so
|
|
182
|
+
# a retry is free and worth making. Those still retry.
|
|
183
|
+
if isinstance(exc, TimeoutError) or isinstance(
|
|
184
|
+
getattr(exc, "reason", None), TimeoutError
|
|
185
|
+
):
|
|
186
|
+
raise
|
|
165
187
|
last_exc = exc
|
|
166
188
|
if attempt == self._retries:
|
|
167
189
|
break
|
|
@@ -176,9 +198,30 @@ class LayerCall:
|
|
|
176
198
|
"""VPN, proxy, Tor, datacenter, geolocation and ASN for an IP."""
|
|
177
199
|
return self._request("/v1/score/ip", query={"ip": ip, "strictness": strictness})
|
|
178
200
|
|
|
179
|
-
def verify_email(
|
|
180
|
-
|
|
181
|
-
|
|
201
|
+
def verify_email(
|
|
202
|
+
self,
|
|
203
|
+
email: str,
|
|
204
|
+
*,
|
|
205
|
+
strictness: Strictness | None = None,
|
|
206
|
+
wait_for_mailbox: bool = False,
|
|
207
|
+
) -> dict[str, Any]:
|
|
208
|
+
"""Syntax, MX, disposable, role account, homograph and domain age.
|
|
209
|
+
|
|
210
|
+
wait_for_mailbox blocks until the SMTP mailbox probe finishes rather
|
|
211
|
+
than returning mailbox_status "pending". It is the documented remedy
|
|
212
|
+
for a pending mailbox, and it was reachable only by hand-writing the
|
|
213
|
+
HTTP call - the docs told you to pass it and the recommended client had
|
|
214
|
+
no way to. Costs 2-10 seconds on a cache miss, which is why it is
|
|
215
|
+
opt-in.
|
|
216
|
+
"""
|
|
217
|
+
return self._request(
|
|
218
|
+
"/v1/verify/email",
|
|
219
|
+
query={
|
|
220
|
+
"email": email,
|
|
221
|
+
"strictness": strictness,
|
|
222
|
+
"wait_for_mailbox": "true" if wait_for_mailbox else None,
|
|
223
|
+
},
|
|
224
|
+
)
|
|
182
225
|
|
|
183
226
|
def lookup_phone(
|
|
184
227
|
self,
|
|
@@ -193,9 +236,12 @@ class LayerCall:
|
|
|
193
236
|
query={"phone": phone, "country": country, "strictness": strictness},
|
|
194
237
|
)
|
|
195
238
|
|
|
196
|
-
def score_domain(self, domain: str) -> dict[str, Any]:
|
|
239
|
+
def score_domain(self, domain: str, *, strictness: Strictness | None = None) -> dict[str, Any]:
|
|
197
240
|
"""RDAP registration date, registrar, MX/SPF/DMARC, risky TLD."""
|
|
198
|
-
return self._request(
|
|
241
|
+
return self._request(
|
|
242
|
+
"/v1/score/domain",
|
|
243
|
+
query={"domain": domain, "strictness": strictness},
|
|
244
|
+
)
|
|
199
245
|
|
|
200
246
|
def score_device(
|
|
201
247
|
self,
|
|
@@ -204,11 +250,15 @@ class LayerCall:
|
|
|
204
250
|
ip: str | None = None,
|
|
205
251
|
signals: dict[str, Any] | None = None,
|
|
206
252
|
automation: dict[str, Any] | None = None,
|
|
253
|
+
strictness: Strictness | None = None,
|
|
207
254
|
) -> dict[str, Any]:
|
|
208
255
|
"""Score a device fingerprint from /fp.js.
|
|
209
256
|
|
|
210
257
|
POST, never GET: a device id in a URL lands in access logs and Referer
|
|
211
258
|
headers, and that is a tracking identifier.
|
|
259
|
+
|
|
260
|
+
``strictness`` rides in the query string, not the body — the route
|
|
261
|
+
reads it from the URL for every endpoint, POST ones included.
|
|
212
262
|
"""
|
|
213
263
|
body: dict[str, Any] = {"device_id": device_id}
|
|
214
264
|
if ip is not None:
|
|
@@ -217,7 +267,12 @@ class LayerCall:
|
|
|
217
267
|
body["signals"] = signals
|
|
218
268
|
if automation is not None:
|
|
219
269
|
body["automation"] = automation
|
|
220
|
-
return self._request(
|
|
270
|
+
return self._request(
|
|
271
|
+
"/v1/score/device",
|
|
272
|
+
method="POST",
|
|
273
|
+
body=body,
|
|
274
|
+
query={"strictness": strictness},
|
|
275
|
+
)
|
|
221
276
|
|
|
222
277
|
def verify_agent(
|
|
223
278
|
self,
|
|
@@ -372,6 +427,66 @@ class LayerCall:
|
|
|
372
427
|
raise ValueError("score_user: provide at least one of ip, email or phone.")
|
|
373
428
|
return self._request("/v1/score/user", body=payload)
|
|
374
429
|
|
|
430
|
+
# -- custom rules -----------------------------------------------------
|
|
431
|
+
#
|
|
432
|
+
# One of the nine products on the homepage, three endpoints, and until now
|
|
433
|
+
# no method in either SDK for any of them. A customer following our own
|
|
434
|
+
# advice to use the SDK found a promoted feature reachable only by
|
|
435
|
+
# hand-writing HTTP, and would reasonably conclude it was unfinished.
|
|
436
|
+
#
|
|
437
|
+
# A rule's kind (ip, cidr, email, domain, phone, asn) is detected from the
|
|
438
|
+
# value unless you force it.
|
|
439
|
+
|
|
440
|
+
def list_rules(self) -> dict[str, Any]:
|
|
441
|
+
"""Every rule on the account."""
|
|
442
|
+
return self._request("/v1/rules")
|
|
443
|
+
|
|
444
|
+
def add_rules(
|
|
445
|
+
self,
|
|
446
|
+
action: str,
|
|
447
|
+
values: str | list[str],
|
|
448
|
+
*,
|
|
449
|
+
kind: str | None = None,
|
|
450
|
+
) -> dict[str, Any]:
|
|
451
|
+
"""Add one rule or many. Re-adding an existing rule is idempotent."""
|
|
452
|
+
body: dict[str, Any] = {
|
|
453
|
+
"action": action,
|
|
454
|
+
"values": [values] if isinstance(values, str) else list(values),
|
|
455
|
+
}
|
|
456
|
+
if kind is not None:
|
|
457
|
+
body["kind"] = kind
|
|
458
|
+
return self._request("/v1/rules", body=body)
|
|
459
|
+
|
|
460
|
+
def delete_rule(self, rule_id: str) -> dict[str, Any]:
|
|
461
|
+
"""Remove one rule by id."""
|
|
462
|
+
return self._request(f"/v1/rules/{urllib.parse.quote(rule_id, safe='')}", method="DELETE")
|
|
463
|
+
|
|
464
|
+
def clear_rules(self, *, confirm: bool = False) -> dict[str, Any]:
|
|
465
|
+
"""Remove EVERY rule on the account.
|
|
466
|
+
|
|
467
|
+
confirm=True is required by the API and deliberately not defaulted
|
|
468
|
+
here - the whole point of the flag is that it cannot happen by
|
|
469
|
+
accident, and an SDK that fills it in for you removes the guard.
|
|
470
|
+
"""
|
|
471
|
+
if not confirm:
|
|
472
|
+
raise ValueError(
|
|
473
|
+
"clear_rules deletes every rule on the account - pass confirm=True."
|
|
474
|
+
)
|
|
475
|
+
return self._request("/v1/rules", method="DELETE", query={"confirm": "true"})
|
|
476
|
+
|
|
477
|
+
def import_rules(
|
|
478
|
+
self,
|
|
479
|
+
action: str,
|
|
480
|
+
text: str,
|
|
481
|
+
*,
|
|
482
|
+
kind: str | None = None,
|
|
483
|
+
) -> dict[str, Any]:
|
|
484
|
+
"""Import rules from pasted text - one value per line, or CSV."""
|
|
485
|
+
body: dict[str, Any] = {"action": action, "text": text}
|
|
486
|
+
if kind is not None:
|
|
487
|
+
body["kind"] = kind
|
|
488
|
+
return self._request("/v1/rules/import", body=body)
|
|
489
|
+
|
|
375
490
|
def batch(
|
|
376
491
|
self,
|
|
377
492
|
type: Literal["ip", "email", "phone", "domain"],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: layercall
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.6
|
|
4
4
|
Summary: Official LayerCall client — score an IP, email, phone, domain or a whole signup for fraud in one call. Zero dependencies.
|
|
5
5
|
Author: LayerCall
|
|
6
6
|
License: MIT
|
|
@@ -72,13 +72,68 @@ lc.verify_email("someone@mailinator.com")
|
|
|
72
72
|
lc.lookup_phone("+14155552671")
|
|
73
73
|
lc.lookup_phone("4155552671", country="US")
|
|
74
74
|
lc.score_domain("example.com")
|
|
75
|
+
lc.score_device(device_id, ip=ip, signals=signals) # fingerprint from /fp.js
|
|
75
76
|
lc.score_user(ip=ip, email=email, phone=phone, device_id=device_id)
|
|
76
|
-
lc.batch("email", ["a@x.com", "b@y.com"])
|
|
77
|
+
lc.batch("email", ["a@x.com", "b@y.com"]) # up to 500
|
|
78
|
+
|
|
79
|
+
# AI agents — proof of identity, then your policy applied to it
|
|
80
|
+
lc.verify_agent(url=url, headers=headers) # who is this?
|
|
81
|
+
lc.authorize_agent(method=method, url=url, headers=headers) # may they, here?
|
|
82
|
+
lc.get_agent_policy()
|
|
83
|
+
lc.set_agent_policy([{"trigger": "crawler", "path": "/api", "action": "deny"}])
|
|
84
|
+
|
|
85
|
+
# Tell us whether a score was right. Free, and the only thing that improves it.
|
|
86
|
+
lc.report_outcome(request_id=r["request_id"], outcome="fraud")
|
|
87
|
+
|
|
88
|
+
# Report confirmed fraud to the shared reputation network. Live keys only.
|
|
89
|
+
lc.report("ip", "185.220.101.1", reason="carding")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Custom rules - your lists always win
|
|
93
|
+
|
|
94
|
+
A rule overrides the computed score for that value. The kind (`ip`, `cidr`,
|
|
95
|
+
`email`, `domain`, `phone`, `asn`) is detected from the value unless you force
|
|
96
|
+
it.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
lc.add_rules("block", ["185.220.101.1", "mailinator.com"])
|
|
100
|
+
lc.add_rules("allow", "10.0.0.0/8")
|
|
101
|
+
lc.add_rules("block", ["AS14061"], kind="asn")
|
|
102
|
+
|
|
103
|
+
listed = lc.list_rules()
|
|
104
|
+
lc.delete_rule(listed["rules"][0]["id"])
|
|
105
|
+
|
|
106
|
+
# Paste a list - one value per line, or CSV.
|
|
107
|
+
lc.import_rules("block", "1.2.3.4\n5.6.7.8\nspam.example")
|
|
108
|
+
|
|
109
|
+
# Removes EVERY rule on the account. confirm is required, deliberately.
|
|
110
|
+
lc.clear_rules(confirm=True)
|
|
77
111
|
```
|
|
78
112
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
113
|
+
### A pending mailbox
|
|
114
|
+
|
|
115
|
+
`verify_email` returns `mailbox_status` `"pending"` when the SMTP probe has not
|
|
116
|
+
finished - it is queued and the answer is there on the next lookup. If you
|
|
117
|
+
would rather wait for it, say so. It costs 2-10 seconds on a cache miss, which
|
|
118
|
+
is why it is opt-in:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
r = lc.verify_email("someone@example.com", wait_for_mailbox=True)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
That is all thirteen. This block used to list seven, with no hint there were
|
|
125
|
+
more — so a Python developer reasonably concluded that device scoring, agent
|
|
126
|
+
authorization and outcome feedback were Node-only features. They were not;
|
|
127
|
+
they were shipped, working and undocumented.
|
|
128
|
+
|
|
129
|
+
Every **scoring** method takes `strictness` (`0` lenient → `3` paranoid) —
|
|
130
|
+
`score_ip`, `verify_email`, `lookup_phone`, `score_domain`, `score_device`,
|
|
131
|
+
`score_user` and `batch`. It moves the verdict thresholds only; the risk score
|
|
132
|
+
never changes, so you can re-tune without re-scoring anything.
|
|
133
|
+
|
|
134
|
+
The other methods report or configure rather than score, and take no
|
|
135
|
+
strictness: `report`, `report_outcome`, `verify_agent`, `authorize_agent`,
|
|
136
|
+
`get_agent_policy`, `set_agent_policy`.
|
|
82
137
|
|
|
83
138
|
## None means unknown, never "no"
|
|
84
139
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "layercall"
|
|
7
|
-
version = "1.2.
|
|
7
|
+
version = "1.2.6"
|
|
8
8
|
description = "Official LayerCall client — score an IP, email, phone, domain or a whole signup for fraud in one call. Zero dependencies."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|