precleared 0.1.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/.gitignore +8 -0
- precleared-0.1.0/LICENSE +21 -0
- precleared-0.1.0/PKG-INFO +240 -0
- precleared-0.1.0/PUBLISHING.md +127 -0
- precleared-0.1.0/README.md +197 -0
- precleared-0.1.0/pyproject.toml +77 -0
- precleared-0.1.0/src/precleared/__init__.py +65 -0
- precleared-0.1.0/src/precleared/_schemas.py +36 -0
- precleared-0.1.0/src/precleared/_tools.py +234 -0
- precleared-0.1.0/src/precleared/_version.py +1 -0
- precleared-0.1.0/src/precleared/client.py +561 -0
- precleared-0.1.0/src/precleared/crewai.py +98 -0
- precleared-0.1.0/src/precleared/errors.py +115 -0
- precleared-0.1.0/src/precleared/langchain.py +84 -0
- precleared-0.1.0/src/precleared/llamaindex.py +74 -0
- precleared-0.1.0/src/precleared/models.py +426 -0
- precleared-0.1.0/src/precleared/openai_agents.py +122 -0
- precleared-0.1.0/src/precleared/py.typed +0 -0
- precleared-0.1.0/tests/conftest.py +208 -0
- precleared-0.1.0/tests/test_async_client.py +108 -0
- precleared-0.1.0/tests/test_client.py +499 -0
- precleared-0.1.0/tests/test_crewai.py +75 -0
- precleared-0.1.0/tests/test_import_guards.py +69 -0
- precleared-0.1.0/tests/test_langchain.py +103 -0
- precleared-0.1.0/tests/test_live_samples.py +174 -0
- precleared-0.1.0/tests/test_llamaindex.py +86 -0
- precleared-0.1.0/tests/test_models.py +226 -0
- precleared-0.1.0/tests/test_openai_agents.py +124 -0
- precleared-0.1.0/tests/test_tools_common.py +182 -0
precleared-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Precleared
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: precleared
|
|
3
|
+
Version: 0.1.0
|
|
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
|
+
Project-URL: Homepage, https://precleared.dev
|
|
6
|
+
Project-URL: Documentation, https://precleared.dev/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/gabrielaoimevbore-beep/precleared-python
|
|
8
|
+
Project-URL: API reference, https://precleared.dev/openapi.json
|
|
9
|
+
Project-URL: Issues, https://github.com/gabrielaoimevbore-beep/precleared-python/issues
|
|
10
|
+
Author-email: Precleared <support@precleared.dev>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: CAN-SPAM,CASL,GDPR,PECR,ai agents,cold email,crewai,email compliance,langchain,llamaindex,openai-agents
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Communications :: Email
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Requires-Dist: httpx>=0.25.0
|
|
29
|
+
Provides-Extra: all
|
|
30
|
+
Requires-Dist: crewai>=0.80.0; extra == 'all'
|
|
31
|
+
Requires-Dist: langchain-core>=0.3.0; extra == 'all'
|
|
32
|
+
Requires-Dist: llama-index-core>=0.11.0; extra == 'all'
|
|
33
|
+
Requires-Dist: openai-agents>=0.0.3; extra == 'all'
|
|
34
|
+
Provides-Extra: crewai
|
|
35
|
+
Requires-Dist: crewai>=0.80.0; extra == 'crewai'
|
|
36
|
+
Provides-Extra: langchain
|
|
37
|
+
Requires-Dist: langchain-core>=0.3.0; extra == 'langchain'
|
|
38
|
+
Provides-Extra: llamaindex
|
|
39
|
+
Requires-Dist: llama-index-core>=0.11.0; extra == 'llamaindex'
|
|
40
|
+
Provides-Extra: openai-agents
|
|
41
|
+
Requires-Dist: openai-agents>=0.0.3; extra == 'openai-agents'
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
# precleared
|
|
45
|
+
|
|
46
|
+
Python client for [Precleared](https://precleared.dev), a cold-email legality check for AI agents.
|
|
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.
|
|
49
|
+
|
|
50
|
+
Decision support with sources, not legal advice.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install precleared # client only (depends on httpx)
|
|
56
|
+
pip install "precleared[langchain]" # + LangChain / LangGraph tools
|
|
57
|
+
pip install "precleared[crewai]" # + CrewAI tools
|
|
58
|
+
pip install "precleared[llamaindex]" # + LlamaIndex tools
|
|
59
|
+
pip install "precleared[openai-agents]" # + OpenAI Agents SDK tools
|
|
60
|
+
pip install "precleared[all]"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Python 3.9 or later for the client. The framework extras follow the frameworks' own Python requirements (CrewAI and the current LangChain, LlamaIndex and Agents SDK releases need 3.10 or later).
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from precleared import Precleared
|
|
69
|
+
|
|
70
|
+
client = Precleared() # keyless: 10 checks a day per network. Or Precleared("pc_live_...")
|
|
71
|
+
result = client.check("jane@acme.co.uk", pitch="Bookkeeping software for small firms", address_source="unknown")
|
|
72
|
+
if result.ok:
|
|
73
|
+
print("Send, with a footer that meets:", result.footer_text)
|
|
74
|
+
else:
|
|
75
|
+
print(result.verdict, result.next_step, result.receipt_url)
|
|
76
|
+
```
|
|
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`.
|
|
79
|
+
|
|
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
|
+
|
|
82
|
+
### Reading a result
|
|
83
|
+
|
|
84
|
+
| Verdict | Meaning | `result.ok` | `result.must_not_send` |
|
|
85
|
+
| --- | --- | --- | --- |
|
|
86
|
+
| `ok_to_email` | Send, with a footer that meets `footer_text` | `True` | `False` |
|
|
87
|
+
| `consent_needed` | Do not send. Follow `next_step` | `False` | `True` |
|
|
88
|
+
| `blocked` | Do not send. Follow `next_step` | `False` | `True` |
|
|
89
|
+
| `unknown` | A fact could not be established. Treat as `consent_needed` unless you can supply the missing fact (for example `company_number` or `recipient_country`) and check again | `False` | `False` (see `needs_more_facts`) |
|
|
90
|
+
|
|
91
|
+
`footer_text` describes a requirement (who you are, a postal address, a way to opt out). It is not wording to paste.
|
|
92
|
+
|
|
93
|
+
`CheckResult` is a dataclass with typed fields (`verdict`, `confidence`, `reasons`, `rules_applied`, `footer_text`, `next_step`, `facts`, `receipt`, `usage`, `disclaimer`, `rules_version`) plus:
|
|
94
|
+
|
|
95
|
+
- `.raw`: the full response as a dict
|
|
96
|
+
- `.rule`: `rule_key` of the reason that decided the verdict
|
|
97
|
+
- `.reason`: that reason, with `text`, `source_title` and `source_url`
|
|
98
|
+
- `.notes`: the other reasons entries
|
|
99
|
+
- `.receipt_id` and `.receipt_url` (`https://precleared.dev/r/<id>`)
|
|
100
|
+
- `.summary()` and `.summary_text()`: the compact forms the framework tools return
|
|
101
|
+
|
|
102
|
+
### Receipts and health
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
receipt = client.verify_receipt("cc_rcpt_...") # also accepts a receipt URL
|
|
106
|
+
receipt.verdict, receipt.request_hash, receipt.response_hash, receipt.signature, receipt.public_key
|
|
107
|
+
|
|
108
|
+
client.health().ok # GET /api/v1/health: status, checked_at, api_ok, mcp_ok, worker_ok
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A receipt proves what was asked and answered. The signature is Ed25519 over `request_hash.response_hash`; the receipt endpoint returns the public key alongside it, and `GET /api/v1/public-key` serves it too.
|
|
112
|
+
|
|
113
|
+
### Async
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from precleared import AsyncPrecleared
|
|
117
|
+
|
|
118
|
+
async with AsyncPrecleared("pc_live_...") as client:
|
|
119
|
+
result = await client.check("jane@acme.co.uk", address_source="bought")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Framework tools
|
|
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`.
|
|
125
|
+
|
|
126
|
+
Example tool output:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
verdict: consent_needed (confidence: medium)
|
|
130
|
+
rule: gb_individual_consent
|
|
131
|
+
reason: The recipient is a sole trader or a partnership in England, Wales or Northern Ireland. PECR treats them as individual subscribers, so unsolicited marketing email needs their prior consent, or the soft opt-in for existing customers. Source: PECR 2003 regulation 22(2) and ICO Guide to PECR, https://www.legislation.gov.uk/uksi/2003/2426/regulation/22
|
|
132
|
+
next_step: Do not cold email. Use a route they offer, such as their contact form, or obtain consent first.
|
|
133
|
+
receipt: cc_rcpt_E21TTJV8D9JAYTQZ https://precleared.dev/r/cc_rcpt_E21TTJV8D9JAYTQZ
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
API errors come back as text too (`error: daily_limit`, with `get_key_url`, `buy_url` or `retry_after` when the API gives one), so the agent can react instead of crashing.
|
|
137
|
+
|
|
138
|
+
### LangChain / LangGraph
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from langchain.agents import create_agent # pip install langchain
|
|
142
|
+
from precleared.langchain import precleared_tools
|
|
143
|
+
|
|
144
|
+
agent = create_agent(model, tools=precleared_tools(api_key="pc_live_..."))
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`precleared_tools()` returns two `StructuredTool`s with pydantic argument schemas, so they also work with `model.bind_tools(...)` and LangGraph's prebuilt agents. `precleared_check_tool()` and `precleared_verify_receipt_tool()` build them one at a time.
|
|
148
|
+
|
|
149
|
+
### CrewAI
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from crewai import Agent
|
|
153
|
+
from precleared.crewai import PreclearedCheckTool, PreclearedVerifyReceiptTool
|
|
154
|
+
|
|
155
|
+
sdr = Agent(role="SDR", goal="...", backstory="...", tools=[PreclearedCheckTool(api_key="pc_live_..."), PreclearedVerifyReceiptTool()])
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The key is held privately, so it does not appear in the tool's `repr()` or `model_dump()`.
|
|
159
|
+
|
|
160
|
+
### LlamaIndex
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from llama_index.core.agent.workflow import FunctionAgent
|
|
164
|
+
from precleared.llamaindex import precleared_tools
|
|
165
|
+
|
|
166
|
+
agent = FunctionAgent(tools=precleared_tools(), llm=llm)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### OpenAI Agents SDK
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from agents import Agent
|
|
173
|
+
from precleared.openai_agents import precleared_tools
|
|
174
|
+
|
|
175
|
+
agent = Agent(name="SDR", instructions="Check every address before the first email.", tools=precleared_tools())
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Errors
|
|
179
|
+
|
|
180
|
+
Everything raised after a request is sent is a subclass of `precleared.PreclearedError`, with `.message`, `.status_code`, `.code` and `.body`. Bad arguments raise a plain `ValueError` before anything is sent (for example an `address_source` outside the five allowed values).
|
|
181
|
+
|
|
182
|
+
| Class | When | Extra attributes |
|
|
183
|
+
| --- | --- | --- |
|
|
184
|
+
| `InvalidRequest` | 400, the body names the invalid field | |
|
|
185
|
+
| `InvalidKey` | 401, `invalid_key` or `revoked_key` | `.code` |
|
|
186
|
+
| `PaymentRequired` | 402, free checks used and no pack credits | `.buy_url` |
|
|
187
|
+
| `DailyLimit` | 429 `daily_limit`, the free daily allowance is used | `.get_key_url` |
|
|
188
|
+
| `RateLimited` | 429 `rate_limited`, more than 60 checks a minute | `.retry_after` (seconds, from the body or the `Retry-After` header) |
|
|
189
|
+
| `ReceiptNotFound` | 404 from the receipt endpoint | |
|
|
190
|
+
| `ServerError` | 5xx | |
|
|
191
|
+
| `NetworkError` | the request did not complete (DNS, connection, timeout) | `__cause__` is the httpx exception |
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from precleared import Precleared, DailyLimit, RateLimited
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
result = Precleared().check("jane@acme.co.uk")
|
|
198
|
+
except RateLimited as exc:
|
|
199
|
+
time.sleep(exc.retry_after or 1)
|
|
200
|
+
except DailyLimit as exc:
|
|
201
|
+
print("Get a free key:", exc.get_key_url)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The API key is never logged, printed or included in an exception.
|
|
205
|
+
|
|
206
|
+
## Allowances and limits
|
|
207
|
+
|
|
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.
|
|
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`.
|
|
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
|
+
|
|
214
|
+
## Configuration
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
Precleared(
|
|
218
|
+
api_key=None, # or the PRECLEARED_API_KEY environment variable
|
|
219
|
+
base_url="https://precleared.dev",
|
|
220
|
+
timeout=30, # seconds, or an httpx.Timeout
|
|
221
|
+
user_agent="precleared-python/0.1.0", # Precleared's hosting refuses requests without one
|
|
222
|
+
)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Disclaimer
|
|
226
|
+
|
|
227
|
+
Decision support with sources, not legal advice. The receipt proves what was asked and answered, not that an email was sent.
|
|
228
|
+
|
|
229
|
+
## Support
|
|
230
|
+
|
|
231
|
+
support@precleared.dev. API reference: https://precleared.dev/docs and https://precleared.dev/openapi.json.
|
|
232
|
+
|
|
233
|
+
## Development
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
pip install -e . pytest anyio
|
|
237
|
+
pytest
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The tests mock all HTTP with `httpx.MockTransport` and never call the live API. The adapter tests run when the matching framework is installed and skip otherwise.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Publishing `precleared` to PyPI
|
|
2
|
+
|
|
3
|
+
Steps for publishing this package under your own PyPI account. Nothing here has been run; the package name `precleared` was free on PyPI on 17 October 2026 (so was `precleared-sdk`, kept in reserve).
|
|
4
|
+
|
|
5
|
+
## 0. One-off account setup
|
|
6
|
+
|
|
7
|
+
1. Create an account at https://pypi.org/account/register/ and turn on two-factor authentication (PyPI requires it for uploads).
|
|
8
|
+
2. Create an API token at https://pypi.org/manage/account/token/ with scope "Entire account" for the first upload. After the first release, replace it with a token scoped to the `precleared` project.
|
|
9
|
+
3. Optional but recommended: do the same at https://test.pypi.org to rehearse.
|
|
10
|
+
|
|
11
|
+
Keep tokens out of the repository. `twine` reads them from `~/.pypirc` or from the `TWINE_USERNAME` and `TWINE_PASSWORD` environment variables.
|
|
12
|
+
|
|
13
|
+
## 1. Check the version
|
|
14
|
+
|
|
15
|
+
The version lives in two places and must match:
|
|
16
|
+
|
|
17
|
+
- `pyproject.toml`: `version = "0.1.0"`
|
|
18
|
+
- `src/precleared/_version.py`: `__version__ = "0.1.0"`
|
|
19
|
+
|
|
20
|
+
PyPI never lets you re-upload the same version, so bump both before every release.
|
|
21
|
+
|
|
22
|
+
## 2. Run the tests
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd sdk-python
|
|
26
|
+
python -m venv .venv && source .venv/bin/activate
|
|
27
|
+
pip install -e ".[all]" pytest anyio
|
|
28
|
+
pytest
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The framework tests skip when a framework is missing, so `pip install -e . pytest anyio` is enough for the core suite.
|
|
32
|
+
|
|
33
|
+
## 3. Build
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install --upgrade build twine
|
|
37
|
+
rm -rf dist
|
|
38
|
+
python -m build
|
|
39
|
+
twine check dist/*
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`python -m build` produces `dist/precleared-0.1.0.tar.gz` and `dist/precleared-0.1.0-py3-none-any.whl`. `twine check` must report `PASSED` for both.
|
|
43
|
+
|
|
44
|
+
## 4. Rehearse on TestPyPI (optional)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
twine upload --repository testpypi dist/*
|
|
48
|
+
python -m venv /tmp/try && /tmp/try/bin/pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ precleared
|
|
49
|
+
/tmp/try/bin/python -c "import precleared; print(precleared.__version__)"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 5. Upload to PyPI
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
twine upload dist/*
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
When prompted, the username is `__token__` and the password is the API token (including the `pypi-` prefix). Then confirm:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python -m venv /tmp/check && /tmp/check/bin/pip install precleared
|
|
62
|
+
/tmp/check/bin/python -c "from precleared import Precleared; print(Precleared())"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The project page will be https://pypi.org/project/precleared/.
|
|
66
|
+
|
|
67
|
+
## 6. Trusted publishing instead of tokens (recommended once the code is on GitHub)
|
|
68
|
+
|
|
69
|
+
Trusted publishing lets GitHub Actions upload without a long-lived token.
|
|
70
|
+
|
|
71
|
+
1. On PyPI, open https://pypi.org/manage/account/publishing/ and add a "pending publisher": PyPI project name `precleared`, your GitHub owner and repository name, workflow file name `publish.yml`, environment name `pypi`.
|
|
72
|
+
2. In the repository, create a `pypi` environment under Settings, Environments.
|
|
73
|
+
3. Add `.github/workflows/publish.yml`:
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
name: Publish to PyPI
|
|
77
|
+
on:
|
|
78
|
+
release:
|
|
79
|
+
types: [published]
|
|
80
|
+
jobs:
|
|
81
|
+
publish:
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
environment: pypi
|
|
84
|
+
permissions:
|
|
85
|
+
id-token: write
|
|
86
|
+
contents: read
|
|
87
|
+
steps:
|
|
88
|
+
- uses: actions/checkout@v4
|
|
89
|
+
- uses: actions/setup-python@v5
|
|
90
|
+
with:
|
|
91
|
+
python-version: "3.12"
|
|
92
|
+
- run: python -m pip install --upgrade build
|
|
93
|
+
- run: python -m build
|
|
94
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
4. Publish a GitHub release tagged `v0.1.0`. The workflow builds and uploads; no token is stored anywhere.
|
|
98
|
+
|
|
99
|
+
The first upload can be done either way. If you upload with a token first, add the trusted publisher afterwards from the project's "Publishing" settings page.
|
|
100
|
+
|
|
101
|
+
## 7. After publishing: get listed by the frameworks
|
|
102
|
+
|
|
103
|
+
### LangChain (verified on 17 October 2026)
|
|
104
|
+
|
|
105
|
+
LangChain no longer accepts integration code or new documentation pages by pull request. The route for a published package is the integration listing form in the `langchain-ai/docs` repository:
|
|
106
|
+
|
|
107
|
+
- Open https://github.com/langchain-ai/docs/issues/new?template=06-integration-submission.yml and fill it in with the PyPI name `precleared`, the docs URL (`https://precleared.dev/docs` or the README on PyPI) and the component type "tools".
|
|
108
|
+
- A maintainer applies the `integration-run` label and automation opens the pull request that adds a row to `scripts/data/integration_external_docs.yaml`. Do not open that PR by hand and do not add an MDX page; automation closes manual ones.
|
|
109
|
+
- A hosted guide under `src/oss/python/integrations/tools/` (template `src/oss/python/integrations/tools/TEMPLATE.mdx` in `langchain-ai/docs`) is only for packages with 50,000 or more monthly downloads, or ones the maintainers choose to feature.
|
|
110
|
+
|
|
111
|
+
Source: https://docs.langchain.com/oss/python/contributing/integrations-langchain
|
|
112
|
+
|
|
113
|
+
### LlamaIndex (partly verified)
|
|
114
|
+
|
|
115
|
+
LlamaIndex lists tools on LlamaHub from packages inside its monorepo, `run-llama/llama_index`. Tool packages live under `llama-index-integrations/tools/llama-index-tools-<name>/` (confirmed from the README of the published `llama-index-tools-tavily-research` package, which points at `llama-index-integrations/tools/llama-index-tools-tavily-research/examples/tavily.ipynb`). The contributing guide at https://docs.llamaindex.ai/en/latest/CONTRIBUTING asks for unit tests that mock the remote system and for coverage of at least 50 percent.
|
|
116
|
+
|
|
117
|
+
So the route is a pull request to `run-llama/llama_index` adding `llama-index-integrations/tools/llama-index-tools-precleared/` (its own `pyproject.toml`, `README.md`, `llama_index/tools/precleared/base.py` wrapping `precleared.llamaindex`, `tests/` and an `examples/` notebook). Not verified: the exact scaffold command (`llamaindex-cli new-package` is the usual one) and whether a docs notebook under `docs/docs/examples/tools/` is still required; check the repository's current `CONTRIBUTING.md` before opening the PR.
|
|
118
|
+
|
|
119
|
+
### CrewAI and the OpenAI Agents SDK (not verified)
|
|
120
|
+
|
|
121
|
+
Community CrewAI tools are collected in the `crewAIInc/crewAI-tools` repository; this was not verified from inside this session. The OpenAI Agents SDK has no integration directory; the README example is the integration.
|
|
122
|
+
|
|
123
|
+
## 8. Next releases
|
|
124
|
+
|
|
125
|
+
1. Bump the version in both files and note the change in the README if the surface changed.
|
|
126
|
+
2. `pytest`, `python -m build`, `twine check dist/*`.
|
|
127
|
+
3. `twine upload dist/*`, or publish a GitHub release if trusted publishing is set up.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# precleared
|
|
2
|
+
|
|
3
|
+
Python client for [Precleared](https://precleared.dev), a cold-email legality check for AI agents.
|
|
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.
|
|
6
|
+
|
|
7
|
+
Decision support with sources, not legal advice.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install precleared # client only (depends on httpx)
|
|
13
|
+
pip install "precleared[langchain]" # + LangChain / LangGraph tools
|
|
14
|
+
pip install "precleared[crewai]" # + CrewAI tools
|
|
15
|
+
pip install "precleared[llamaindex]" # + LlamaIndex tools
|
|
16
|
+
pip install "precleared[openai-agents]" # + OpenAI Agents SDK tools
|
|
17
|
+
pip install "precleared[all]"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Python 3.9 or later for the client. The framework extras follow the frameworks' own Python requirements (CrewAI and the current LangChain, LlamaIndex and Agents SDK releases need 3.10 or later).
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from precleared import Precleared
|
|
26
|
+
|
|
27
|
+
client = Precleared() # keyless: 10 checks a day per network. Or Precleared("pc_live_...")
|
|
28
|
+
result = client.check("jane@acme.co.uk", pitch="Bookkeeping software for small firms", address_source="unknown")
|
|
29
|
+
if result.ok:
|
|
30
|
+
print("Send, with a footer that meets:", result.footer_text)
|
|
31
|
+
else:
|
|
32
|
+
print(result.verdict, result.next_step, result.receipt_url)
|
|
33
|
+
```
|
|
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`.
|
|
36
|
+
|
|
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
|
+
|
|
39
|
+
### Reading a result
|
|
40
|
+
|
|
41
|
+
| Verdict | Meaning | `result.ok` | `result.must_not_send` |
|
|
42
|
+
| --- | --- | --- | --- |
|
|
43
|
+
| `ok_to_email` | Send, with a footer that meets `footer_text` | `True` | `False` |
|
|
44
|
+
| `consent_needed` | Do not send. Follow `next_step` | `False` | `True` |
|
|
45
|
+
| `blocked` | Do not send. Follow `next_step` | `False` | `True` |
|
|
46
|
+
| `unknown` | A fact could not be established. Treat as `consent_needed` unless you can supply the missing fact (for example `company_number` or `recipient_country`) and check again | `False` | `False` (see `needs_more_facts`) |
|
|
47
|
+
|
|
48
|
+
`footer_text` describes a requirement (who you are, a postal address, a way to opt out). It is not wording to paste.
|
|
49
|
+
|
|
50
|
+
`CheckResult` is a dataclass with typed fields (`verdict`, `confidence`, `reasons`, `rules_applied`, `footer_text`, `next_step`, `facts`, `receipt`, `usage`, `disclaimer`, `rules_version`) plus:
|
|
51
|
+
|
|
52
|
+
- `.raw`: the full response as a dict
|
|
53
|
+
- `.rule`: `rule_key` of the reason that decided the verdict
|
|
54
|
+
- `.reason`: that reason, with `text`, `source_title` and `source_url`
|
|
55
|
+
- `.notes`: the other reasons entries
|
|
56
|
+
- `.receipt_id` and `.receipt_url` (`https://precleared.dev/r/<id>`)
|
|
57
|
+
- `.summary()` and `.summary_text()`: the compact forms the framework tools return
|
|
58
|
+
|
|
59
|
+
### Receipts and health
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
receipt = client.verify_receipt("cc_rcpt_...") # also accepts a receipt URL
|
|
63
|
+
receipt.verdict, receipt.request_hash, receipt.response_hash, receipt.signature, receipt.public_key
|
|
64
|
+
|
|
65
|
+
client.health().ok # GET /api/v1/health: status, checked_at, api_ok, mcp_ok, worker_ok
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
A receipt proves what was asked and answered. The signature is Ed25519 over `request_hash.response_hash`; the receipt endpoint returns the public key alongside it, and `GET /api/v1/public-key` serves it too.
|
|
69
|
+
|
|
70
|
+
### Async
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from precleared import AsyncPrecleared
|
|
74
|
+
|
|
75
|
+
async with AsyncPrecleared("pc_live_...") as client:
|
|
76
|
+
result = await client.check("jane@acme.co.uk", address_source="bought")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Framework tools
|
|
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`.
|
|
82
|
+
|
|
83
|
+
Example tool output:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
verdict: consent_needed (confidence: medium)
|
|
87
|
+
rule: gb_individual_consent
|
|
88
|
+
reason: The recipient is a sole trader or a partnership in England, Wales or Northern Ireland. PECR treats them as individual subscribers, so unsolicited marketing email needs their prior consent, or the soft opt-in for existing customers. Source: PECR 2003 regulation 22(2) and ICO Guide to PECR, https://www.legislation.gov.uk/uksi/2003/2426/regulation/22
|
|
89
|
+
next_step: Do not cold email. Use a route they offer, such as their contact form, or obtain consent first.
|
|
90
|
+
receipt: cc_rcpt_E21TTJV8D9JAYTQZ https://precleared.dev/r/cc_rcpt_E21TTJV8D9JAYTQZ
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
API errors come back as text too (`error: daily_limit`, with `get_key_url`, `buy_url` or `retry_after` when the API gives one), so the agent can react instead of crashing.
|
|
94
|
+
|
|
95
|
+
### LangChain / LangGraph
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from langchain.agents import create_agent # pip install langchain
|
|
99
|
+
from precleared.langchain import precleared_tools
|
|
100
|
+
|
|
101
|
+
agent = create_agent(model, tools=precleared_tools(api_key="pc_live_..."))
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`precleared_tools()` returns two `StructuredTool`s with pydantic argument schemas, so they also work with `model.bind_tools(...)` and LangGraph's prebuilt agents. `precleared_check_tool()` and `precleared_verify_receipt_tool()` build them one at a time.
|
|
105
|
+
|
|
106
|
+
### CrewAI
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from crewai import Agent
|
|
110
|
+
from precleared.crewai import PreclearedCheckTool, PreclearedVerifyReceiptTool
|
|
111
|
+
|
|
112
|
+
sdr = Agent(role="SDR", goal="...", backstory="...", tools=[PreclearedCheckTool(api_key="pc_live_..."), PreclearedVerifyReceiptTool()])
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The key is held privately, so it does not appear in the tool's `repr()` or `model_dump()`.
|
|
116
|
+
|
|
117
|
+
### LlamaIndex
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from llama_index.core.agent.workflow import FunctionAgent
|
|
121
|
+
from precleared.llamaindex import precleared_tools
|
|
122
|
+
|
|
123
|
+
agent = FunctionAgent(tools=precleared_tools(), llm=llm)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### OpenAI Agents SDK
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from agents import Agent
|
|
130
|
+
from precleared.openai_agents import precleared_tools
|
|
131
|
+
|
|
132
|
+
agent = Agent(name="SDR", instructions="Check every address before the first email.", tools=precleared_tools())
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Errors
|
|
136
|
+
|
|
137
|
+
Everything raised after a request is sent is a subclass of `precleared.PreclearedError`, with `.message`, `.status_code`, `.code` and `.body`. Bad arguments raise a plain `ValueError` before anything is sent (for example an `address_source` outside the five allowed values).
|
|
138
|
+
|
|
139
|
+
| Class | When | Extra attributes |
|
|
140
|
+
| --- | --- | --- |
|
|
141
|
+
| `InvalidRequest` | 400, the body names the invalid field | |
|
|
142
|
+
| `InvalidKey` | 401, `invalid_key` or `revoked_key` | `.code` |
|
|
143
|
+
| `PaymentRequired` | 402, free checks used and no pack credits | `.buy_url` |
|
|
144
|
+
| `DailyLimit` | 429 `daily_limit`, the free daily allowance is used | `.get_key_url` |
|
|
145
|
+
| `RateLimited` | 429 `rate_limited`, more than 60 checks a minute | `.retry_after` (seconds, from the body or the `Retry-After` header) |
|
|
146
|
+
| `ReceiptNotFound` | 404 from the receipt endpoint | |
|
|
147
|
+
| `ServerError` | 5xx | |
|
|
148
|
+
| `NetworkError` | the request did not complete (DNS, connection, timeout) | `__cause__` is the httpx exception |
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from precleared import Precleared, DailyLimit, RateLimited
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
result = Precleared().check("jane@acme.co.uk")
|
|
155
|
+
except RateLimited as exc:
|
|
156
|
+
time.sleep(exc.retry_after or 1)
|
|
157
|
+
except DailyLimit as exc:
|
|
158
|
+
print("Get a free key:", exc.get_key_url)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The API key is never logged, printed or included in an exception.
|
|
162
|
+
|
|
163
|
+
## Allowances and limits
|
|
164
|
+
|
|
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.
|
|
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`.
|
|
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
|
+
|
|
171
|
+
## Configuration
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
Precleared(
|
|
175
|
+
api_key=None, # or the PRECLEARED_API_KEY environment variable
|
|
176
|
+
base_url="https://precleared.dev",
|
|
177
|
+
timeout=30, # seconds, or an httpx.Timeout
|
|
178
|
+
user_agent="precleared-python/0.1.0", # Precleared's hosting refuses requests without one
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Disclaimer
|
|
183
|
+
|
|
184
|
+
Decision support with sources, not legal advice. The receipt proves what was asked and answered, not that an email was sent.
|
|
185
|
+
|
|
186
|
+
## Support
|
|
187
|
+
|
|
188
|
+
support@precleared.dev. API reference: https://precleared.dev/docs and https://precleared.dev/openapi.json.
|
|
189
|
+
|
|
190
|
+
## Development
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
pip install -e . pytest anyio
|
|
194
|
+
pytest
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The tests mock all HTTP with `httpx.MockTransport` and never call the live API. The adapter tests run when the matching framework is installed and skip otherwise.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "precleared"
|
|
7
|
+
version = "0.1.0"
|
|
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
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
authors = [{ name = "Precleared", email = "support@precleared.dev" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"cold email",
|
|
16
|
+
"email compliance",
|
|
17
|
+
"PECR",
|
|
18
|
+
"GDPR",
|
|
19
|
+
"CAN-SPAM",
|
|
20
|
+
"CASL",
|
|
21
|
+
"ai agents",
|
|
22
|
+
"langchain",
|
|
23
|
+
"crewai",
|
|
24
|
+
"llamaindex",
|
|
25
|
+
"openai-agents",
|
|
26
|
+
]
|
|
27
|
+
classifiers = [
|
|
28
|
+
"Development Status :: 4 - Beta",
|
|
29
|
+
"Intended Audience :: Developers",
|
|
30
|
+
"Operating System :: OS Independent",
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
33
|
+
"Programming Language :: Python :: 3.9",
|
|
34
|
+
"Programming Language :: Python :: 3.10",
|
|
35
|
+
"Programming Language :: Python :: 3.11",
|
|
36
|
+
"Programming Language :: Python :: 3.12",
|
|
37
|
+
"Programming Language :: Python :: 3.13",
|
|
38
|
+
"Topic :: Communications :: Email",
|
|
39
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
40
|
+
"Typing :: Typed",
|
|
41
|
+
]
|
|
42
|
+
dependencies = ["httpx>=0.25.0"]
|
|
43
|
+
|
|
44
|
+
[project.optional-dependencies]
|
|
45
|
+
langchain = ["langchain-core>=0.3.0"]
|
|
46
|
+
crewai = ["crewai>=0.80.0"]
|
|
47
|
+
llamaindex = ["llama-index-core>=0.11.0"]
|
|
48
|
+
openai-agents = ["openai-agents>=0.0.3"]
|
|
49
|
+
all = [
|
|
50
|
+
"langchain-core>=0.3.0",
|
|
51
|
+
"crewai>=0.80.0",
|
|
52
|
+
"llama-index-core>=0.11.0",
|
|
53
|
+
"openai-agents>=0.0.3",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[project.urls]
|
|
57
|
+
Homepage = "https://precleared.dev"
|
|
58
|
+
Documentation = "https://precleared.dev/docs"
|
|
59
|
+
Repository = "https://github.com/gabrielaoimevbore-beep/precleared-python"
|
|
60
|
+
"API reference" = "https://precleared.dev/openapi.json"
|
|
61
|
+
Issues = "https://github.com/gabrielaoimevbore-beep/precleared-python/issues"
|
|
62
|
+
|
|
63
|
+
[dependency-groups]
|
|
64
|
+
dev = ["pytest>=7.0", "anyio>=3.0"]
|
|
65
|
+
|
|
66
|
+
[tool.hatch.build.targets.wheel]
|
|
67
|
+
packages = ["src/precleared"]
|
|
68
|
+
|
|
69
|
+
[tool.hatch.build.targets.sdist]
|
|
70
|
+
# Leading slashes anchor the patterns to the project root.
|
|
71
|
+
include = ["/src/precleared", "/tests", "/README.md", "/PUBLISHING.md", "/LICENSE", "/pyproject.toml"]
|
|
72
|
+
exclude = ["/.venv*", "**/__pycache__", "**/.pytest_cache"]
|
|
73
|
+
|
|
74
|
+
[tool.pytest.ini_options]
|
|
75
|
+
testpaths = ["tests"]
|
|
76
|
+
addopts = "-ra"
|
|
77
|
+
filterwarnings = ["error::DeprecationWarning:precleared.*"]
|