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.
@@ -0,0 +1,8 @@
1
+ .venv*/
2
+ dist/
3
+ build/
4
+ __pycache__/
5
+ *.pyc
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
@@ -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.*"]