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.
Files changed (30) hide show
  1. raain_guard-1.0.0/.gitignore +28 -0
  2. raain_guard-1.0.0/CHANGELOG.md +56 -0
  3. raain_guard-1.0.0/LICENSE +21 -0
  4. raain_guard-1.0.0/PKG-INFO +512 -0
  5. raain_guard-1.0.0/README.md +481 -0
  6. raain_guard-1.0.0/examples/quickstart.py +12 -0
  7. raain_guard-1.0.0/pyproject.toml +53 -0
  8. raain_guard-1.0.0/raain_guard/__init__.py +115 -0
  9. raain_guard-1.0.0/raain_guard/_version.py +3 -0
  10. raain_guard-1.0.0/raain_guard/client.py +295 -0
  11. raain_guard-1.0.0/raain_guard/errors.py +182 -0
  12. raain_guard-1.0.0/raain_guard/models.py +602 -0
  13. raain_guard-1.0.0/raain_guard/resources/__init__.py +19 -0
  14. raain_guard-1.0.0/raain_guard/resources/alerts.py +79 -0
  15. raain_guard-1.0.0/raain_guard/resources/findings.py +231 -0
  16. raain_guard-1.0.0/raain_guard/resources/ingest.py +510 -0
  17. raain_guard-1.0.0/raain_guard/resources/outputs.py +94 -0
  18. raain_guard-1.0.0/raain_guard/resources/query.py +313 -0
  19. raain_guard-1.0.0/raain_guard/resources/templates.py +41 -0
  20. raain_guard-1.0.0/raain_guard/resources/tenant_tokens.py +96 -0
  21. raain_guard-1.0.0/raain_guard/resources/webhooks.py +61 -0
  22. raain_guard-1.0.0/raain_guard/templates.py +42 -0
  23. raain_guard-1.0.0/tests/__init__.py +1 -0
  24. raain_guard-1.0.0/tests/test_client.py +854 -0
  25. raain_guard-1.0.0/tests/test_contract.py +346 -0
  26. raain_guard-1.0.0/tests/test_quickstart.py +34 -0
  27. raain_guard-1.0.0/tests/test_redirects.py +42 -0
  28. raain_guard-1.0.0/tests/test_retry_after.py +35 -0
  29. raain_guard-1.0.0/tests/test_templates_reports_outputs.py +131 -0
  30. 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
+ [![pypi](https://img.shields.io/pypi/v/raain-guard)](https://pypi.org/project/raain-guard/) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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.