ragnarok-api 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.
- ragnarok_api-0.1.0/.gitignore +22 -0
- ragnarok_api-0.1.0/LICENSE +21 -0
- ragnarok_api-0.1.0/PKG-INFO +361 -0
- ragnarok_api-0.1.0/README.md +317 -0
- ragnarok_api-0.1.0/pyproject.toml +50 -0
- ragnarok_api-0.1.0/src/ragnarok_api/__init__.py +223 -0
- ragnarok_api-0.1.0/src/ragnarok_api/_base.py +178 -0
- ragnarok_api-0.1.0/src/ragnarok_api/_bridge.py +63 -0
- ragnarok_api-0.1.0/src/ragnarok_api/_core.py +287 -0
- ragnarok_api-0.1.0/src/ragnarok_api/_routes.py +505 -0
- ragnarok_api-0.1.0/src/ragnarok_api/_upload.py +700 -0
- ragnarok_api-0.1.0/src/ragnarok_api/_version.py +14 -0
- ragnarok_api-0.1.0/src/ragnarok_api/accounting.py +112 -0
- ragnarok_api-0.1.0/src/ragnarok_api/async_client.py +629 -0
- ragnarok_api-0.1.0/src/ragnarok_api/client.py +778 -0
- ragnarok_api-0.1.0/src/ragnarok_api/credentials.py +184 -0
- ragnarok_api-0.1.0/src/ragnarok_api/errors.py +387 -0
- ragnarok_api-0.1.0/src/ragnarok_api/events.py +516 -0
- ragnarok_api-0.1.0/src/ragnarok_api/models.py +717 -0
- ragnarok_api-0.1.0/src/ragnarok_api/paging.py +71 -0
- ragnarok_api-0.1.0/src/ragnarok_api/py.typed +0 -0
- ragnarok_api-0.1.0/src/ragnarok_api/sse.py +127 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Local dependencies, builds and credentials
|
|
2
|
+
.venv/
|
|
3
|
+
.venv-*/
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.py[cod]
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
.ruff_cache/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
node_modules
|
|
10
|
+
/target/
|
|
11
|
+
target/
|
|
12
|
+
dist/
|
|
13
|
+
build/
|
|
14
|
+
build-test/
|
|
15
|
+
.env
|
|
16
|
+
.env.*
|
|
17
|
+
!.env.example
|
|
18
|
+
*.token
|
|
19
|
+
*.tokens
|
|
20
|
+
*.pem
|
|
21
|
+
untracked/
|
|
22
|
+
.DS_Store
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Laurent-Philippe Albou
|
|
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,361 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ragnarok-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the RAGnarok HTTP API (/api/v1): auth, streaming queries, ingest, graph and sources.
|
|
5
|
+
Author: Laurent-Philippe Albou
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Laurent-Philippe Albou
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Keywords: graphrag,knowledge-graph,rag,ragnarok,sdk,sse
|
|
29
|
+
Classifier: Development Status :: 3 - Alpha
|
|
30
|
+
Classifier: Intended Audience :: Developers
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Programming Language :: Python :: 3
|
|
33
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
37
|
+
Classifier: Typing :: Typed
|
|
38
|
+
Requires-Python: >=3.10
|
|
39
|
+
Requires-Dist: httpx>=0.27
|
|
40
|
+
Requires-Dist: pydantic>=2
|
|
41
|
+
Provides-Extra: test
|
|
42
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# ragnarok-api (Python)
|
|
46
|
+
|
|
47
|
+
The Python SDK for the RAGnarok HTTP API (`/api/v1`). It is a thin layer over the
|
|
48
|
+
published contract (`specs/openapi.json` in this repository): typed
|
|
49
|
+
models, the SSE event stream of `/query` and of ingest jobs, multipart upload with
|
|
50
|
+
progress, binary downloads, typed errors and cursor paging. It holds no business
|
|
51
|
+
logic. Caps, roles, concurrency and model allow-lists are enforced by the server
|
|
52
|
+
and come back to you as typed errors.
|
|
53
|
+
|
|
54
|
+
- Runtime dependencies: `httpx>=0.27`, `pydantic>=2`. Python 3.10+.
|
|
55
|
+
- Sync `Client` and async `AsyncClient` with the same surface.
|
|
56
|
+
- Versioning: the SDK major follows the API major (`1.x` speaks `/api/v1`; `0.x` is the
|
|
57
|
+
pre-release line for v1). `client.check_api_version()` refuses a server whose
|
|
58
|
+
`/api/health` reports another major.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install -e ./sdks/python
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Connect
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from ragnarok_api import Client, Token
|
|
68
|
+
|
|
69
|
+
client = Client("https://ragnarok.example", Token("rgk_...")) # personal API token
|
|
70
|
+
print(client.health().status, client.check_api_version().api_version)
|
|
71
|
+
print(client.auth.whoami().email)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`api_prefix` defaults to `/api/v1`. Pass `api_prefix="/api"` for a server that
|
|
75
|
+
predates the versioned mount. `timeout` defaults to 240 s. SSE streams use
|
|
76
|
+
a 480 s read backstop (`stream_read_timeout`). Any httpx transport works, including
|
|
77
|
+
the in-process `httpx.ASGITransport(app=app)`.
|
|
78
|
+
|
|
79
|
+
## 1. Query with streaming
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from ragnarok_api import AnswerEvent, DoneEvent, ErrorEvent, SourcesEvent, StatusEvent
|
|
83
|
+
|
|
84
|
+
for event in client.query.stream("What is the approved indication?", workspace="epar", mode="quick"):
|
|
85
|
+
if isinstance(event, StatusEvent):
|
|
86
|
+
print("..", event.message)
|
|
87
|
+
elif isinstance(event, AnswerEvent):
|
|
88
|
+
print(event.text)
|
|
89
|
+
elif isinstance(event, SourcesEvent):
|
|
90
|
+
for src in event.vector_sources:
|
|
91
|
+
print(f" [{src.citation_label}] {src.chunk_id}")
|
|
92
|
+
elif isinstance(event, ErrorEvent):
|
|
93
|
+
print("error:", event.code, event.message)
|
|
94
|
+
elif isinstance(event, DoneEvent):
|
|
95
|
+
print("done, success =", event.success)
|
|
96
|
+
|
|
97
|
+
# Or collect everything (raises QueryFailed on an `error` event):
|
|
98
|
+
result = client.query.ask("What is the approved indication?", workspace="epar")
|
|
99
|
+
print(result.answer, len(result.sources.vector_sources), result.stats.elapsed_ms)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Events: `status, answer, answer_delta, citation_map, references, notices, sources,
|
|
103
|
+
entities, graph, stats, error, done`. An event the SDK cannot type (a new event name,
|
|
104
|
+
or data that is not JSON) arrives as `UnknownEvent`. It is never dropped. If the
|
|
105
|
+
stream ends without `done`, the SDK yields a client-generated `error`
|
|
106
|
+
(`code="query_network_error"`) and then `done(success=False)`, as the web UI does.
|
|
107
|
+
|
|
108
|
+
The server records every completed answer in the query history, shared by the web UI,
|
|
109
|
+
the `ragnarok` app and the SDKs: `DoneEvent.history_id` (and `result.history_id` from
|
|
110
|
+
`ask`) names the entry; it is `None` with `history_error` set when recording failed.
|
|
111
|
+
Pass `record_history=False` to keep a query out of the history (evaluations, scripted
|
|
112
|
+
batches), or `history_id="..."` to record under your own id (a retry with the same id
|
|
113
|
+
updates the entry, never duplicates it).
|
|
114
|
+
|
|
115
|
+
Async:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
import asyncio
|
|
119
|
+
from ragnarok_api import AsyncClient, Session
|
|
120
|
+
|
|
121
|
+
async def main():
|
|
122
|
+
async with AsyncClient("https://ragnarok.example") as client:
|
|
123
|
+
await Session.alogin(client, "me@example.com", "secret")
|
|
124
|
+
async for event in client.query.stream("Summarise the safety profile", workspace="epar"):
|
|
125
|
+
print(event.event)
|
|
126
|
+
|
|
127
|
+
asyncio.run(main())
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## 2. Ingest a folder
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from ragnarok_api import collect_folder
|
|
134
|
+
|
|
135
|
+
folder = collect_folder("./dossiers") # .pdf .txt .md .rmd, relative upload names
|
|
136
|
+
print(len(folder), "files;", len(folder.skipped), "skipped:", folder.skipped)
|
|
137
|
+
|
|
138
|
+
estimate = client.ingest.estimate(folder, workspace="my-workspace", level="BALANCED")
|
|
139
|
+
print(estimate.full_ingest["totals"])
|
|
140
|
+
|
|
141
|
+
def progress(sent: int, total: int | None) -> None:
|
|
142
|
+
print(f"\rupload {sent}/{total or '?'} bytes", end="")
|
|
143
|
+
|
|
144
|
+
job = client.ingest.submit(folder, workspace="my-workspace", level="BALANCED",
|
|
145
|
+
run_postprocess=True, doc_workers=3, progress=progress).job
|
|
146
|
+
for ev in client.ingest.jobs.events(job.id): # replays history, then tails until done
|
|
147
|
+
print(ev.event, ev.progress, ev.message)
|
|
148
|
+
print(client.ingest.jobs.get(job.id).status)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Uploads are sent as `multipart/form-data` with the same field names the web client
|
|
152
|
+
uses: `workspace` and a repeated `files`, plus `level`, `run_postprocess`,
|
|
153
|
+
`doc_workers`, `document_prefix` and the rest of the documented options. An option
|
|
154
|
+
name that the route does not accept raises `TypeError` instead of being ignored.
|
|
155
|
+
Ingest requires the `data_manager` role on the workspace (or `admin`).
|
|
156
|
+
|
|
157
|
+
Concurrency of a run: `doc_workers` (documents in parallel, 1-64), `llm_concurrency` and
|
|
158
|
+
`vlm_concurrency` (calls per document, 1-1000); without them the server uses the Settings
|
|
159
|
+
`ingest.concurrency.*` (3 documents x 10 / 10). `client.ingest.plan(ws, doc_workers=…,
|
|
160
|
+
llm_concurrency=…)` answers `plan.concurrency`: per role the calls in flight (`effective`) and the
|
|
161
|
+
limit that binds (`run`, or the endpoint's `endpoint_ceiling`).
|
|
162
|
+
|
|
163
|
+
### One job for a large folder (uploads in several requests)
|
|
164
|
+
|
|
165
|
+
A long ingest is ONE server job: created with every run option (frozen on the job),
|
|
166
|
+
filled by as many upload requests as the server's limits need, then submitted with the
|
|
167
|
+
full manifest. Each file travels with its sha256 (computed locally); staging is
|
|
168
|
+
idempotent per name + sha256, so a retried or resumed upload never duplicates a file.
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from ragnarok_api import UploadInterrupted, ManifestMismatch
|
|
172
|
+
|
|
173
|
+
ref = load_or_create_my_ref() # e.g. ragnarok_api.new_client_ref(), persisted by you
|
|
174
|
+
try:
|
|
175
|
+
result = client.ingest.upload_job(folder, workspace="my-workspace", level="BALANCED", client_ref=ref,
|
|
176
|
+
progress=lambda p: print(p.batch, p.batches, p.staged_files))
|
|
177
|
+
except UploadInterrupted as exc: # a request kept failing: the job keeps what it staged
|
|
178
|
+
result = client.ingest.upload_job(folder, workspace="my-workspace", client_ref=exc.client_ref)
|
|
179
|
+
for ev in client.ingest.events(result.job_id, after=0): # after=<last seq> on a re-attach
|
|
180
|
+
print(ev.step, ev.accounting) # store_done / doc_skipped / doc_failed carry the counts
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`client_ref` (generated when not given; on the result and on `UploadInterrupted`) names the
|
|
184
|
+
job for the caller: a later call with the same ref finds it (`job_by_client_ref`) and
|
|
185
|
+
continues it, and a create repeated after a lost response answers the same job
|
|
186
|
+
(`existing`), never a second one. With `job_id` (or a ref) of a job already submitted it
|
|
187
|
+
only re-attaches (`already_submitted=True`, nothing sent). The low-level calls are `limits()`,
|
|
188
|
+
`create_job()` (`submit=false`, `client_ref=`), `upload_files()`, `remove_files()`, `submit_job()` and
|
|
189
|
+
`uploading_jobs()`. Typed refusals: `ManifestMismatch` (`missing`, `unexpected`,
|
|
190
|
+
`sha256_differs`), `StagedNameConflict`, `ChecksumMismatch`, `StagingQuotaExceeded`
|
|
191
|
+
(`used_bytes`, `limit_bytes`), `FilesTooLarge` (a file no request can carry; nothing sent).
|
|
192
|
+
Counts come from the server only: `job.accounting` (pages, chunks, tables, figures,
|
|
193
|
+
documents) and `IngestDoneEvent.summary.accounting`.
|
|
194
|
+
|
|
195
|
+
A job found that already ended `failed`/`cancelled` (e.g. `upload_abandoned`) raises
|
|
196
|
+
`UploadJobFinished` (start over with a NEW `client_ref`); a `paused` one is re-attached
|
|
197
|
+
(`paused=True`). A repeated create with other options raises `ClientRefConflict`. Against a
|
|
198
|
+
server before resumable uploads, `upload_job` sends ONE one-shot request
|
|
199
|
+
(`legacy_single_upload=True`, `message`), or raises `ServerTooOldForUpload` when the files
|
|
200
|
+
cannot fit in one request.
|
|
201
|
+
|
|
202
|
+
### Persistent jobs
|
|
203
|
+
|
|
204
|
+
Jobs and their events live in MongoDB, so they outlive an API restart. Admins see every
|
|
205
|
+
job; other callers see the jobs they submitted.
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from ragnarok_api import Conflict, IngestDoneEvent, IngestErrorEvent
|
|
209
|
+
|
|
210
|
+
for job in client.ingest.jobs.list(workspace="my-workspace", status="running", limit=20):
|
|
211
|
+
print(job.id, job.status, job.progress, job.owner_email)
|
|
212
|
+
|
|
213
|
+
client.ingest.jobs.cancel(job.id) # cooperative: the job ends `cancelled`
|
|
214
|
+
try:
|
|
215
|
+
client.ingest.jobs.cancel(job.id)
|
|
216
|
+
except Conflict as exc: # 409 job_not_running once it has finished
|
|
217
|
+
print(exc.code, exc.detail["status"])
|
|
218
|
+
|
|
219
|
+
for ev in client.ingest.jobs.events(job.id):
|
|
220
|
+
if isinstance(ev, IngestDoneEvent): # compact: counts + the job path, not the result
|
|
221
|
+
print(ev.success, ev.cancelled, ev.summary.documents_committed, ev.job_path)
|
|
222
|
+
elif isinstance(ev, IngestErrorEvent):
|
|
223
|
+
print(ev.code, ev.error_message) # e.g. code="access_revoked"
|
|
224
|
+
full = client.ingest.jobs.get(job.id).result # the full per-document result
|
|
225
|
+
|
|
226
|
+
report = client.workspaces.repair("my-workspace") # admin; dry run
|
|
227
|
+
print(report.rollback_count, [(f.document_name, f.kind) for f in report.findings])
|
|
228
|
+
client.workspaces.repair("my-workspace", apply=True) # roll back the leftovers
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`client.ingest.job(id)` and `client.ingest.events(id)` remain as aliases.
|
|
232
|
+
|
|
233
|
+
### Ingest models
|
|
234
|
+
|
|
235
|
+
Per role (extraction, vision) a run uses the request's choice, else the model the workspace
|
|
236
|
+
remembers from its last run, else the Settings ingest profile. Ask the server before uploading:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from ragnarok_api import BadRequest, Conflict
|
|
240
|
+
|
|
241
|
+
plan = client.ingest.plan("my-workspace", level="BALANCED",
|
|
242
|
+
semantic_extraction_llm_model="lm::qwen3", # endpoint::model, optional
|
|
243
|
+
llm_reasoning_effort="low") # also vlm_model, vlm_reasoning_effort
|
|
244
|
+
x = plan.extraction # roles[extraction_role]; plan.vlm is roles[vision_role]
|
|
245
|
+
print(x.model, x.source, x.reasoning_effort, x.status) # source: request | remembered | profile
|
|
246
|
+
print(plan.embedding.model, plan.embedding.dimensions, plan.embedding.locked)
|
|
247
|
+
for notice in plan.run_notices: # the notices about this run's roles
|
|
248
|
+
print("!", notice)
|
|
249
|
+
|
|
250
|
+
try: # the same four fields are form options of estimate() and submit()
|
|
251
|
+
client.ingest.submit(folder, workspace="my-workspace", vlm_model="gw::vision")
|
|
252
|
+
except BadRequest as exc: # 422 ingest_model_invalid: the server's reason
|
|
253
|
+
print(exc.code, exc.message)
|
|
254
|
+
|
|
255
|
+
try: # the embedder changes only while the workspace holds no document
|
|
256
|
+
client.workspaces.set_embedding("new-workspace", "lm::embed-2", 768)
|
|
257
|
+
except Conflict as exc: # 409 embedding_locked
|
|
258
|
+
print(exc.message)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`estimate.models`, the `data.models` of a job's `init` and `run_start` status events and
|
|
262
|
+
`job.result["models"]` have the same shape (`IngestPlan.model_validate(...)` reads them); a
|
|
263
|
+
`model_notice` status event announces a remembered model the server replaced.
|
|
264
|
+
|
|
265
|
+
## 3. Read the knowledge graph
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
hits = client.graph.search("pembrolizumab", workspace="epar").results
|
|
269
|
+
entity = client.graph.entity(hits[0].id, workspace="epar", include_description=True, include_neighbors=True)
|
|
270
|
+
sub = client.graph.neighborhood(hits[0].id, workspace="epar", hops=2)
|
|
271
|
+
print(entity.entity["display_name"], len(sub.entities), len(sub.relationships))
|
|
272
|
+
|
|
273
|
+
chunks = client.search.chunks("overall survival", workspace="epar", limit=20).chunks
|
|
274
|
+
texts = client.sources.batch([c.chunk_id for c in chunks], workspace="epar").contents
|
|
275
|
+
pdf = client.sources.file(chunks[0].document_name, workspace="epar") # bytes
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Cursor-paged routes, such as the planned KG API, can use
|
|
279
|
+
`ragnarok_api.iterate(fetch_page, cursor_field="next_cursor")`.
|
|
280
|
+
|
|
281
|
+
## 4. Manage access
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
from ragnarok_api import Client, Session, TermsRequired
|
|
285
|
+
|
|
286
|
+
client = Client("https://ragnarok.example")
|
|
287
|
+
try:
|
|
288
|
+
session = Session.login(client, "admin@example.com", "secret")
|
|
289
|
+
except TermsRequired as exc: # first login, or terms older than the re-accept window
|
|
290
|
+
session = exc.session.accept_terms() # explicit user act; the server issues a fresh token
|
|
291
|
+
|
|
292
|
+
me = client.auth.whoami()
|
|
293
|
+
print(me.email, me.account_type, me.is_admin, me.role_on("epar")) # "admin", "owner", "viewer"... or None
|
|
294
|
+
print([(g.workspace, g.role) for g in me.grants])
|
|
295
|
+
print([(w.name, w.role) for w in client.workspaces.list()]) # your role on each workspace
|
|
296
|
+
|
|
297
|
+
# Admin: accounts with per-workspace roles (viewer < data_manager < owner)
|
|
298
|
+
client.admin.create_user("analyst@example.com", "a-long-password", grants=[("epar", "viewer")])
|
|
299
|
+
client.admin.update_user("analyst@example.com", grants=[("epar", "viewer"), ("has", "data_manager")])
|
|
300
|
+
|
|
301
|
+
# Owner session, or admin credential allowed by the server privileged-write policy
|
|
302
|
+
grants = client.workspaces.grants.list("epar")
|
|
303
|
+
print(grants.your_role, [(g.email, g.role) for g in grants.grants], grants.admins)
|
|
304
|
+
change = client.workspaces.grants.put("epar", "analyst@example.com", "data_manager")
|
|
305
|
+
print(change.previous_role, "->", change.role, change.changed)
|
|
306
|
+
client.workspaces.grants.delete("epar", "analyst@example.com")
|
|
307
|
+
print(client.workspaces.meta("epar").owners) # data_manager or more
|
|
308
|
+
|
|
309
|
+
client.admin.deactivate_user("analyst@example.com")
|
|
310
|
+
session.logout() # revokes the token server-side
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Roles are per workspace. An admin holds every role everywhere and has
|
|
314
|
+
no grants. An owner may grant `viewer` or `data_manager` only, so granting `owner` raises
|
|
315
|
+
`Forbidden` with `code="role_ceiling"`. Other refusals are `admin_only_workspace`,
|
|
316
|
+
`self_grant` and `owner_grant`. A route above your role raises
|
|
317
|
+
`Forbidden(code="workspace_role_required")`, whose `detail` holds `required` and `role`.
|
|
318
|
+
A concurrent grant change raises `Conflict(code="concurrent_update")`.
|
|
319
|
+
`allowed_workspaces` is deprecated: pass `grants`, and never both (422
|
|
320
|
+
`grants_conflict`).
|
|
321
|
+
|
|
322
|
+
`Token` and `Session` never show their secret in `repr()`. The password is sent once
|
|
323
|
+
and is not stored.
|
|
324
|
+
|
|
325
|
+
## Errors
|
|
326
|
+
|
|
327
|
+
All errors subclass `RagnarokError` and carry `status`, `code`, `detail` and
|
|
328
|
+
`request_id` (from `x-request-id`, else the error body's `request_id`):
|
|
329
|
+
|
|
330
|
+
| Error | When |
|
|
331
|
+
|---|---|
|
|
332
|
+
| `Unauthorized` | 401 |
|
|
333
|
+
| `Forbidden` | 403. `code` gives the reason, e.g. `model_not_allowed`, `workspace_role_required`, `role_ceiling`, `admin_only_workspace`, `session_required` |
|
|
334
|
+
| `TermsRequired` | 403 `terms_required`, or a login that needs terms acceptance. Carries `.session` |
|
|
335
|
+
| `EmbeddingMismatch` | 403 `embedding_mismatch` |
|
|
336
|
+
| `NotFound` / `Conflict` / `BadRequest` | 404 / 409 (e.g. `job_not_running`, `concurrent_update`) / 400 and 422 |
|
|
337
|
+
| `RateLimited` | 429. `retry_after` is in seconds |
|
|
338
|
+
| `ServerError` | 5xx |
|
|
339
|
+
| `TransportError` | no HTTP response (connection refused or reset, timeout) |
|
|
340
|
+
| `ApiVersionMismatch` | the server's API major is not 1 |
|
|
341
|
+
| `QueryFailed` | the query stream emitted `error`. Carries `.error` and `.result` |
|
|
342
|
+
|
|
343
|
+
Retries: only idempotent GETs, and only on connection failures (at most 2 retries,
|
|
344
|
+
with jitter). The SDK does no caching.
|
|
345
|
+
|
|
346
|
+
## Development tests
|
|
347
|
+
|
|
348
|
+
From the repository root:
|
|
349
|
+
|
|
350
|
+
```sh
|
|
351
|
+
python -m pip install -e './sdks/python[test]'
|
|
352
|
+
python -m pytest -c sdks/python/pyproject.toml sdks/python/tests -q
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
These tests use recorded fixtures and mocked HTTP transports; no server or database is needed.
|
|
356
|
+
|
|
357
|
+
## License
|
|
358
|
+
|
|
359
|
+
MIT
|
|
360
|
+
|
|
361
|
+
Query modes are `talk`, `quick`, `balanced` and `deepdive`; availability and authorization are enforced by the server.
|