precleared 0.1.0__tar.gz → 0.2.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.
- {precleared-0.1.0 → precleared-0.2.0}/PKG-INFO +6 -6
- {precleared-0.1.0 → precleared-0.2.0}/README.md +5 -5
- {precleared-0.1.0 → precleared-0.2.0}/pyproject.toml +1 -1
- {precleared-0.1.0 → precleared-0.2.0}/src/precleared/_schemas.py +2 -0
- {precleared-0.1.0 → precleared-0.2.0}/src/precleared/_tools.py +16 -4
- precleared-0.2.0/src/precleared/_version.py +1 -0
- {precleared-0.1.0 → precleared-0.2.0}/src/precleared/client.py +49 -1
- {precleared-0.1.0 → precleared-0.2.0}/src/precleared/errors.py +1 -1
- {precleared-0.1.0 → precleared-0.2.0}/src/precleared/models.py +8 -0
- {precleared-0.1.0 → precleared-0.2.0}/src/precleared/openai_agents.py +6 -0
- {precleared-0.1.0 → precleared-0.2.0}/tests/test_async_client.py +1 -1
- {precleared-0.1.0 → precleared-0.2.0}/tests/test_client.py +33 -2
- {precleared-0.1.0 → precleared-0.2.0}/tests/test_langchain.py +1 -0
- {precleared-0.1.0 → precleared-0.2.0}/tests/test_openai_agents.py +2 -0
- {precleared-0.1.0 → precleared-0.2.0}/tests/test_tools_common.py +1 -1
- precleared-0.1.0/src/precleared/_version.py +0 -1
- {precleared-0.1.0 → precleared-0.2.0}/.gitignore +0 -0
- {precleared-0.1.0 → precleared-0.2.0}/LICENSE +0 -0
- {precleared-0.1.0 → precleared-0.2.0}/PUBLISHING.md +0 -0
- {precleared-0.1.0 → precleared-0.2.0}/src/precleared/__init__.py +0 -0
- {precleared-0.1.0 → precleared-0.2.0}/src/precleared/crewai.py +0 -0
- {precleared-0.1.0 → precleared-0.2.0}/src/precleared/langchain.py +0 -0
- {precleared-0.1.0 → precleared-0.2.0}/src/precleared/llamaindex.py +0 -0
- {precleared-0.1.0 → precleared-0.2.0}/src/precleared/py.typed +0 -0
- {precleared-0.1.0 → precleared-0.2.0}/tests/conftest.py +0 -0
- {precleared-0.1.0 → precleared-0.2.0}/tests/test_crewai.py +0 -0
- {precleared-0.1.0 → precleared-0.2.0}/tests/test_import_guards.py +0 -0
- {precleared-0.1.0 → precleared-0.2.0}/tests/test_live_samples.py +0 -0
- {precleared-0.1.0 → precleared-0.2.0}/tests/test_llamaindex.py +0 -0
- {precleared-0.1.0 → precleared-0.2.0}/tests/test_models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: precleared
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Cold-email legality check for AI agents: a Python client for the Precleared API, with LangChain, CrewAI, LlamaIndex and OpenAI Agents tools.
|
|
5
5
|
Project-URL: Homepage, https://precleared.dev
|
|
6
6
|
Project-URL: Documentation, https://precleared.dev/docs
|
|
@@ -45,7 +45,7 @@ Description-Content-Type: text/markdown
|
|
|
45
45
|
|
|
46
46
|
Python client for [Precleared](https://precleared.dev), a cold-email legality check for AI agents.
|
|
47
47
|
|
|
48
|
-
One call per recipient, before the first cold email, answers: is it legal to send this person a cold or marketing email? Precleared looks the recipient up live (for the UK: Companies House, the domain and the company website) and applies the local rules (UK PECR and GDPR, US CAN-SPAM, Canadian CASL, German UWG, French CNIL guidance and the rules for Australia, Ireland and the
|
|
48
|
+
One call per recipient, before the first cold email, answers: is it legal to send this person a cold or marketing email? Precleared looks the recipient up live (for the UK: Companies House, the domain and the company website) and applies the local rules (UK PECR and GDPR, US CAN-SPAM, Canadian CASL, German UWG, French CNIL guidance and the rules for Australia, Ireland, the Netherlands, Spain, Italy, Latvia and the United Arab Emirates). It returns a verdict, the rule that decided it with its source, what the footer must include, the next step and a signed receipt.
|
|
49
49
|
|
|
50
50
|
Decision support with sources, not legal advice.
|
|
51
51
|
|
|
@@ -75,7 +75,7 @@ else:
|
|
|
75
75
|
print(result.verdict, result.next_step, result.receipt_url)
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
`check()` takes the same fields as the API: `recipient_email` or `recipient_domain`, `pitch`, `address_source` (`published`, `bought`, `scraped`, `given` or `unknown`), `sender_country` (default `GB`), `existing_customer`, `pitch_relevant`, `company_number`, `recipient_country`, `default_recipient_country` and `
|
|
78
|
+
`check()` takes the same fields as the API: `recipient_email` or `recipient_domain`, `pitch`, `address_source` (`published`, `bought`, `scraped`, `given` or `unknown`), `sender_country` (`GB`, `US`, `AE` or `CA`, default `GB`), `existing_customer`, `pitch_relevant`, `company_number`, `company_name`, `recipient_name`, `recipient_country`, `default_recipient_country`, `suppressed` and `email`. Pass the draft as `email={"subject": ..., "body": ..., "from_name": ..., "from_email": ...}` and the result's `email_check` says whether its footer has an opt-out, names the sender and, where the rule needs one, carries a postal address; the draft is not stored.
|
|
79
79
|
|
|
80
80
|
`address_source` defaults to `unknown` and is always sent (the API treats a missing value as `unknown` too). It changes verdicts, so never pass `published` unless the recipient put the address on their website or a public listing for people to contact them.
|
|
81
81
|
|
|
@@ -121,7 +121,7 @@ async with AsyncPrecleared("pc_live_...") as client:
|
|
|
121
121
|
|
|
122
122
|
## Framework tools
|
|
123
123
|
|
|
124
|
-
Each adapter exposes the same two tools, `precleared_check` and `precleared_verify_receipt`, with descriptions that tell the model when to call them: before the first cold email to anyone who is not already a customer, in the UK, US, Canada, Germany, France, Australia, Ireland or the
|
|
124
|
+
Each adapter exposes the same two tools, `precleared_check` and `precleared_verify_receipt`, with descriptions that tell the model when to call them: before the first cold email to anyone who is not already a customer, in the UK, US, Canada, Germany, France, Australia, Ireland, the Netherlands, Spain, Italy, Latvia or the UAE, and not for transactional email. The tools return compact text (verdict, rule, footer_text, next_step, receipt URL) rather than the whole JSON. Pass `to_dict=True` for a dictionary instead. Every factory takes an optional `api_key`.
|
|
125
125
|
|
|
126
126
|
Example tool output:
|
|
127
127
|
|
|
@@ -206,9 +206,9 @@ The API key is never logged, printed or included in an exception.
|
|
|
206
206
|
## Allowances and limits
|
|
207
207
|
|
|
208
208
|
- Without a key: 10 checks a day over the REST API, shared by everyone on the same network.
|
|
209
|
-
- With a free key (`https://precleared.dev/get-key
|
|
209
|
+
- With a free key (`https://precleared.dev/get-key`, no card): 100 checks a day for the first 14 days, then 25 a day over the REST API and 100 over MCP.
|
|
210
210
|
- 60 checks a minute per key. Above that the API returns 429 `rate_limited` with `retry_after`.
|
|
211
|
-
- Days run on UK time.
|
|
211
|
+
- Days run on UK time. For more, £15 a month keeps 100 checks a day, or buy a pack: £9 for 300 checks (a key's first purchase) or £39 for 2,000. Pack checks are used after the day's free checks, never expire and stack on the same key. When no credits are left the API returns 402 with `buy_url`.
|
|
212
212
|
- A check can take up to 25 seconds when a register or website is slow; the client's default timeout is 30 seconds.
|
|
213
213
|
|
|
214
214
|
## Configuration
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Python client for [Precleared](https://precleared.dev), a cold-email legality check for AI agents.
|
|
4
4
|
|
|
5
|
-
One call per recipient, before the first cold email, answers: is it legal to send this person a cold or marketing email? Precleared looks the recipient up live (for the UK: Companies House, the domain and the company website) and applies the local rules (UK PECR and GDPR, US CAN-SPAM, Canadian CASL, German UWG, French CNIL guidance and the rules for Australia, Ireland and the
|
|
5
|
+
One call per recipient, before the first cold email, answers: is it legal to send this person a cold or marketing email? Precleared looks the recipient up live (for the UK: Companies House, the domain and the company website) and applies the local rules (UK PECR and GDPR, US CAN-SPAM, Canadian CASL, German UWG, French CNIL guidance and the rules for Australia, Ireland, the Netherlands, Spain, Italy, Latvia and the United Arab Emirates). It returns a verdict, the rule that decided it with its source, what the footer must include, the next step and a signed receipt.
|
|
6
6
|
|
|
7
7
|
Decision support with sources, not legal advice.
|
|
8
8
|
|
|
@@ -32,7 +32,7 @@ else:
|
|
|
32
32
|
print(result.verdict, result.next_step, result.receipt_url)
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
`check()` takes the same fields as the API: `recipient_email` or `recipient_domain`, `pitch`, `address_source` (`published`, `bought`, `scraped`, `given` or `unknown`), `sender_country` (default `GB`), `existing_customer`, `pitch_relevant`, `company_number`, `recipient_country`, `default_recipient_country` and `
|
|
35
|
+
`check()` takes the same fields as the API: `recipient_email` or `recipient_domain`, `pitch`, `address_source` (`published`, `bought`, `scraped`, `given` or `unknown`), `sender_country` (`GB`, `US`, `AE` or `CA`, default `GB`), `existing_customer`, `pitch_relevant`, `company_number`, `company_name`, `recipient_name`, `recipient_country`, `default_recipient_country`, `suppressed` and `email`. Pass the draft as `email={"subject": ..., "body": ..., "from_name": ..., "from_email": ...}` and the result's `email_check` says whether its footer has an opt-out, names the sender and, where the rule needs one, carries a postal address; the draft is not stored.
|
|
36
36
|
|
|
37
37
|
`address_source` defaults to `unknown` and is always sent (the API treats a missing value as `unknown` too). It changes verdicts, so never pass `published` unless the recipient put the address on their website or a public listing for people to contact them.
|
|
38
38
|
|
|
@@ -78,7 +78,7 @@ async with AsyncPrecleared("pc_live_...") as client:
|
|
|
78
78
|
|
|
79
79
|
## Framework tools
|
|
80
80
|
|
|
81
|
-
Each adapter exposes the same two tools, `precleared_check` and `precleared_verify_receipt`, with descriptions that tell the model when to call them: before the first cold email to anyone who is not already a customer, in the UK, US, Canada, Germany, France, Australia, Ireland or the
|
|
81
|
+
Each adapter exposes the same two tools, `precleared_check` and `precleared_verify_receipt`, with descriptions that tell the model when to call them: before the first cold email to anyone who is not already a customer, in the UK, US, Canada, Germany, France, Australia, Ireland, the Netherlands, Spain, Italy, Latvia or the UAE, and not for transactional email. The tools return compact text (verdict, rule, footer_text, next_step, receipt URL) rather than the whole JSON. Pass `to_dict=True` for a dictionary instead. Every factory takes an optional `api_key`.
|
|
82
82
|
|
|
83
83
|
Example tool output:
|
|
84
84
|
|
|
@@ -163,9 +163,9 @@ The API key is never logged, printed or included in an exception.
|
|
|
163
163
|
## Allowances and limits
|
|
164
164
|
|
|
165
165
|
- Without a key: 10 checks a day over the REST API, shared by everyone on the same network.
|
|
166
|
-
- With a free key (`https://precleared.dev/get-key
|
|
166
|
+
- With a free key (`https://precleared.dev/get-key`, no card): 100 checks a day for the first 14 days, then 25 a day over the REST API and 100 over MCP.
|
|
167
167
|
- 60 checks a minute per key. Above that the API returns 429 `rate_limited` with `retry_after`.
|
|
168
|
-
- Days run on UK time.
|
|
168
|
+
- Days run on UK time. For more, £15 a month keeps 100 checks a day, or buy a pack: £9 for 300 checks (a key's first purchase) or £39 for 2,000. Pack checks are used after the day's free checks, never expire and stack on the same key. When no credits are left the API returns 402 with `buy_url`.
|
|
169
169
|
- A check can take up to 25 seconds when a register or website is slow; the client's default timeout is 30 seconds.
|
|
170
170
|
|
|
171
171
|
## Configuration
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "precleared"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Cold-email legality check for AI agents: a Python client for the Precleared API, with LangChain, CrewAI, LlamaIndex and OpenAI Agents tools."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -25,6 +25,8 @@ class PreclearedCheckArgs(BaseModel):
|
|
|
25
25
|
recipient_country: Optional[str] = Field(default=None, description=_D["recipient_country"])
|
|
26
26
|
company_number: Optional[str] = Field(default=None, description=_D["company_number"])
|
|
27
27
|
suppressed: Optional[bool] = Field(default=None, description=_D["suppressed"])
|
|
28
|
+
company_name: Optional[str] = Field(default=None, description=_D["company_name"])
|
|
29
|
+
recipient_name: Optional[str] = Field(default=None, description=_D["recipient_name"])
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
class PreclearedVerifyReceiptArgs(BaseModel):
|
|
@@ -24,9 +24,9 @@ VERIFY_TOOL_NAME = "precleared_verify_receipt"
|
|
|
24
24
|
CHECK_DESCRIPTION = (
|
|
25
25
|
"Check whether a cold or marketing email to one recipient is legal. "
|
|
26
26
|
"Call this once per address before the first cold email to anyone who is not already a customer, "
|
|
27
|
-
"in the UK, US, Canada, Germany, France, Australia, Ireland
|
|
28
|
-
"address in a lead list. Not needed for transactional email such as invoices
|
|
29
|
-
"It applies the local rules (UK PECR, US CAN-SPAM,
|
|
27
|
+
"in the UK, US, Canada, Germany, France, Australia, Ireland, the Netherlands, Spain, Italy, Latvia or the UAE, "
|
|
28
|
+
"including every address in a lead list. Not needed for transactional email such as invoices. "
|
|
29
|
+
"It applies the local rules (UK PECR, US CAN-SPAM, CASL and others) with live lookups "
|
|
30
30
|
"such as UK Companies House. Returns a verdict, the rule that decided it with its source, "
|
|
31
31
|
"footer_text, next_step and a signed receipt id. ok_to_email means send, with a footer that meets "
|
|
32
32
|
"footer_text (a requirement, not wording to paste). consent_needed and blocked mean do not send; "
|
|
@@ -57,7 +57,15 @@ ARG_DESCRIPTIONS: Dict[str, str] = {
|
|
|
57
57
|
"True if the pitch relates to the recipient's role or business, false if it does not. "
|
|
58
58
|
"Leave unset when unsure."
|
|
59
59
|
),
|
|
60
|
-
"sender_country": "Two-letter code for where the sender is based,
|
|
60
|
+
"sender_country": "Two-letter code for where the sender is based: GB, US, AE or CA. Default GB.",
|
|
61
|
+
"company_name": (
|
|
62
|
+
"The recipient's company or organisation name when you know it, for example from a CRM. Helps find "
|
|
63
|
+
"a UK company at Companies House when the domain alone does not."
|
|
64
|
+
),
|
|
65
|
+
"recipient_name": (
|
|
66
|
+
"The recipient's full name when you know it. For UK recipients it is matched against the company's "
|
|
67
|
+
"officers. Precleared does not store it."
|
|
68
|
+
),
|
|
61
69
|
"recipient_country": (
|
|
62
70
|
"Two-letter country code for the recipient when you know it. Otherwise Precleared works it out "
|
|
63
71
|
"from the domain, the register and the website."
|
|
@@ -177,6 +185,8 @@ class PreclearedTools:
|
|
|
177
185
|
recipient_country: Optional[str] = None,
|
|
178
186
|
company_number: Optional[str] = None,
|
|
179
187
|
suppressed: Optional[bool] = None,
|
|
188
|
+
company_name: Optional[str] = None,
|
|
189
|
+
recipient_name: Optional[str] = None,
|
|
180
190
|
) -> Union[str, Dict[str, Any]]:
|
|
181
191
|
"""Run one check and return compact text (or a dict when ``to_dict``)."""
|
|
182
192
|
try:
|
|
@@ -192,6 +202,8 @@ class PreclearedTools:
|
|
|
192
202
|
company_number=_blank_to_none(company_number),
|
|
193
203
|
recipient_country=_blank_to_none(recipient_country),
|
|
194
204
|
suppressed=coerce_bool(suppressed, "suppressed"),
|
|
205
|
+
company_name=_blank_to_none(company_name),
|
|
206
|
+
recipient_name=_blank_to_none(recipient_name),
|
|
195
207
|
)
|
|
196
208
|
except (PreclearedError, ValueError) as exc:
|
|
197
209
|
return self._failure(exc)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -91,6 +91,9 @@ def build_check_body(
|
|
|
91
91
|
recipient_country: Optional[str] = None,
|
|
92
92
|
default_recipient_country: Optional[str] = None,
|
|
93
93
|
suppressed: Optional[bool] = None,
|
|
94
|
+
company_name: Optional[str] = None,
|
|
95
|
+
recipient_name: Optional[str] = None,
|
|
96
|
+
email: Optional[Dict[str, Any]] = None,
|
|
94
97
|
**extra: Any,
|
|
95
98
|
) -> Dict[str, Any]:
|
|
96
99
|
"""Validate the check fields and build the JSON body.
|
|
@@ -143,6 +146,29 @@ def build_check_body(
|
|
|
143
146
|
)
|
|
144
147
|
if suppressed is not None:
|
|
145
148
|
body["suppressed"] = _require_bool(suppressed, "suppressed")
|
|
149
|
+
for name, value in (("company_name", company_name), ("recipient_name", recipient_name)):
|
|
150
|
+
if value is not None:
|
|
151
|
+
if not isinstance(value, str):
|
|
152
|
+
raise ValueError(f"{name} must be a string")
|
|
153
|
+
text = value.strip()
|
|
154
|
+
if text:
|
|
155
|
+
body[name] = text
|
|
156
|
+
if email is not None:
|
|
157
|
+
if not isinstance(email, dict):
|
|
158
|
+
raise ValueError("email must be a dict with a body, for example {'body': '...'}")
|
|
159
|
+
draft_body = email.get("body")
|
|
160
|
+
if not isinstance(draft_body, str) or not draft_body.strip():
|
|
161
|
+
raise ValueError("email.body is required: the draft's text or HTML, including the footer")
|
|
162
|
+
draft: Dict[str, str] = {"body": draft_body}
|
|
163
|
+
for name in ("subject", "from_name", "from_email"):
|
|
164
|
+
value = email.get(name)
|
|
165
|
+
if value is None:
|
|
166
|
+
continue
|
|
167
|
+
if not isinstance(value, str):
|
|
168
|
+
raise ValueError(f"email.{name} must be a string")
|
|
169
|
+
if value.strip():
|
|
170
|
+
draft[name] = value
|
|
171
|
+
body["email"] = draft
|
|
146
172
|
|
|
147
173
|
for key, value in extra.items():
|
|
148
174
|
if value is not None and key not in body:
|
|
@@ -394,6 +420,9 @@ class Precleared(_Base):
|
|
|
394
420
|
recipient_country: Optional[str] = None,
|
|
395
421
|
default_recipient_country: Optional[str] = None,
|
|
396
422
|
suppressed: Optional[bool] = None,
|
|
423
|
+
company_name: Optional[str] = None,
|
|
424
|
+
recipient_name: Optional[str] = None,
|
|
425
|
+
email: Optional[Dict[str, Any]] = None,
|
|
397
426
|
**extra: Any,
|
|
398
427
|
) -> CheckResult:
|
|
399
428
|
"""Check whether a cold email may lawfully be sent to one address.
|
|
@@ -407,7 +436,8 @@ class Precleared(_Base):
|
|
|
407
436
|
``scraped``, ``given`` or ``unknown``. Defaults to ``unknown``.
|
|
408
437
|
Only say ``published`` when the recipient put the address on
|
|
409
438
|
their own website or a public listing for people to contact them.
|
|
410
|
-
sender_country: Two-letter code for where you send from
|
|
439
|
+
sender_country: Two-letter code for where you send from: ``GB``,
|
|
440
|
+
``US``, ``AE`` or ``CA``. Default ``GB``.
|
|
411
441
|
existing_customer: True if the recipient has bought from you.
|
|
412
442
|
pitch_relevant: Whether the pitch relates to the recipient's role.
|
|
413
443
|
company_number: UK Companies House number, when you know it.
|
|
@@ -415,6 +445,15 @@ class Precleared(_Base):
|
|
|
415
445
|
default_recipient_country: Used only when the country cannot be
|
|
416
446
|
worked out from the address, register or website.
|
|
417
447
|
suppressed: True if the address is on your own suppression list.
|
|
448
|
+
company_name: The recipient's company or organisation name, if you have it.
|
|
449
|
+
recipient_name: The recipient's full name, if you have it. Used during
|
|
450
|
+
the check and not stored.
|
|
451
|
+
email: The draft, if you want the answer to say whether its footer
|
|
452
|
+
meets the rule: ``{"subject": ..., "body": ..., "from_name": ...,
|
|
453
|
+
"from_email": ...}`` (``body`` required). The result's
|
|
454
|
+
``email_check`` then says whether the footer has an opt-out, names
|
|
455
|
+
the sender and, where the rule needs one, carries a postal
|
|
456
|
+
address. The draft is not stored; only its hash goes on the receipt.
|
|
418
457
|
**extra: Forwarded as is, for request fields newer than this client.
|
|
419
458
|
|
|
420
459
|
Raises:
|
|
@@ -433,6 +472,9 @@ class Precleared(_Base):
|
|
|
433
472
|
recipient_country=recipient_country,
|
|
434
473
|
default_recipient_country=default_recipient_country,
|
|
435
474
|
suppressed=suppressed,
|
|
475
|
+
company_name=company_name,
|
|
476
|
+
recipient_name=recipient_name,
|
|
477
|
+
email=email,
|
|
436
478
|
**extra,
|
|
437
479
|
)
|
|
438
480
|
response = self._request("POST", "/api/v1/check", json=body)
|
|
@@ -506,6 +548,9 @@ class AsyncPrecleared(_Base):
|
|
|
506
548
|
recipient_country: Optional[str] = None,
|
|
507
549
|
default_recipient_country: Optional[str] = None,
|
|
508
550
|
suppressed: Optional[bool] = None,
|
|
551
|
+
company_name: Optional[str] = None,
|
|
552
|
+
recipient_name: Optional[str] = None,
|
|
553
|
+
email: Optional[Dict[str, Any]] = None,
|
|
509
554
|
**extra: Any,
|
|
510
555
|
) -> CheckResult:
|
|
511
556
|
"""Async version of :meth:`Precleared.check`."""
|
|
@@ -521,6 +566,9 @@ class AsyncPrecleared(_Base):
|
|
|
521
566
|
recipient_country=recipient_country,
|
|
522
567
|
default_recipient_country=default_recipient_country,
|
|
523
568
|
suppressed=suppressed,
|
|
569
|
+
company_name=company_name,
|
|
570
|
+
recipient_name=recipient_name,
|
|
571
|
+
email=email,
|
|
524
572
|
**extra,
|
|
525
573
|
)
|
|
526
574
|
response = await self._request("POST", "/api/v1/check", json=body)
|
|
@@ -64,7 +64,7 @@ class DailyLimit(PreclearedError):
|
|
|
64
64
|
"""HTTP 429 ``daily_limit``: the free daily allowance is used up.
|
|
65
65
|
|
|
66
66
|
Keyless use allows 10 REST checks a day, shared by everyone on the same
|
|
67
|
-
network. A free key allows 25 a day.
|
|
67
|
+
network. A free key allows 100 a day for its first 14 days, then 25 a day.
|
|
68
68
|
|
|
69
69
|
Attributes:
|
|
70
70
|
get_key_url: Where to get a free key.
|
|
@@ -255,6 +255,10 @@ class CheckResult:
|
|
|
255
255
|
usage: Remaining free checks and pack credits.
|
|
256
256
|
disclaimer: Fixed line: decision support with sources, not legal advice.
|
|
257
257
|
rules_version: Version of the rule set used.
|
|
258
|
+
email_check: Only when a draft was passed in ``email``: whether its
|
|
259
|
+
footer meets the rule (``status``, ``requirements``, ``missing``).
|
|
260
|
+
charged: False when the answer was free: no rule applied, a re-check
|
|
261
|
+
within a day, or an address on your opt-out list.
|
|
258
262
|
raw: The full response as returned by the API.
|
|
259
263
|
"""
|
|
260
264
|
|
|
@@ -269,6 +273,8 @@ class CheckResult:
|
|
|
269
273
|
usage: Optional[Usage] = None
|
|
270
274
|
disclaimer: Optional[str] = None
|
|
271
275
|
rules_version: Optional[str] = None
|
|
276
|
+
email_check: Optional[Dict[str, Any]] = None
|
|
277
|
+
charged: Optional[bool] = None
|
|
272
278
|
raw: Dict[str, Any] = field(default_factory=dict, repr=False)
|
|
273
279
|
base_url: str = field(default=DEFAULT_BASE_URL, repr=False)
|
|
274
280
|
|
|
@@ -305,6 +311,8 @@ class CheckResult:
|
|
|
305
311
|
usage=Usage.from_dict(data["usage"]) if isinstance(data.get("usage"), dict) else None,
|
|
306
312
|
disclaimer=_opt_str(data.get("disclaimer")),
|
|
307
313
|
rules_version=_opt_str(data.get("rules_version")),
|
|
314
|
+
email_check=dict(data["email_check"]) if isinstance(data.get("email_check"), dict) else None,
|
|
315
|
+
charged=bool(data["charged"]) if isinstance(data.get("charged"), bool) else None,
|
|
308
316
|
raw=dict(data),
|
|
309
317
|
base_url=base_url,
|
|
310
318
|
)
|
|
@@ -45,6 +45,8 @@ _CHECK_ARGS = [
|
|
|
45
45
|
"recipient_country",
|
|
46
46
|
"company_number",
|
|
47
47
|
"suppressed",
|
|
48
|
+
"company_name",
|
|
49
|
+
"recipient_name",
|
|
48
50
|
]
|
|
49
51
|
|
|
50
52
|
|
|
@@ -60,6 +62,8 @@ def _check_tool(runner: PreclearedTools) -> FunctionTool:
|
|
|
60
62
|
recipient_country: Optional[str] = None,
|
|
61
63
|
company_number: Optional[str] = None,
|
|
62
64
|
suppressed: Optional[bool] = None,
|
|
65
|
+
company_name: Optional[str] = None,
|
|
66
|
+
recipient_name: Optional[str] = None,
|
|
63
67
|
) -> Union[str, Dict[str, Any]]:
|
|
64
68
|
return runner.check(
|
|
65
69
|
recipient_email=recipient_email,
|
|
@@ -72,6 +76,8 @@ def _check_tool(runner: PreclearedTools) -> FunctionTool:
|
|
|
72
76
|
recipient_country=recipient_country,
|
|
73
77
|
company_number=company_number,
|
|
74
78
|
suppressed=suppressed,
|
|
79
|
+
company_name=company_name,
|
|
80
|
+
recipient_name=recipient_name,
|
|
75
81
|
)
|
|
76
82
|
|
|
77
83
|
precleared_check.__doc__ = google_docstring(
|
|
@@ -30,7 +30,7 @@ async def test_async_check_success(make_async_client, recorder):
|
|
|
30
30
|
assert result.rule == "gb_pecr_corporate_subscriber"
|
|
31
31
|
assert result.receipt_url == "https://precleared.dev/r/cc_rcpt_0123456789ABCDEF"
|
|
32
32
|
assert recorder.last.headers["authorization"] == f"Bearer {KEY}"
|
|
33
|
-
assert recorder.last.headers["user-agent"] == "precleared-python/0.
|
|
33
|
+
assert recorder.last.headers["user-agent"] == "precleared-python/0.2.0"
|
|
34
34
|
assert recorder.last_json()["address_source"] == "given"
|
|
35
35
|
assert client._http.is_closed
|
|
36
36
|
|
|
@@ -85,7 +85,7 @@ def test_check_sends_expected_body_and_headers_keyless(make_client, recorder):
|
|
|
85
85
|
assert request.method == "POST"
|
|
86
86
|
assert str(request.url) == "https://precleared.dev/api/v1/check"
|
|
87
87
|
assert request.headers["content-type"] == "application/json"
|
|
88
|
-
assert request.headers["user-agent"] == "precleared-python/0.
|
|
88
|
+
assert request.headers["user-agent"] == "precleared-python/0.2.0" == DEFAULT_USER_AGENT
|
|
89
89
|
assert request.headers["accept"] == "application/json"
|
|
90
90
|
assert "authorization" not in request.headers
|
|
91
91
|
|
|
@@ -494,6 +494,37 @@ def test_public_exports():
|
|
|
494
494
|
):
|
|
495
495
|
assert name in precleared.__all__
|
|
496
496
|
assert hasattr(precleared, name)
|
|
497
|
-
assert precleared.__version__ == "0.
|
|
497
|
+
assert precleared.__version__ == "0.2.0"
|
|
498
498
|
for cls in (InvalidRequest, InvalidKey, PaymentRequired, DailyLimit, RateLimited, ReceiptNotFound, ServerError, NetworkError):
|
|
499
499
|
assert issubclass(cls, PreclearedError)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def test_check_sends_company_name_recipient_name_and_draft_email(make_client, recorder):
|
|
504
|
+
payload = {
|
|
505
|
+
"verdict": "ok_to_email",
|
|
506
|
+
"email_check": {"status": "fail", "country": "US", "requirements": [], "missing": ["postal_address"], "footer_text": None, "note": "Presence checks for US."},
|
|
507
|
+
"charged": True,
|
|
508
|
+
}
|
|
509
|
+
client = make_client(routed(check=json_response(200, payload)))
|
|
510
|
+
result = client.check(
|
|
511
|
+
"sales@precleared.dev",
|
|
512
|
+
sender_country="ca",
|
|
513
|
+
company_name=" Acme Ltd ",
|
|
514
|
+
recipient_name="Jane Smith",
|
|
515
|
+
email={"subject": "Hi", "body": "Hello {{unsubscribe}}", "from_name": "Sam", "from_email": ""},
|
|
516
|
+
)
|
|
517
|
+
body = recorder.last_json()
|
|
518
|
+
assert body["sender_country"] == "CA"
|
|
519
|
+
assert body["company_name"] == "Acme Ltd"
|
|
520
|
+
assert body["recipient_name"] == "Jane Smith"
|
|
521
|
+
assert body["email"] == {"subject": "Hi", "body": "Hello {{unsubscribe}}", "from_name": "Sam"}
|
|
522
|
+
assert result.email_check == payload["email_check"]
|
|
523
|
+
assert result.charged is True
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def test_check_rejects_a_draft_without_a_body(make_client, recorder):
|
|
527
|
+
client = make_client()
|
|
528
|
+
with pytest.raises(ValueError, match="email.body is required"):
|
|
529
|
+
client.check("a@b.co", email={"subject": "Hi"})
|
|
530
|
+
assert recorder.requests == []
|
|
@@ -20,6 +20,7 @@ from conftest import json_response, routed # noqa: E402
|
|
|
20
20
|
CHECK_FIELDS = {
|
|
21
21
|
"recipient_email", "recipient_domain", "pitch", "address_source", "existing_customer",
|
|
22
22
|
"pitch_relevant", "sender_country", "recipient_country", "company_number", "suppressed",
|
|
23
|
+
"company_name", "recipient_name",
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
|
|
@@ -28,7 +28,7 @@ def test_tool_names_and_descriptions_tell_the_model_when_to_call():
|
|
|
28
28
|
for phrase in (
|
|
29
29
|
"before the first cold email",
|
|
30
30
|
"not already a customer",
|
|
31
|
-
"UK, US, Canada, Germany, France, Australia, Ireland or the
|
|
31
|
+
"UK, US, Canada, Germany, France, Australia, Ireland, the Netherlands, Spain, Italy, Latvia or the UAE",
|
|
32
32
|
"Not needed for transactional email",
|
|
33
33
|
"ok_to_email means send, with a footer that meets footer_text",
|
|
34
34
|
"consent_needed and blocked mean do not send",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|