raain-guard 1.0.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.
- raain_guard-1.0.0/.gitignore +28 -0
- raain_guard-1.0.0/CHANGELOG.md +56 -0
- raain_guard-1.0.0/LICENSE +21 -0
- raain_guard-1.0.0/PKG-INFO +512 -0
- raain_guard-1.0.0/README.md +481 -0
- raain_guard-1.0.0/examples/quickstart.py +12 -0
- raain_guard-1.0.0/pyproject.toml +53 -0
- raain_guard-1.0.0/raain_guard/__init__.py +115 -0
- raain_guard-1.0.0/raain_guard/_version.py +3 -0
- raain_guard-1.0.0/raain_guard/client.py +295 -0
- raain_guard-1.0.0/raain_guard/errors.py +182 -0
- raain_guard-1.0.0/raain_guard/models.py +602 -0
- raain_guard-1.0.0/raain_guard/resources/__init__.py +19 -0
- raain_guard-1.0.0/raain_guard/resources/alerts.py +79 -0
- raain_guard-1.0.0/raain_guard/resources/findings.py +231 -0
- raain_guard-1.0.0/raain_guard/resources/ingest.py +510 -0
- raain_guard-1.0.0/raain_guard/resources/outputs.py +94 -0
- raain_guard-1.0.0/raain_guard/resources/query.py +313 -0
- raain_guard-1.0.0/raain_guard/resources/templates.py +41 -0
- raain_guard-1.0.0/raain_guard/resources/tenant_tokens.py +96 -0
- raain_guard-1.0.0/raain_guard/resources/webhooks.py +61 -0
- raain_guard-1.0.0/raain_guard/templates.py +42 -0
- raain_guard-1.0.0/tests/__init__.py +1 -0
- raain_guard-1.0.0/tests/test_client.py +854 -0
- raain_guard-1.0.0/tests/test_contract.py +346 -0
- raain_guard-1.0.0/tests/test_quickstart.py +34 -0
- raain_guard-1.0.0/tests/test_redirects.py +42 -0
- raain_guard-1.0.0/tests/test_retry_after.py +35 -0
- raain_guard-1.0.0/tests/test_templates_reports_outputs.py +131 -0
- raain_guard-1.0.0/uv.lock +609 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Build and dependency artifacts for the eight SDK packages.
|
|
2
|
+
#
|
|
3
|
+
# There was no ignore file here until the Angular package was implemented,
|
|
4
|
+
# because nobody had run `npm install` or `tsc` inside this tree — so the first
|
|
5
|
+
# person to build one would have been offered `node_modules/` and `dist/` as
|
|
6
|
+
# untracked files to commit.
|
|
7
|
+
#
|
|
8
|
+
# Patterns are anchored to `src/<package>/` rather than left bare, following the
|
|
9
|
+
# rule the repo-root `.gitignore` records: `lib/` and `build/` are real source
|
|
10
|
+
# directory names here (`raain-guard-sdk-flutter/lib/` is the whole Flutter
|
|
11
|
+
# implementation), and an unanchored pattern silently untracks source.
|
|
12
|
+
|
|
13
|
+
node_modules/
|
|
14
|
+
|
|
15
|
+
src/*/dist/
|
|
16
|
+
src/*/build/
|
|
17
|
+
src/*/package-lock.json
|
|
18
|
+
|
|
19
|
+
# Python (raain-guard-sdk-python)
|
|
20
|
+
src/*/*.egg-info/
|
|
21
|
+
src/*/.pytest_cache/
|
|
22
|
+
|
|
23
|
+
# Java (raain-guard-sdk-java)
|
|
24
|
+
src/*/target/
|
|
25
|
+
|
|
26
|
+
# .NET (raain-guard-sdk-dotnet)
|
|
27
|
+
src/*/bin/
|
|
28
|
+
src/*/obj/
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this SDK are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the version
|
|
5
|
+
follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Every
|
|
6
|
+
RAAIN Guard SDK shares one version: the same number in any language speaks
|
|
7
|
+
the same API contract.
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [1.0.0] - 2026-11-07
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Every request names this SDK** — `X-RAAIN-SDK: python/<version>`, so the platform can see which SDK trains are still in use. `raain_guard.__version__` states the version.
|
|
16
|
+
|
|
17
|
+
### Security
|
|
18
|
+
|
|
19
|
+
- **Redirects are refused** — a `3xx` answer raises `RAAINGuardError` with its `status_code` instead of being treated as success; the clients never follow one, so the API key is not sent to another address.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Tenant tokens bound to a user** — `client.tenant_tokens.create(..., user_id=None, group_ids=None)` binds the end user your server authenticated, and their groups; the token then reads only the labeled documents that user may read. `TenantToken` gains `user_id` and `group_ids`.
|
|
24
|
+
- **Query templates** — `client.templates.list()` and `client.query.ask(Templates.X, parameters=None)` asks a ready-made question through the one query path (async: `alist`, `aask`). New models `QueryTemplate`, `TemplateParameter`, `TemplateReference`.
|
|
25
|
+
- **Output validation** — `client.outputs.validate(text=... | document=..., policies=None, tenant_id=None)` (async: `avalidate`) returns an `OutputValidation`. New models `OutputValidation`, `ValidationFinding`, `PolicyReference`.
|
|
26
|
+
|
|
27
|
+
- **Upload validation** — `client.ingest.validate(bundle, guard, content, filename=None, content_type=None)` (and `avalidate`) checks a delimited file against a guard's canonical schema, with the project's column mapping applied, and writes nothing. New model `UploadValidation`.
|
|
28
|
+
- **Findings** — `client.findings.get(finding_id, tenant_id=None)` reads one finding with its evidence (`query:read`, works under a tenant token); `lifecycle(finding_id)` (async: `alifecycle`) returns a `FindingLifecycle` with every `DecisionEntry` and `OutcomeEntry`. `record_decision()` and `record_outcome()` record what was decided and what happened (`feedback:write`, server-side only). Async: `aget`, `arecord_decision`, `arecord_outcome`. New models `Finding`, `DecisionRecorded`, `OutcomeRecorded` and the `Decision` / `Outcome` literals.
|
|
29
|
+
|
|
30
|
+
- **Repeated questions** — `ask`, `aask`, `stream` and `astream` take `cache`, `cache_max_age_seconds` and `idempotency_key` (sent as `Idempotency-Key`); `QueryResult` carries `cache_status` and `cached_at`.
|
|
31
|
+
|
|
32
|
+
- **Tenant purge** — `client.ingest.purge_tenant()` / `apurge_tenant()` erases one tenant's canonical records, vectors in every bundle and stored documents. Irreversible, idempotent; needs `ingest:write`.
|
|
33
|
+
- **Tenant tokens** — `client.tenant_tokens.create()` mints a short-lived credential bound to one end customer, for browser and app code. Server-side only.
|
|
34
|
+
- **Ingestion jobs** — `client.ingest.job()`, `records()` and `replay()`: one job's state; what it stored, with each pipeline stage's outcome and why it stored nothing; and a retry from the stored content.
|
|
35
|
+
- Async counterparts: `ajob`, `arecords`, `areplay`, `tenant_tokens.acreate`.
|
|
36
|
+
|
|
37
|
+
- validate outputs before partners show or send them (`de8e244af`)
|
|
38
|
+
- generate partner reports over findings as JSON, CSV, XLSX or PDF (`ad32b927d`)
|
|
39
|
+
- add typed, versioned query templates (`bba204829`)
|
|
40
|
+
- expose templates, reports and output validation in TypeScript and Python (`ddeee94ac`)
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- registry pages, credentials preflight and release credential docs (`f983a9ddb`)
|
|
45
|
+
- release trains — one MAJOR.MINOR for all eight SDKs, a patch per SDK (`3f18a297d`)
|
|
46
|
+
- one tested quickstart per SDK, shown by its README, its docs page and the site (`fa7795105`)
|
|
47
|
+
- every SDK names itself and its version; the platform counts trains in use (`bae0a1bbf`)
|
|
48
|
+
- streaming NDJSON ingestion is a partner endpoint (`a1d7fb782`)
|
|
49
|
+
- ingest.stream sends an NDJSON batch in every server SDK (`11f513da0`)
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- keep the service prefix on redirects; point SDK direct-port examples at :8020 (`564402d45`)
|
|
54
|
+
- refuse redirects and read an HTTP-date Retry-After in every SDK (T-0040) (`c1a11a52c`)
|
|
55
|
+
- every link a package publishes is public — raainguard.com or its registry (`4f7fefaed`)
|
|
56
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RAAIN Tech Pvt. Ltd.
|
|
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,512 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: raain-guard
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK for RAAIN Guard — embedded B2B intelligence platform
|
|
5
|
+
Project-URL: Homepage, https://raainguard.com
|
|
6
|
+
Project-URL: Documentation, https://raainguard.com/build/sdks/python
|
|
7
|
+
Project-URL: Support, https://raainguard.com/contact
|
|
8
|
+
Project-URL: Changelog, https://raainguard.com/build/sdks/python/changelog
|
|
9
|
+
Author-email: RAAIN Guard <contact.raaintech@gmail.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: b2b,guard,intelligence,raain,sdk
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: httpx<1,>=0.28
|
|
24
|
+
Requires-Dist: pydantic<3,>=2.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: anyio[trio]; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# RAAIN Guard Python SDK
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/raain-guard/) [](https://raainguard.com/build/sdks/python)
|
|
35
|
+
|
|
36
|
+
Official Python SDK for [RAAIN Guard](https://raainguard.com) — the B2B
|
|
37
|
+
intelligence platform.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Table of contents
|
|
42
|
+
|
|
43
|
+
- [Installation](#installation)
|
|
44
|
+
- [Quickstart](#quickstart)
|
|
45
|
+
- [Tenants](#tenants)
|
|
46
|
+
- [Where requests go](#where-requests-go)
|
|
47
|
+
- [Configuration](#configuration)
|
|
48
|
+
- [Capabilities](#capabilities)
|
|
49
|
+
- [Sync vs async usage](#sync-vs-async-usage)
|
|
50
|
+
- [Error handling](#error-handling)
|
|
51
|
+
- [Running the tests](#running-the-tests)
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install raain-guard
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Requires Python 3.10 or later. The only runtime dependencies are
|
|
62
|
+
[httpx](https://www.python-httpx.org/) and [pydantic v2](https://docs.pydantic.dev/).
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Quickstart
|
|
67
|
+
|
|
68
|
+
<!-- generated:quickstart -->
|
|
69
|
+
```python
|
|
70
|
+
from raain_guard import RAAINGuardClient
|
|
71
|
+
|
|
72
|
+
# Server-side only: the secret key is read from RAAIN_GUARD_API_KEY.
|
|
73
|
+
client = RAAINGuardClient(tenant_id="customer-42")
|
|
74
|
+
|
|
75
|
+
answer = client.query.ask("Which invoices are overdue?")
|
|
76
|
+
print(answer.message)
|
|
77
|
+
print(f"answered by {answer.guard}")
|
|
78
|
+
```
|
|
79
|
+
<!-- /generated:quickstart -->
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Tenants
|
|
84
|
+
|
|
85
|
+
Every query and every ingest is scoped to one **tenant** — your end-customer.
|
|
86
|
+
It is required: give the client a default `tenant_id`, or pass one per call to
|
|
87
|
+
act for a different customer.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
# A default for the client
|
|
91
|
+
client = RAAINGuardClient(tenant_id="customer-42")
|
|
92
|
+
|
|
93
|
+
# Or per call
|
|
94
|
+
client.query.ask("…", tenant_id="customer-7")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Asking with no tenant raises `ValueError` before any request is sent, rather
|
|
98
|
+
than letting the platform answer with a 422 naming a field you never typed.
|
|
99
|
+
|
|
100
|
+
Your own **project** is not something you send. The platform resolves it from
|
|
101
|
+
the API key, so there is no `project_id` parameter anywhere in this SDK.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Where requests go
|
|
106
|
+
|
|
107
|
+
The platform runs two services, published under a prefix each:
|
|
108
|
+
|
|
109
|
+
| Prefix | Serves |
|
|
110
|
+
| ---------------- | ----------------- |
|
|
111
|
+
| `/runtime` | `query`, `alerts`, `findings`, `tenant_tokens` |
|
|
112
|
+
| `/ingestion` | `ingest` |
|
|
113
|
+
|
|
114
|
+
`base_url` is the host and the SDK adds the prefix. Point a service somewhere
|
|
115
|
+
else when you are running it directly on its own port (the dev stack: runtime `:8020`, ingestion `:8011`; the dev gateway at `http://localhost:8000` needs only `base_url`):
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
client = RAAINGuardClient(
|
|
119
|
+
runtime_url="http://localhost:8020",
|
|
120
|
+
ingestion_url="http://localhost:8011",
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Configuration
|
|
127
|
+
|
|
128
|
+
### API key
|
|
129
|
+
|
|
130
|
+
The client reads the key from `RAAIN_GUARD_API_KEY` when `api_key` is not
|
|
131
|
+
supplied:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
export RAAIN_GUARD_API_KEY="rg_live_sk_..."
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
client = RAAINGuardClient() # key read from env
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
If neither is set the constructor raises `AuthenticationError` immediately — no
|
|
142
|
+
lazy key lookup.
|
|
143
|
+
|
|
144
|
+
**A key carries the scopes it was issued with, and nothing more.** Issue one
|
|
145
|
+
with only what the surface needs — `query:read` for something that only asks
|
|
146
|
+
questions — and it can do only that. Scopes are enforced by the platform on
|
|
147
|
+
every request.
|
|
148
|
+
|
|
149
|
+
**Keys stay on your server.** Browser and app code holds a **tenant token** instead: mint one per end customer with `client.tenant_tokens.create(tenant_id)` and hand it to the page from an endpoint your own session protects. It is bound to that one tenant, carries `query:read`, plus `alerts:read` for that tenant's alert feed when your server asks for it, expires within an hour, and dies with the key that minted it. Publishable keys (`rg_live_pk_`) are retired and refused.
|
|
150
|
+
|
|
151
|
+
> **A key in client-side code is readable by anyone who can open it**, and a
|
|
152
|
+
> key lets its holder choose the tenant. Never ship one; mint tenant tokens.
|
|
153
|
+
|
|
154
|
+
### Constructor options
|
|
155
|
+
|
|
156
|
+
| Parameter | Default | Description |
|
|
157
|
+
| --------------- | ------------------------- | ----------------------------------------------- |
|
|
158
|
+
| `api_key` | `RAAIN_GUARD_API_KEY` env | Partner API key |
|
|
159
|
+
| `base_url` | `https://api.raainguard.com` | Host serving the platform |
|
|
160
|
+
| `runtime_url` | — | Full base URL for guard-runtime |
|
|
161
|
+
| `ingestion_url` | — | Full base URL for guard-ingestion |
|
|
162
|
+
| `tenant_id` | — | Default end-customer for every call |
|
|
163
|
+
| `timeout` | `30.0` | Request timeout in seconds |
|
|
164
|
+
|
|
165
|
+
### Headers sent automatically
|
|
166
|
+
|
|
167
|
+
| Header | Value |
|
|
168
|
+
| --------------------- | ---------------------------------- |
|
|
169
|
+
| `X-API-Key` | Your API key |
|
|
170
|
+
| `X-RAAIN-API-Version` | `2026-10-11` |
|
|
171
|
+
| `X-Tenant-Id` | The tenant this call is acting for |
|
|
172
|
+
| `X-RAAIN-SDK` | `python/<this version>` |
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Capabilities
|
|
177
|
+
|
|
178
|
+
### Ingest
|
|
179
|
+
|
|
180
|
+
Ingest a document's text so a guard can reason over it. The content is chunked,
|
|
181
|
+
embedded and stored against the end-customer.
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
job = client.ingest.document(
|
|
185
|
+
bundle="finance",
|
|
186
|
+
guard="cash",
|
|
187
|
+
doc_id="stmt-2026-01",
|
|
188
|
+
content=statement_text,
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`doc_id` is your own identifier: re-ingesting the same id replaces the previous
|
|
193
|
+
content rather than duplicating it.
|
|
194
|
+
|
|
195
|
+
Upload files instead:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
job = client.ingest.upload(
|
|
199
|
+
"finance",
|
|
200
|
+
"cash",
|
|
201
|
+
["reports/january.csv", "reports/february.csv"],
|
|
202
|
+
)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Sizes, counts and accepted extensions are enforced by the platform and
|
|
206
|
+
published — read them rather than hard-coding them, because they are
|
|
207
|
+
configuration and the server rejects anything over its own ceiling regardless of
|
|
208
|
+
what a client believes:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
limits = client.ingest.limits()
|
|
212
|
+
print(limits.max_files, limits.allowed_extensions)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
To check a CSV before sending it — which record types it reaches, missing
|
|
216
|
+
required columns, headers nothing reads — with the project's column mapping
|
|
217
|
+
applied and nothing written:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
report = client.ingest.validate("inventory", "stock_accuracy", csv_text)
|
|
221
|
+
if not report.writable:
|
|
222
|
+
print(report.skipped_targets, report.unmapped_headers)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
And to see what is in flight:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
page = client.ingest.status(bundle="finance", page=1, page_size=20)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
A batch of documents in one request, as NDJSON (Enterprise). Each line gets its own result, so one
|
|
232
|
+
refused line does not fail the rest:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from raain_guard import StreamDocument
|
|
236
|
+
|
|
237
|
+
results = client.ingest.stream("finance", [
|
|
238
|
+
StreamDocument(guard="cash", doc_id="txn-001", content=text_1),
|
|
239
|
+
StreamDocument(guard="cash", doc_id="txn-002", content=text_2),
|
|
240
|
+
])
|
|
241
|
+
refused = [r for r in results if not r.accepted]
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Async variants: `adocument`, `aupload`, `astream`, `alimits`.
|
|
245
|
+
|
|
246
|
+
### Query
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
# Non-streaming (sync)
|
|
250
|
+
answer = client.query.ask("Which invoices are overdue?")
|
|
251
|
+
print(answer.message, answer.bundle, answer.guard)
|
|
252
|
+
|
|
253
|
+
# Address a guard directly, bypassing intent classification
|
|
254
|
+
answer = client.query.ask(
|
|
255
|
+
"What should we reorder?",
|
|
256
|
+
bundle_hint="inventory",
|
|
257
|
+
guard="reorder",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Continue a conversation
|
|
261
|
+
follow_up = client.query.ask("Which of those is largest?", session_id=answer.session_id)
|
|
262
|
+
|
|
263
|
+
# Streaming (sync) — yields the answer text as it arrives, and ends with the answer.
|
|
264
|
+
# Use ask() when you need the structured result (guard, grounded, finding_id).
|
|
265
|
+
for chunk in client.query.stream("What changed this week?"):
|
|
266
|
+
print(chunk, end="", flush=True)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Pair `guard` with `bundle_hint` when the guard name appears in more than one
|
|
270
|
+
bundle — `accuracy`, `compliance`, `fraud`, `analytics` and `automation` all do.
|
|
271
|
+
|
|
272
|
+
Async variants: `aask`, `astream`.
|
|
273
|
+
|
|
274
|
+
The response carries more than the fields this SDK models — confidence
|
|
275
|
+
metadata, metric trajectories, cross-domain signals. Those
|
|
276
|
+
are documented in the platform docs and reachable on the model, which permits
|
|
277
|
+
extra fields rather than rejecting an answer it does not fully mirror.
|
|
278
|
+
|
|
279
|
+
### Alerts
|
|
280
|
+
|
|
281
|
+
The end-customer's prioritized proactive alert feed, generated per request from
|
|
282
|
+
the findings your enabled guards have produced and ranked by impact and
|
|
283
|
+
severity.
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
feed = client.alerts.feed(max_alerts=10)
|
|
287
|
+
|
|
288
|
+
for alert in feed.alerts:
|
|
289
|
+
print(alert.severity, alert.title, alert.total_impact_amount, alert.impact_currency)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
`max_alerts` caps the feed at 1–100. The Free plan is capped further
|
|
293
|
+
server-side, so a feed may be shorter than you asked for.
|
|
294
|
+
|
|
295
|
+
There is no cursor and nothing to acknowledge: the feed is computed, not a
|
|
296
|
+
stored list you page through and mark off.
|
|
297
|
+
|
|
298
|
+
`total_impact_amount` is a decimal-safe **string**, and `impact_currency` is its
|
|
299
|
+
ISO-4217 currency code — the tenant's currency.
|
|
300
|
+
|
|
301
|
+
Async variant: `afeed`.
|
|
302
|
+
|
|
303
|
+
### Findings
|
|
304
|
+
|
|
305
|
+
Read one finding with the evidence it was reasoned from — the id comes from a
|
|
306
|
+
query result, an alert's `finding_ids`, or a `finding.created` webhook — and
|
|
307
|
+
close the loop on it: what a person decided, then what happened.
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
finding = client.findings.get(feed.alerts[0].finding_ids[0])
|
|
311
|
+
print(finding.severity, finding.impact_amount, finding.evidence)
|
|
312
|
+
|
|
313
|
+
# Server-side only: both need `feedback:write`, which a tenant token lacks.
|
|
314
|
+
decided = client.findings.record_decision(
|
|
315
|
+
finding.finding_id, user_id="u-17", decision="acted", action_taken="Held the payment"
|
|
316
|
+
)
|
|
317
|
+
client.findings.record_outcome(
|
|
318
|
+
finding.finding_id,
|
|
319
|
+
decision_id=decided.decision_id,
|
|
320
|
+
outcome="confirmed",
|
|
321
|
+
observation_window_days=30,
|
|
322
|
+
actual_impact_amount=Decimal("3450.00"),
|
|
323
|
+
)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
`get` needs `query:read`, so it works under a tenant token, which always reads
|
|
327
|
+
its own tenant; `tenant_id=` restricts a secret-key read to one tenant. A
|
|
328
|
+
missing finding raises `NotFoundError`. Decisions are `acted`, `dismissed`,
|
|
329
|
+
`escalated` or `deferred`; outcomes are `confirmed`, `false_positive`,
|
|
330
|
+
`avoided`, `worsened` or `inconclusive`. Both are append-only. Amounts are
|
|
331
|
+
decimal-safe strings; pass a `Decimal` or a string, never a float.
|
|
332
|
+
|
|
333
|
+
Async variants: `aget`, `arecord_decision`, `arecord_outcome`.
|
|
334
|
+
|
|
335
|
+
### Webhooks
|
|
336
|
+
|
|
337
|
+
Verify HMAC-SHA256 signatures on incoming deliveries. A pure cryptographic
|
|
338
|
+
utility — no HTTP requests are made.
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
import json
|
|
342
|
+
import os
|
|
343
|
+
|
|
344
|
+
from raain_guard import RAAINGuardClient
|
|
345
|
+
|
|
346
|
+
client = RAAINGuardClient()
|
|
347
|
+
|
|
348
|
+
# In your webhook handler (framework-agnostic):
|
|
349
|
+
raw_body: bytes = request.body # raw bytes, do NOT decode
|
|
350
|
+
signature: str = request.headers["X-Webhook-Signature"]
|
|
351
|
+
secret: str = os.environ["RAAIN_GUARD_WEBHOOK_SECRET"]
|
|
352
|
+
|
|
353
|
+
if not client.webhooks.verify(raw_body, signature, secret):
|
|
354
|
+
return 400, "Invalid signature"
|
|
355
|
+
|
|
356
|
+
event = json.loads(raw_body)
|
|
357
|
+
print(event["event_type"]) # "query.completed"
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
The `X-Webhook-Signature` header format is `sha256=<hex_digest>`, computed over
|
|
361
|
+
the raw body exactly as delivered — a re-serialized JSON object almost never
|
|
362
|
+
reproduces the bytes that were signed.
|
|
363
|
+
|
|
364
|
+
Delivered events are `query.completed`, `safety.violation` and `fraud.alert`.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Sync vs async usage
|
|
369
|
+
|
|
370
|
+
The same `RAAINGuardClient` supports both. The underlying HTTP clients are
|
|
371
|
+
created lazily and held for the lifetime of the instance.
|
|
372
|
+
|
|
373
|
+
### Sync
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
from raain_guard import RAAINGuardClient
|
|
377
|
+
|
|
378
|
+
# As a plain object (lives until garbage-collected)
|
|
379
|
+
client = RAAINGuardClient(api_key="rg_live_sk_...", tenant_id="customer-42")
|
|
380
|
+
answer = client.query.ask("What is our cash runway?")
|
|
381
|
+
|
|
382
|
+
# As a context manager (recommended — ensures connection cleanup)
|
|
383
|
+
with RAAINGuardClient(tenant_id="customer-42") as client:
|
|
384
|
+
answer = client.query.ask("What is our cash runway?")
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Async
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
import asyncio
|
|
391
|
+
|
|
392
|
+
from raain_guard import RAAINGuardClient
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
async def main() -> None:
|
|
396
|
+
async with RAAINGuardClient(tenant_id="customer-42") as client:
|
|
397
|
+
job = await client.ingest.adocument("finance", "cash", "stmt-1", text)
|
|
398
|
+
answer = await client.query.aask("What is the cash balance?")
|
|
399
|
+
print(answer.message)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
asyncio.run(main())
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
All async methods are prefixed with `a` — `aask`, `adocument`, `afeed`.
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Error handling
|
|
410
|
+
|
|
411
|
+
All SDK exceptions inherit from `RAAINGuardError`.
|
|
412
|
+
|
|
413
|
+
```python
|
|
414
|
+
import time
|
|
415
|
+
|
|
416
|
+
from raain_guard import (
|
|
417
|
+
RAAINGuardClient,
|
|
418
|
+
RAAINGuardError,
|
|
419
|
+
AuthenticationError,
|
|
420
|
+
PermissionError,
|
|
421
|
+
NotFoundError,
|
|
422
|
+
InvalidRequestError,
|
|
423
|
+
RateLimitError,
|
|
424
|
+
APIError,
|
|
425
|
+
NetworkError,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
client = RAAINGuardClient(tenant_id="customer-42")
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
answer = client.query.ask("...")
|
|
432
|
+
|
|
433
|
+
except AuthenticationError:
|
|
434
|
+
# API key missing or rejected (HTTP 401)
|
|
435
|
+
print("Check your RAAIN_GUARD_API_KEY")
|
|
436
|
+
|
|
437
|
+
except PermissionError:
|
|
438
|
+
# The key does not carry the scope this call needs (HTTP 403)
|
|
439
|
+
print("Insufficient scope")
|
|
440
|
+
|
|
441
|
+
except NotFoundError:
|
|
442
|
+
# HTTP 404
|
|
443
|
+
print("Resource not found")
|
|
444
|
+
|
|
445
|
+
except InvalidRequestError as exc:
|
|
446
|
+
# The platform rejected the request (HTTP 400 or 422). `errors` carries
|
|
447
|
+
# the per-field detail, and the message names the offending field.
|
|
448
|
+
print(exc)
|
|
449
|
+
for err in exc.errors:
|
|
450
|
+
print(f" {err}")
|
|
451
|
+
|
|
452
|
+
except RateLimitError as exc:
|
|
453
|
+
# HTTP 429
|
|
454
|
+
print(f"Rate limited — retry after {exc.retry_after}s")
|
|
455
|
+
time.sleep(exc.retry_after)
|
|
456
|
+
|
|
457
|
+
except APIError:
|
|
458
|
+
# HTTP 5xx
|
|
459
|
+
print("RAAIN Guard server error — try again later")
|
|
460
|
+
|
|
461
|
+
except NetworkError:
|
|
462
|
+
# Connection-level failure
|
|
463
|
+
print("Could not reach the platform — check connectivity")
|
|
464
|
+
|
|
465
|
+
except RAAINGuardError:
|
|
466
|
+
raise
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Every exception exposes `.status_code` (`None` for `NetworkError`, and for an
|
|
470
|
+
`AuthenticationError` raised before any request).
|
|
471
|
+
|
|
472
|
+
Asking or ingesting without a tenant raises `ValueError` rather than
|
|
473
|
+
`RAAINGuardError`: it is a mistake in the call, caught before anything is sent.
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## Running the tests
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
pip install -e ".[dev]"
|
|
481
|
+
pytest
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
The suite uses [respx](https://lundberg.github.io/respx/) to mock the httpx
|
|
485
|
+
transport — no real network calls are made. `tests/test_contract.py` checks this
|
|
486
|
+
SDK against `raain-guard-sdk/CONTRACT.json`, which is generated from the
|
|
487
|
+
platform's own OpenAPI, so a path or field that drifts from what the platform
|
|
488
|
+
serves fails the build.
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## Documentation and support
|
|
493
|
+
|
|
494
|
+
<!-- generated:links -->
|
|
495
|
+
| | |
|
|
496
|
+
| --- | --- |
|
|
497
|
+
| Guide for this SDK | [raainguard.com/build/sdks/python](https://raainguard.com/build/sdks/python) |
|
|
498
|
+
| API reference | [raainguard.com/sdk-reference/python](https://raainguard.com/sdk-reference/python/index.html) |
|
|
499
|
+
| Changelog | [raainguard.com/build/sdks/python/changelog](https://raainguard.com/build/sdks/python/changelog) |
|
|
500
|
+
| This package on its registry | [pypi.org/project/raain-guard/](https://pypi.org/project/raain-guard/) |
|
|
501
|
+
| HTTP API reference | [api.raainguard.com/reference/](https://api.raainguard.com/reference/) |
|
|
502
|
+
| Bugs and questions | [raainguard.com/contact](https://raainguard.com/contact) |
|
|
503
|
+
<!-- /generated:links -->
|
|
504
|
+
|
|
505
|
+
This package is on SDK train **1.0**. Every RAAIN Guard SDK on the same `MAJOR.MINOR` train has the
|
|
506
|
+
same capabilities and pins the same API version, in every language; the patch number is this
|
|
507
|
+
package's own fixes. A breaking change is a new major train and is listed first in the changelog.
|
|
508
|
+
The platform supports the current and the previous train.
|
|
509
|
+
|
|
510
|
+
## License
|
|
511
|
+
|
|
512
|
+
MIT — see the `LICENSE` file in this package for details.
|