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.
Files changed (30) hide show
  1. {precleared-0.1.0 → precleared-0.2.0}/PKG-INFO +6 -6
  2. {precleared-0.1.0 → precleared-0.2.0}/README.md +5 -5
  3. {precleared-0.1.0 → precleared-0.2.0}/pyproject.toml +1 -1
  4. {precleared-0.1.0 → precleared-0.2.0}/src/precleared/_schemas.py +2 -0
  5. {precleared-0.1.0 → precleared-0.2.0}/src/precleared/_tools.py +16 -4
  6. precleared-0.2.0/src/precleared/_version.py +1 -0
  7. {precleared-0.1.0 → precleared-0.2.0}/src/precleared/client.py +49 -1
  8. {precleared-0.1.0 → precleared-0.2.0}/src/precleared/errors.py +1 -1
  9. {precleared-0.1.0 → precleared-0.2.0}/src/precleared/models.py +8 -0
  10. {precleared-0.1.0 → precleared-0.2.0}/src/precleared/openai_agents.py +6 -0
  11. {precleared-0.1.0 → precleared-0.2.0}/tests/test_async_client.py +1 -1
  12. {precleared-0.1.0 → precleared-0.2.0}/tests/test_client.py +33 -2
  13. {precleared-0.1.0 → precleared-0.2.0}/tests/test_langchain.py +1 -0
  14. {precleared-0.1.0 → precleared-0.2.0}/tests/test_openai_agents.py +2 -0
  15. {precleared-0.1.0 → precleared-0.2.0}/tests/test_tools_common.py +1 -1
  16. precleared-0.1.0/src/precleared/_version.py +0 -1
  17. {precleared-0.1.0 → precleared-0.2.0}/.gitignore +0 -0
  18. {precleared-0.1.0 → precleared-0.2.0}/LICENSE +0 -0
  19. {precleared-0.1.0 → precleared-0.2.0}/PUBLISHING.md +0 -0
  20. {precleared-0.1.0 → precleared-0.2.0}/src/precleared/__init__.py +0 -0
  21. {precleared-0.1.0 → precleared-0.2.0}/src/precleared/crewai.py +0 -0
  22. {precleared-0.1.0 → precleared-0.2.0}/src/precleared/langchain.py +0 -0
  23. {precleared-0.1.0 → precleared-0.2.0}/src/precleared/llamaindex.py +0 -0
  24. {precleared-0.1.0 → precleared-0.2.0}/src/precleared/py.typed +0 -0
  25. {precleared-0.1.0 → precleared-0.2.0}/tests/conftest.py +0 -0
  26. {precleared-0.1.0 → precleared-0.2.0}/tests/test_crewai.py +0 -0
  27. {precleared-0.1.0 → precleared-0.2.0}/tests/test_import_guards.py +0 -0
  28. {precleared-0.1.0 → precleared-0.2.0}/tests/test_live_samples.py +0 -0
  29. {precleared-0.1.0 → precleared-0.2.0}/tests/test_llamaindex.py +0 -0
  30. {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.1.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 Netherlands). It returns a verdict, the rule that decided it with its source, what the footer must include, the next step and a signed receipt.
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 `suppressed`.
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 Netherlands, 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`.
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`): 25 checks a day over the REST API.
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. After the free checks, checks use pack credits: a pack is £39 for 2,000 checks, packs never expire and stack on the same key. When no credits are left the API returns 402 with `buy_url`.
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 Netherlands). It returns a verdict, the rule that decided it with its source, what the footer must include, the next step and a signed receipt.
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 `suppressed`.
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 Netherlands, 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`.
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`): 25 checks a day over the REST API.
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. After the free checks, checks use pack credits: a pack is £39 for 2,000 checks, packs never expire and stack on the same key. When no credits are left the API returns 402 with `buy_url`.
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.1.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 or the Netherlands, including every "
28
- "address in a lead list. Not needed for transactional email such as invoices or password resets. "
29
- "It applies the local rules (UK PECR, US CAN-SPAM, Canadian CASL and others) using live lookups "
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, for example GB or US. Default GB.",
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. Default ``GB``.
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.1.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.1.0" == DEFAULT_USER_AGENT
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.1.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
 
@@ -37,6 +37,8 @@ FULL_ARGS = {
37
37
  "recipient_country": None,
38
38
  "company_number": None,
39
39
  "suppressed": None,
40
+ "company_name": None,
41
+ "recipient_name": None,
40
42
  }
41
43
 
42
44
 
@@ -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 Netherlands",
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