querri 2.1.0__tar.gz → 2.2.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.
- querri-2.2.0/CHANGELOG.md +238 -0
- {querri-2.1.0 → querri-2.2.0}/CLAUDE.md +4 -4
- {querri-2.1.0 → querri-2.2.0}/PKG-INFO +58 -2
- {querri-2.1.0 → querri-2.2.0}/README.md +57 -1
- {querri-2.1.0 → querri-2.2.0}/docs/data-library.md +6 -1
- querri-2.2.0/docs/roles.md +151 -0
- {querri-2.1.0 → querri-2.2.0}/docs/server-sdk.md +212 -5
- {querri-2.1.0 → querri-2.2.0}/examples/05_get_session.py +29 -1
- {querri-2.1.0 → querri-2.2.0}/examples/10_error_handling.py +14 -0
- {querri-2.1.0 → querri-2.2.0}/examples/web-embed/server.py +46 -3
- querri-2.2.0/querri/__init__.py +110 -0
- {querri-2.1.0 → querri-2.2.0}/querri/_auth.py +12 -1
- {querri-2.1.0 → querri-2.2.0}/querri/_base_client.py +42 -4
- {querri-2.1.0 → querri-2.2.0}/querri/_client.py +29 -0
- {querri-2.1.0 → querri-2.2.0}/querri/_config.py +11 -0
- {querri-2.1.0 → querri-2.2.0}/querri/_convenience.py +25 -1
- querri-2.2.0/querri/_exceptions.py +440 -0
- {querri-2.1.0 → querri-2.2.0}/querri/_user_client.py +6 -2
- querri-2.2.0/querri/_version.py +1 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/_app.py +6 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/_context.py +20 -6
- {querri-2.1.0 → querri-2.2.0}/querri/cli/_output.py +63 -4
- {querri-2.1.0 → querri-2.2.0}/querri/cli/auth.py +6 -1
- {querri-2.1.0 → querri-2.2.0}/querri/cli/embed.py +21 -3
- {querri-2.1.0 → querri-2.2.0}/querri/cli/keys.py +2 -1
- querri-2.2.0/querri/cli/roles.py +411 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/sources.py +1 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/views.py +2 -1
- {querri-2.1.0 → querri-2.2.0}/querri/resources/embed.py +66 -6
- querri-2.2.0/querri/resources/roles.py +324 -0
- {querri-2.1.0 → querri-2.2.0}/querri/resources/users.py +16 -0
- {querri-2.1.0 → querri-2.2.0}/querri/types/__init__.py +19 -0
- {querri-2.1.0 → querri-2.2.0}/querri/types/embed.py +2 -0
- querri-2.2.0/querri/types/role.py +136 -0
- {querri-2.1.0 → querri-2.2.0}/skills/querri-cli/SKILL.md +55 -5
- {querri-2.1.0 → querri-2.2.0}/skills/querri-cli/references/json-shapes.md +71 -8
- {querri-2.1.0 → querri-2.2.0}/tests/test_cli.py +58 -3
- {querri-2.1.0 → querri-2.2.0}/tests/test_cli_commands.py +222 -0
- querri-2.2.0/tests/test_cli_json_errors.py +402 -0
- querri-2.2.0/tests/test_client_header.py +62 -0
- {querri-2.1.0 → querri-2.2.0}/tests/test_convenience.py +149 -0
- querri-2.2.0/tests/test_exceptions.py +977 -0
- {querri-2.1.0 → querri-2.2.0}/tests/test_resources.py +199 -1
- querri-2.2.0/tests/test_roles.py +529 -0
- {querri-2.1.0 → querri-2.2.0}/tests/test_security.py +49 -0
- querri-2.1.0/CHANGELOG.md +0 -109
- querri-2.1.0/querri/__init__.py +0 -53
- querri-2.1.0/querri/_exceptions.py +0 -220
- querri-2.1.0/querri/_version.py +0 -1
- querri-2.1.0/tests/test_exceptions.py +0 -196
- {querri-2.1.0 → querri-2.2.0}/.github/CONTRIBUTING.md +0 -0
- {querri-2.1.0 → querri-2.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {querri-2.1.0 → querri-2.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {querri-2.1.0 → querri-2.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {querri-2.1.0 → querri-2.2.0}/.github/SECURITY.md +0 -0
- {querri-2.1.0 → querri-2.2.0}/.github/workflows/ci.yml +0 -0
- {querri-2.1.0 → querri-2.2.0}/.github/workflows/release.yml +0 -0
- {querri-2.1.0 → querri-2.2.0}/.gitignore +0 -0
- {querri-2.1.0 → querri-2.2.0}/LICENSE +0 -0
- {querri-2.1.0 → querri-2.2.0}/TODO.md +0 -0
- {querri-2.1.0 → querri-2.2.0}/docs/MIGRATION.md +0 -0
- {querri-2.1.0 → querri-2.2.0}/examples/01_quick_start.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/examples/02_user_management.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/examples/03_access_policies.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/examples/04_embed_session.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/examples/06_projects.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/examples/07_dashboards.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/examples/08_chat_streaming.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/examples/09_async_client.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/examples/11_user_scoped_client.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/examples/12_data_operations.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/examples/web-embed/index.html +0 -0
- {querri-2.1.0 → querri-2.2.0}/pyproject.toml +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/_pagination.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/_streaming.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/__init__.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/_image.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/audit.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/chat.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/chats.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/dashboards.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/files.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/library.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/policies.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/projects.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/sharing.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/steps.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/usage.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/users.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/cli/whoami.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/py.typed +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/resources/__init__.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/resources/audit.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/resources/dashboards.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/resources/files.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/resources/keys.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/resources/library.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/resources/policies.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/resources/projects.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/resources/sharing.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/resources/sources.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/resources/usage.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/resources/views.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/types/audit.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/types/chat.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/types/dashboard.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/types/data.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/types/file.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/types/key.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/types/library.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/types/policy.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/types/project.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/types/sharing.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/types/usage.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/querri/types/user.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/scripts/release.sh +0 -0
- {querri-2.1.0 → querri-2.2.0}/skills/README.md +0 -0
- {querri-2.1.0 → querri-2.2.0}/skills/querri-cli/references/troubleshooting.md +0 -0
- {querri-2.1.0 → querri-2.2.0}/tests/__init__.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/tests/conftest.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/tests/rls-integration-results.md +0 -0
- {querri-2.1.0 → querri-2.2.0}/tests/test_client.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/tests/test_config.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/tests/test_integration.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/tests/test_library.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/tests/test_library_streaming.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/tests/test_new_features.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/tests/test_pagination.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/tests/test_rls_integration.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/tests/test_streaming.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/tests/test_streaming_v2.py +0 -0
- {querri-2.1.0 → querri-2.2.0}/uv.lock +0 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.2.0 (2026-10-31)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Custom roles as code.** `client.roles` (sync and async): `list()`,
|
|
8
|
+
`get(ref)`, `catalog()`, `apply(definition, dry_run=)` (idempotent upsert,
|
|
9
|
+
`PUT /roles/{key}`), `import_roles(definitions, dry_run=)` (one call, each
|
|
10
|
+
role independent), `delete(ref, reassign_to=)` and `export()` (definitions
|
|
11
|
+
with the output-only fields stripped, ready to save and re-apply). New
|
|
12
|
+
types in `querri.types`: `RoleDefinition`, `RoleApplyResult`,
|
|
13
|
+
`RoleChange`, `RoleApplyError`, `RoleImportResult`, `RoleDeleteResponse`,
|
|
14
|
+
`PermissionCatalog`, `CatalogPermission`. Needs the `admin:roles:read` /
|
|
15
|
+
`admin:roles:write` scopes. See docs/roles.md.
|
|
16
|
+
- **`querri role`** (alias `querri roles`): `list`, `get`, `export`,
|
|
17
|
+
`apply -f FILE [--dry-run] [--watch]`, `delete [--reassign-to] [--yes]` and
|
|
18
|
+
`catalog [--area]`. `apply` prints a per-field change summary
|
|
19
|
+
(`denies: +chat.code.view -tools.dashboards.use`) and exits non-zero when
|
|
20
|
+
any role failed; `--watch` re-applies whenever the file is saved. JSON
|
|
21
|
+
files only.
|
|
22
|
+
- The **Roles** scope group in the `querri key new` scope picker.
|
|
23
|
+
- **Custom Roles on embed sessions.** `client.embed.create_session()` and
|
|
24
|
+
`client.embed.get_session()` (sync and async) take `role=` — the id or key
|
|
25
|
+
of the Custom Role the session runs as. It is sent only when set, and it
|
|
26
|
+
is not `user["role"]` (the org role used when creating a user). Decide it
|
|
27
|
+
on your server, never from the browser's request. Granting a role the user
|
|
28
|
+
does not hold is **permanent**: it is added to the user's role assignment
|
|
29
|
+
(later sessions and the app see it), revoking the session does not remove
|
|
30
|
+
it, and an admin removes it in Settings → People. `EmbedSession.role_id`
|
|
31
|
+
and `EmbedSessionListItem.role_id` carry the applied role (`None` without
|
|
32
|
+
one — or when an older server ignored `role`, in which case the session
|
|
33
|
+
runs as the user's own role; servers released after 2026-10-21 also
|
|
34
|
+
return it from refresh and list, earlier ones only from create);
|
|
35
|
+
`refresh_session()` returns it too, and the `get_session()` dict carries
|
|
36
|
+
`role_id` when a role was applied. CLI: `querri session new --role` /
|
|
37
|
+
`querri session get --role`; `session new` and `session refresh` print
|
|
38
|
+
`Role ID`, and the `session list` table has a `Role ID` column.
|
|
39
|
+
- **`APIError.permission` and `APIError.reason`.** A `403
|
|
40
|
+
permission_denied` names the permission the call needed (from the body,
|
|
41
|
+
else the `X-Permission-Denied` header) and why it was refused
|
|
42
|
+
(`role_denied`, `not_in_plan`, `flag_off`, `embed_disabled`,
|
|
43
|
+
`dependency_unmet`, `forbidden`, `scope_exceeds_role`; the server may add
|
|
44
|
+
more). The header is used only when the error's code is
|
|
45
|
+
`permission_denied` or there is no code, and only when it holds a
|
|
46
|
+
permission key (contains a `.`), not an error code — so an
|
|
47
|
+
`escalation_denied` refusal never borrows it. The in-app flat body
|
|
48
|
+
`{"detail": {"error": "<code>", ...}}` is parsed too. `RateLimitError`
|
|
49
|
+
keeps them too.
|
|
50
|
+
- **`APIError.body`**: the parsed JSON response body (any JSON value; `None`
|
|
51
|
+
when the response was not JSON), on every `APIError`, streamed requests
|
|
52
|
+
included. It carries the fields the exception does not lift out, e.g.
|
|
53
|
+
`err.body["detail"]["error"]["scopes"]` on `scope_exceeds_role` and
|
|
54
|
+
`err.body["detail"]["error"]["missing"]` on `escalation_denied`.
|
|
55
|
+
- **Error-code constants**: `querri.ROLE_DISABLED`, `querri.PERMISSION_DENIED`,
|
|
56
|
+
`querri.ESCALATION_DENIED` (403), `querri.LAST_ADMIN` (409),
|
|
57
|
+
`querri.INVALID_PARAMETERS` (400), … (26 in all) and the
|
|
58
|
+
`querri.ErrorCode` / `querri.PermissionDeniedReason` `Literal`s — the
|
|
59
|
+
codes for authentication, access, Custom Roles, embed sessions and user
|
|
60
|
+
role changes; the API sends other codes too, so treat `code` as a string.
|
|
61
|
+
docs/server-sdk.md has an Error codes table.
|
|
62
|
+
- `embed:role:assign` in the `querri key new` scope picker.
|
|
63
|
+
- **`X-Querri-Client` header.** Every client sends `X-Querri-Client:
|
|
64
|
+
sdk/<version>`; clients built by the `querri` command, and its
|
|
65
|
+
login/refresh token requests, send `cli/<version>`, so the server can tell
|
|
66
|
+
CLI use from library use.
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
|
|
70
|
+
- **`422` request-validation errors raise `ValidationError`** (was the base
|
|
71
|
+
`APIError`) with a readable message — `"Validation failed: body.ttl: Input
|
|
72
|
+
should be greater than or equal to 900"` — instead of the raw `detail`
|
|
73
|
+
list. A bare `{"detail": {"code", "message"}}` body is parsed like the
|
|
74
|
+
`error` envelope.
|
|
75
|
+
- **Behaviour change — CLI: a 403 exits `6`** instead of `1`. Scripts that
|
|
76
|
+
test for exit code `1` on a refused call must accept `6`. In text mode the
|
|
77
|
+
message gains the permission and reason (`(permission:
|
|
78
|
+
dashboards.list.view, reason: role_denied)`); the API code is printed once,
|
|
79
|
+
on its `Code:` line, as before.
|
|
80
|
+
- **CLI `--json` errors: unchanged from 2.1.0, plus `exit_code`.** Every
|
|
81
|
+
JSON error prints the same keys and values 2.1.0 printed for the same
|
|
82
|
+
situation — a 403 is still `"error": "api_error"`, `code` is still the API
|
|
83
|
+
error code when the server sent one and otherwise the exit code, and
|
|
84
|
+
`permission` / `reason` are not added as keys — plus `"exit_code"`, the
|
|
85
|
+
process exit code. The one value that moves is a consequence of the exit
|
|
86
|
+
code above: a 403 with no API code has `"code": 6` (it was `1`). The `querri
|
|
87
|
+
view run` timeout (`{"error": "<message>"}`) and `querri auth switch-org`
|
|
88
|
+
single-org outputs keep their 2.1.0 shapes, plus `exit_code`.
|
|
89
|
+
- Docs: API-key scopes follow a person's role — a key keeps only the scopes
|
|
90
|
+
both its creator and its bound user currently hold, and the permission
|
|
91
|
+
checks made on its behalf (embed role grants, the users endpoints' role
|
|
92
|
+
guards) use the permissions both hold (servers released after 2026-10-21;
|
|
93
|
+
earlier servers cap only keys created in Settings and check role grants
|
|
94
|
+
against the bound user, else the creator). The keys section covers
|
|
95
|
+
binding (Admins only), pseudo ids, and what a key minted by another key
|
|
96
|
+
inherits. On those servers `client.users` `create` / `update` / `delete` /
|
|
97
|
+
`get_or_create` (and `get_session()` for a new user) can be refused with
|
|
98
|
+
`escalation_denied` or `last_admin`. The real scope list replaces the
|
|
99
|
+
invented names in the CLI skill. `as_user` calls can raise
|
|
100
|
+
`permission_denied` under the session's role.
|
|
101
|
+
|
|
102
|
+
### Fixed
|
|
103
|
+
|
|
104
|
+
- **Streaming calls keep their error details.** A streamed request
|
|
105
|
+
(`projects.chats.stream()`, view streams) that failed raised an
|
|
106
|
+
`APIError` with no `code`, `message`, `permission` or `reason`, because
|
|
107
|
+
the unread body could not be parsed. The body is now read (and the
|
|
108
|
+
response closed) before raising; a body that fails mid-read still raises
|
|
109
|
+
the status's exception class.
|
|
110
|
+
- **Retried streamed responses are closed.** A streamed request that got a
|
|
111
|
+
429 or a retryable 5xx left each discarded response open, holding its
|
|
112
|
+
connection; each is now closed before the retry.
|
|
113
|
+
- **A JSON array or scalar error body no longer crashes error handling.**
|
|
114
|
+
`raise_for_status` assumed a JSON object and raised `AttributeError` on a
|
|
115
|
+
body like `["bad"]` or `"oops"`; it now raises the status's exception.
|
|
116
|
+
- `create_session(role="")` / `get_session(role="")` no longer send an
|
|
117
|
+
empty `role`.
|
|
118
|
+
- **`querri view list` no longer truncates IDs.** The Rich table clipped
|
|
119
|
+
36-character view UUIDs with `…`, and pasting the clipped ID into `querri
|
|
120
|
+
view chat` failed with a misleading 403. ID/UUID columns now never wrap
|
|
121
|
+
(QUE-135).
|
|
122
|
+
- `skills/querri-cli/references/json-shapes.md` documented a non-existent
|
|
123
|
+
`querri embed create-session` and a stale `querri key create`; it now
|
|
124
|
+
documents `querri session new` / `refresh` / `list` / `get`, `querri key
|
|
125
|
+
new` and the `--json` error shape.
|
|
126
|
+
- **`examples/web-embed/server.py` no longer exposes the Werkzeug
|
|
127
|
+
debugger.** It bound `0.0.0.0` with `debug=True`, letting anyone on the
|
|
128
|
+
network run code on the machine. It now binds `127.0.0.1` with debug off;
|
|
129
|
+
`HOST` and `FLASK_DEBUG=1` opt in, and debug is refused on a non-local
|
|
130
|
+
host.
|
|
131
|
+
|
|
132
|
+
## 2.1.0 (2026-09-24)
|
|
133
|
+
|
|
134
|
+
### Added
|
|
135
|
+
|
|
136
|
+
- **`client.library`** (sync and async) and the `querri library` CLI: the Data
|
|
137
|
+
Library surface — libraries, collections, anchor and refining questions,
|
|
138
|
+
graph search and zoom, `record_fact`, the Librarian `chat` / `chat_stream`,
|
|
139
|
+
`ask`, `intake`, `consolidate`, `eval`, backfill and health. Reference:
|
|
140
|
+
[docs/data-library.md](docs/data-library.md). This is the `dev` line that
|
|
141
|
+
had been building since 1.1.0; it ships here unchanged.
|
|
142
|
+
|
|
143
|
+
### Fixed
|
|
144
|
+
|
|
145
|
+
- **Error bodies without a Stripe-style envelope keep their message and
|
|
146
|
+
code.** FastAPI raises `{"detail": {"error": {...}}}` (or a plain string
|
|
147
|
+
`detail`); `APIError.message`, `.code` and `.type` were left as
|
|
148
|
+
`"HTTP <status>"` / `None` for those, so a `NotFoundError` for a step with
|
|
149
|
+
no data read the same as one for a missing project. Both shapes are parsed
|
|
150
|
+
now.
|
|
151
|
+
|
|
152
|
+
## 2.0.0 (2026-08-31)
|
|
153
|
+
|
|
154
|
+
Migration guide with before/after snippets: [docs/MIGRATION.md](docs/MIGRATION.md).
|
|
155
|
+
|
|
156
|
+
### Breaking
|
|
157
|
+
|
|
158
|
+
- **`source_scope` removed from `client.embed.create_session()`** (sync and
|
|
159
|
+
async). It was never enforced by the server — a silently-ignored parameter
|
|
160
|
+
that *looked* like access control. Passing it now raises `TypeError`. Scope
|
|
161
|
+
sessions with access policies instead: `client.embed.get_session(access=...)`.
|
|
162
|
+
- **`as_user()` / `UserQuerri` rebased from `/api` to `/api/v1`.** Embed
|
|
163
|
+
sessions (`X-Embed-Session`) are the public API's highest-priority
|
|
164
|
+
credential, so user-scoped clients now call the same public API paths as
|
|
165
|
+
the admin client.
|
|
166
|
+
- **`user_client.dashboards` is read-only.** Embed sessions exclude the
|
|
167
|
+
`admin:dashboards:write` scope server-side; the user-scoped surface now
|
|
168
|
+
exposes only `list()`, `get()`, and `refresh_status()` — `create`,
|
|
169
|
+
`update`, `delete`, and `refresh` no longer exist there (they always
|
|
170
|
+
belonged to the admin client).
|
|
171
|
+
- There is no `user_client.embed`: embed sessions exclude
|
|
172
|
+
`embed:session:create` server-side (a session cannot mint sessions).
|
|
173
|
+
The accessor never existed in the SDK; the contract is now documented
|
|
174
|
+
and tested.
|
|
175
|
+
- **`ttl` is validated client-side**: `create_session()` and
|
|
176
|
+
`get_session()` raise `ValueError` for TTLs outside [900, 86400] seconds
|
|
177
|
+
instead of sending a value the server would silently clamp.
|
|
178
|
+
|
|
179
|
+
### Added
|
|
180
|
+
|
|
181
|
+
- `client.embed.get_ui_config(org)` — fetches the public, unauthenticated
|
|
182
|
+
`GET {host}/api/embed/ui-config?org=...` (main app path, not `/api/v1`)
|
|
183
|
+
and returns the raw `{chrome, theme, privacy}` dict.
|
|
184
|
+
- `querri session ui-config --org <id>` CLI command for the same.
|
|
185
|
+
- `OriginRequiredError` (subclass of `ValidationError`), raised for
|
|
186
|
+
`400 origin_required` with a message explaining the org's embed-domain
|
|
187
|
+
allowlist and how to pass `origin`.
|
|
188
|
+
- `querri share source org` gained `--disable` to turn org-wide sharing
|
|
189
|
+
off (the server contract always supported `enabled: false`).
|
|
190
|
+
- `querri keys create` interactive scope picker now includes the
|
|
191
|
+
`admin:skills:*`, `admin:library:*` scopes and `*` (superadmin).
|
|
192
|
+
|
|
193
|
+
### Fixed
|
|
194
|
+
|
|
195
|
+
- `client.embed.revoke_user_sessions()` no longer stops at the first
|
|
196
|
+
listing page: the server caps listings at 200 with no cursor, so it now
|
|
197
|
+
loops revoke-and-relist until a listing shows no sessions for the user
|
|
198
|
+
(bounded at 50 passes), and tolerates a session expiring between list
|
|
199
|
+
and delete. Still best-effort — with more than 200 concurrent sessions
|
|
200
|
+
org-wide the listing is a sample and stragglers can survive; do not
|
|
201
|
+
treat a return as proof every session is gone.
|
|
202
|
+
|
|
203
|
+
## 1.1.1 (2026-08-31)
|
|
204
|
+
|
|
205
|
+
### Fixed
|
|
206
|
+
|
|
207
|
+
- **Web-embed example** (`examples/web-embed/server.py`): three dispatched
|
|
208
|
+
actions called keyword-only SDK methods positionally, raising `TypeError`
|
|
209
|
+
(returned as HTTP 500): `embed.refreshSession`, `users.getOrCreate`, and
|
|
210
|
+
`policies.resolve`. All now pass keyword arguments.
|
|
211
|
+
- **Web-embed example** (`examples/web-embed/index.html`): the SDK Explorer's
|
|
212
|
+
"Data" group sent `data.*` action names the server never dispatched — every
|
|
213
|
+
action in the group returned "Unknown action". Renamed to the real
|
|
214
|
+
`sources.*` actions.
|
|
215
|
+
- **Docs**: `docs/server-sdk.md` and `README.md` showed `create_session`,
|
|
216
|
+
`refresh_session`, and `get_session` with positional parameters; all three
|
|
217
|
+
are keyword-only (`create_session(*, user_id=..., origin=..., ttl=...)`,
|
|
218
|
+
`refresh_session(session_token=...)`). Snippets updated to keyword form.
|
|
219
|
+
- **Browser embed loading**: examples and docs now load the embed script from
|
|
220
|
+
the product — `<script src="{serverUrl}/sdk/querri-embed.js">` — instead of
|
|
221
|
+
an unverifiable CDN/npm IIFE path. The web-embed example injects the script
|
|
222
|
+
after fetching `serverUrl` from its `/api/config` endpoint.
|
|
223
|
+
- **Embed chrome options**: examples use the v2 vocabulary
|
|
224
|
+
(`chrome: { rail: { show: true }, header: { show: true } }` — `rail`
|
|
225
|
+
replaces `sidebar`).
|
|
226
|
+
- **Origin allowlist guidance**: create-session docs and examples now pass
|
|
227
|
+
`origin` and document the enforcement behavior — when the organization
|
|
228
|
+
configures allowed embed domains, session creation without an origin fails
|
|
229
|
+
with `400 origin_required`, and an origin not on the allowlist fails with
|
|
230
|
+
`403 origin_not_allowed`.
|
|
231
|
+
|
|
232
|
+
### Deprecated
|
|
233
|
+
|
|
234
|
+
- `source_scope` on `client.embed.create_session()` (sync and async): the
|
|
235
|
+
server does not enforce it, and it will be removed in v2.0.0. Passing it now
|
|
236
|
+
emits a `DeprecationWarning`; the field is still sent, so behavior is
|
|
237
|
+
unchanged. Scope sessions with access policies instead — see
|
|
238
|
+
`client.embed.get_session(access=...)`.
|
|
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
4
4
|
|
|
5
5
|
## Project Overview
|
|
6
6
|
|
|
7
|
-
Querri Python SDK — a typed Python client for the Querri API, providing both sync (`Querri`) and async (`AsyncQuerri`) clients. Built on httpx + Pydantic v2. Requires Python >=3.
|
|
7
|
+
Querri Python SDK — a typed Python client for the Querri API, providing both sync (`Querri`) and async (`AsyncQuerri`) clients. Built on httpx + Pydantic v2. Requires Python >=3.10.
|
|
8
8
|
|
|
9
9
|
## Commands
|
|
10
10
|
|
|
@@ -37,7 +37,7 @@ ruff check querri/ tests/
|
|
|
37
37
|
`Querri` / `AsyncQuerri` (`_client.py`) are the public entry points. They hold lazily-initialized resource namespaces (e.g., `client.users`, `client.projects`) and support context-manager usage. They delegate HTTP to `SyncHTTPClient` / `AsyncHTTPClient` (`_base_client.py`), which handle auth headers (Bearer token + X-Tenant-ID), retries with exponential backoff, and error-to-exception mapping.
|
|
38
38
|
|
|
39
39
|
### User-Scoped Client
|
|
40
|
-
`UserQuerri` / `AsyncUserQuerri` (`_user_client.py`) are created via `client.as_user(session)`. They use `X-Embed-Session` auth against the
|
|
40
|
+
`UserQuerri` / `AsyncUserQuerri` (`_user_client.py`) are created via `client.as_user(session)`. They use `X-Embed-Session` auth against the same public API (`/api/v1`) as the admin client. Only expose FGA-filtered resources: `projects`, `dashboards` (read-only), `sources`, `chats`. Calls are subject to the session's role and can raise `PermissionError` (`permission_denied`).
|
|
41
41
|
|
|
42
42
|
### Resource Pattern
|
|
43
43
|
Each API resource lives in `querri/resources/` with paired sync/async classes (e.g., `Users` / `AsyncUsers`). Resources receive the HTTP client at init and expose CRUD methods that return Pydantic models from `querri/types/`. Projects has a nested `Chats` sub-resource for chat streaming.
|
|
@@ -88,8 +88,8 @@ Every resource, pagination class, and stream has mirrored sync/async implementat
|
|
|
88
88
|
|
|
89
89
|
## Code Quality
|
|
90
90
|
|
|
91
|
-
- **mypy**: Strict mode, target Python 3.
|
|
92
|
-
- **ruff**: Rules E, F, I, N, UP, B, SIM (target Python 3.
|
|
91
|
+
- **mypy**: Strict mode, target Python 3.10
|
|
92
|
+
- **ruff**: Rules E, F, I, N, UP, B, SIM (target Python 3.10)
|
|
93
93
|
- **Tests**: pytest + pytest-asyncio (mode: auto) + respx for HTTP mocking
|
|
94
94
|
|
|
95
95
|
## Releasing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: querri
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Python SDK and CLI for the Querri data analysis platform
|
|
5
5
|
Project-URL: Homepage, https://querri.com
|
|
6
6
|
Project-URL: Documentation, https://docs.querri.com/sdk/python
|
|
@@ -257,6 +257,32 @@ session["expires_in"] # int — seconds until expiry
|
|
|
257
257
|
session["user_id"] # str — Querri user ID
|
|
258
258
|
```
|
|
259
259
|
|
|
260
|
+
To run the session as one of your org's **Custom Roles**, pass `role` (a role
|
|
261
|
+
id or key); the result carries `role_id` when it was applied:
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
session = client.embed.get_session(user="customer-42", role="analyst")
|
|
265
|
+
session.get("role_id") # str — present only when a role was applied
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`role_id` is a custom role's id (`role_…`) or a built-in role's
|
|
269
|
+
(`system:creator`). No `role_id` means the role was not applied — an older
|
|
270
|
+
server ignores `role`, and the session runs as the user's own role.
|
|
271
|
+
|
|
272
|
+
`role` is not `user["role"]` (the org role, `admin`/`member`, used when the
|
|
273
|
+
user is created). **Decide `role` on your server from your own auth — never
|
|
274
|
+
read it from the browser's request.** Granting a role the user does not yet
|
|
275
|
+
hold needs an API key with the `embed:role:assign` scope, and **the grant is
|
|
276
|
+
permanent**: the role is added to the user's assignment, so their later
|
|
277
|
+
sessions and the Querri app see it, and revoking the session does not remove
|
|
278
|
+
it (an admin removes it in Settings → People). Failures raise
|
|
279
|
+
`NotFoundError` (`role_not_found`) or `PermissionError` (`role_not_allowed`,
|
|
280
|
+
`role_disabled`, `insufficient_scope`, `seat_change_required`). The user is
|
|
281
|
+
created/updated and access policies applied before the session is requested,
|
|
282
|
+
so if the role is refused those steps have still happened. On servers
|
|
283
|
+
released after 2026-10-21, creating that user can itself be refused
|
|
284
|
+
(`escalation_denied`, `last_admin` — see below).
|
|
285
|
+
|
|
260
286
|
You can also pass pre-created policy IDs directly:
|
|
261
287
|
|
|
262
288
|
```python
|
|
@@ -318,7 +344,7 @@ All errors extend `QuerriError`:
|
|
|
318
344
|
```
|
|
319
345
|
QuerriError
|
|
320
346
|
├── APIError — HTTP error responses
|
|
321
|
-
│ ├── ValidationError — 400
|
|
347
|
+
│ ├── ValidationError — 400 / 422
|
|
322
348
|
│ │ └── OriginRequiredError — 400 origin_required (org has an embed-domain allowlist)
|
|
323
349
|
│ ├── AuthenticationError — 401
|
|
324
350
|
│ ├── PermissionError — 403
|
|
@@ -330,10 +356,40 @@ QuerriError
|
|
|
330
356
|
└── ConfigError — missing/invalid configuration
|
|
331
357
|
```
|
|
332
358
|
|
|
359
|
+
Every `APIError` carries `status`, `message`, `type`, `code`, `doc_url`,
|
|
360
|
+
`request_id` and `body` (the parsed JSON response, `None` when it was not
|
|
361
|
+
JSON — it holds the fields the exception does not lift out, e.g.
|
|
362
|
+
`e.body["detail"]["error"]["scopes"]` on `scope_exceeds_role`). A `403
|
|
363
|
+
permission_denied` (the caller's role does not allow the action) also carries
|
|
364
|
+
`permission` and `reason` (`role_denied`, `not_in_plan`, `flag_off`,
|
|
365
|
+
`embed_disabled`, `dependency_unmet`, `forbidden`, `scope_exceeds_role` — the
|
|
366
|
+
server may add more). On servers
|
|
367
|
+
released after 2026-10-21, `client.users` `create` / `update` / `delete` /
|
|
368
|
+
`get_or_create` — and `get_session()` for a new user — can raise
|
|
369
|
+
`PermissionError` (`escalation_denied`, the permissions you lack in
|
|
370
|
+
`e.body["detail"]["error"]["missing"]`) or `ConflictError` (`last_admin`).
|
|
371
|
+
Compare `code` against the
|
|
372
|
+
exported constants (they cover authentication, access, Custom Roles and embed
|
|
373
|
+
sessions; the API sends other codes too, so treat `code` as a string):
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
from querri import PermissionError, PERMISSION_DENIED # shadows the builtin
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
client.dashboards.list()
|
|
380
|
+
except PermissionError as e:
|
|
381
|
+
if e.code == PERMISSION_DENIED:
|
|
382
|
+
print(f"needs {e.permission} ({e.reason})")
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
See [docs/server-sdk.md](docs/server-sdk.md#error-codes) for the code table.
|
|
386
|
+
|
|
333
387
|
The SDK automatically retries **429** (always) and **5xx** (idempotent methods only) with exponential backoff + jitter.
|
|
334
388
|
|
|
335
389
|
### Full Reference
|
|
336
390
|
|
|
391
|
+
Custom roles can be managed as code (export, edit, `querri role apply -f role.json --watch`): see **[docs/roles.md](docs/roles.md)**.
|
|
392
|
+
|
|
337
393
|
See **[docs/server-sdk.md](docs/server-sdk.md)** for complete method signatures, all framework examples, and the `get_session()` deep dive.
|
|
338
394
|
|
|
339
395
|
---
|
|
@@ -211,6 +211,32 @@ session["expires_in"] # int — seconds until expiry
|
|
|
211
211
|
session["user_id"] # str — Querri user ID
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
+
To run the session as one of your org's **Custom Roles**, pass `role` (a role
|
|
215
|
+
id or key); the result carries `role_id` when it was applied:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
session = client.embed.get_session(user="customer-42", role="analyst")
|
|
219
|
+
session.get("role_id") # str — present only when a role was applied
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`role_id` is a custom role's id (`role_…`) or a built-in role's
|
|
223
|
+
(`system:creator`). No `role_id` means the role was not applied — an older
|
|
224
|
+
server ignores `role`, and the session runs as the user's own role.
|
|
225
|
+
|
|
226
|
+
`role` is not `user["role"]` (the org role, `admin`/`member`, used when the
|
|
227
|
+
user is created). **Decide `role` on your server from your own auth — never
|
|
228
|
+
read it from the browser's request.** Granting a role the user does not yet
|
|
229
|
+
hold needs an API key with the `embed:role:assign` scope, and **the grant is
|
|
230
|
+
permanent**: the role is added to the user's assignment, so their later
|
|
231
|
+
sessions and the Querri app see it, and revoking the session does not remove
|
|
232
|
+
it (an admin removes it in Settings → People). Failures raise
|
|
233
|
+
`NotFoundError` (`role_not_found`) or `PermissionError` (`role_not_allowed`,
|
|
234
|
+
`role_disabled`, `insufficient_scope`, `seat_change_required`). The user is
|
|
235
|
+
created/updated and access policies applied before the session is requested,
|
|
236
|
+
so if the role is refused those steps have still happened. On servers
|
|
237
|
+
released after 2026-10-21, creating that user can itself be refused
|
|
238
|
+
(`escalation_denied`, `last_admin` — see below).
|
|
239
|
+
|
|
214
240
|
You can also pass pre-created policy IDs directly:
|
|
215
241
|
|
|
216
242
|
```python
|
|
@@ -272,7 +298,7 @@ All errors extend `QuerriError`:
|
|
|
272
298
|
```
|
|
273
299
|
QuerriError
|
|
274
300
|
├── APIError — HTTP error responses
|
|
275
|
-
│ ├── ValidationError — 400
|
|
301
|
+
│ ├── ValidationError — 400 / 422
|
|
276
302
|
│ │ └── OriginRequiredError — 400 origin_required (org has an embed-domain allowlist)
|
|
277
303
|
│ ├── AuthenticationError — 401
|
|
278
304
|
│ ├── PermissionError — 403
|
|
@@ -284,10 +310,40 @@ QuerriError
|
|
|
284
310
|
└── ConfigError — missing/invalid configuration
|
|
285
311
|
```
|
|
286
312
|
|
|
313
|
+
Every `APIError` carries `status`, `message`, `type`, `code`, `doc_url`,
|
|
314
|
+
`request_id` and `body` (the parsed JSON response, `None` when it was not
|
|
315
|
+
JSON — it holds the fields the exception does not lift out, e.g.
|
|
316
|
+
`e.body["detail"]["error"]["scopes"]` on `scope_exceeds_role`). A `403
|
|
317
|
+
permission_denied` (the caller's role does not allow the action) also carries
|
|
318
|
+
`permission` and `reason` (`role_denied`, `not_in_plan`, `flag_off`,
|
|
319
|
+
`embed_disabled`, `dependency_unmet`, `forbidden`, `scope_exceeds_role` — the
|
|
320
|
+
server may add more). On servers
|
|
321
|
+
released after 2026-10-21, `client.users` `create` / `update` / `delete` /
|
|
322
|
+
`get_or_create` — and `get_session()` for a new user — can raise
|
|
323
|
+
`PermissionError` (`escalation_denied`, the permissions you lack in
|
|
324
|
+
`e.body["detail"]["error"]["missing"]`) or `ConflictError` (`last_admin`).
|
|
325
|
+
Compare `code` against the
|
|
326
|
+
exported constants (they cover authentication, access, Custom Roles and embed
|
|
327
|
+
sessions; the API sends other codes too, so treat `code` as a string):
|
|
328
|
+
|
|
329
|
+
```python
|
|
330
|
+
from querri import PermissionError, PERMISSION_DENIED # shadows the builtin
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
client.dashboards.list()
|
|
334
|
+
except PermissionError as e:
|
|
335
|
+
if e.code == PERMISSION_DENIED:
|
|
336
|
+
print(f"needs {e.permission} ({e.reason})")
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
See [docs/server-sdk.md](docs/server-sdk.md#error-codes) for the code table.
|
|
340
|
+
|
|
287
341
|
The SDK automatically retries **429** (always) and **5xx** (idempotent methods only) with exponential backoff + jitter.
|
|
288
342
|
|
|
289
343
|
### Full Reference
|
|
290
344
|
|
|
345
|
+
Custom roles can be managed as code (export, edit, `querri role apply -f role.json --watch`): see **[docs/roles.md](docs/roles.md)**.
|
|
346
|
+
|
|
291
347
|
See **[docs/server-sdk.md](docs/server-sdk.md)** for complete method signatures, all framework examples, and the `get_session()` deep dive.
|
|
292
348
|
|
|
293
349
|
---
|
|
@@ -177,4 +177,9 @@ Library routes require API-key scopes:
|
|
|
177
177
|
| `admin:library:read` | GET `/library/nodes`, `/library/nodes/{id}`, `/library/status`, `/library/health`, POST `/library/search`, `/library/zoom` |
|
|
178
178
|
| `admin:library:write` | POST `/library/libraries`, `/library/collections`, `/library/questions/*`, `/library/edges`, `/library/facts`, `/library/backfill`, `/library/seed-fixture`, `/library/chat` |
|
|
179
179
|
|
|
180
|
-
The wildcard `*` superadmin scope satisfies both. JWT auth
|
|
180
|
+
The wildcard `*` superadmin scope satisfies both. With JWT auth (the CLI after
|
|
181
|
+
`querri auth login`) the scopes follow your active role (Custom Roles), so
|
|
182
|
+
whether you hold the write scope depends on that role; a Guest role is
|
|
183
|
+
read-only. An API key keeps only the scopes both its creator and its bound
|
|
184
|
+
user currently hold (servers released after 2026-10-21; earlier servers cap
|
|
185
|
+
only keys created in Settings).
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Custom roles as code — CLI + SDK reference
|
|
2
|
+
|
|
3
|
+
A Querri custom role is a JSON **definition document** identified by a stable `key`, much like an AWS IAM policy. You export it, edit it in your editor, and apply it back in one call. Apply is an idempotent upsert: it creates the role when the key is new, replaces it when the key exists, and reports `unchanged` when nothing differs.
|
|
4
|
+
|
|
5
|
+
The `querri role` command (`querri roles` works too) and the `client.roles` SDK resource expose the full surface.
|
|
6
|
+
|
|
7
|
+
## Auth + setup
|
|
8
|
+
|
|
9
|
+
Roles-as-code calls need an API key with the roles scopes:
|
|
10
|
+
|
|
11
|
+
| Scope | Allows |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `admin:roles:read` | `list`, `get`, `catalog`, `export` |
|
|
14
|
+
| `admin:roles:write` | `apply`, `import`, `delete` |
|
|
15
|
+
|
|
16
|
+
An admin creates a key with those scopes in **Settings → API Keys** (or `querri key new` and pick the **Roles** group). The person behind the key must also hold `admin.roles.manage`.
|
|
17
|
+
|
|
18
|
+
A key never does more than its person. A role may not grant a permission the caller does not hold; the server refuses that with 403 `escalation_denied`.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
export QUERRI_API_KEY=qk_...
|
|
22
|
+
export QUERRI_ORG_ID=org_...
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## The workflow
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# 1. Export the custom roles (system roles are left out) to a file.
|
|
29
|
+
querri role export -o roles.json
|
|
30
|
+
|
|
31
|
+
# Or export one role.
|
|
32
|
+
querri role get ev-datapro > ev.json
|
|
33
|
+
|
|
34
|
+
# 2. Preview what an edit would change. Nothing is written.
|
|
35
|
+
querri role apply -f ev.json --dry-run
|
|
36
|
+
|
|
37
|
+
# 3. Apply it, and keep applying on every save.
|
|
38
|
+
querri role apply -f ev.json --watch
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
With `--watch` running, open the app in another browser as someone who holds the role, edit `ev.json`, and save: the role updates within about a second. Each apply prints what changed:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
ev-datapro: updated denies: +chat.code.view -tools.dashboards.use
|
|
45
|
+
ev-datapro: updated config.home_chat: "chat" -> "project"
|
|
46
|
+
ev-datapro: unchanged
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Press Ctrl-C to stop watching. A file that is mid-save or holds invalid JSON prints an error and the watch keeps going.
|
|
50
|
+
|
|
51
|
+
## The definition document
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"schema": "querri.role/v1",
|
|
56
|
+
"key": "ev-datapro",
|
|
57
|
+
"name": "EV DataPro",
|
|
58
|
+
"description": "Estrenar Vivienda end users: projects only, charts on request",
|
|
59
|
+
"base": "creator",
|
|
60
|
+
"enabled": true,
|
|
61
|
+
"grants": [],
|
|
62
|
+
"denies": ["tools.charts.use", "chat.code.view", "chat.tables.view"],
|
|
63
|
+
"settings": {"chat.think_longer.use": {"default": "fast", "user_can_change": false}},
|
|
64
|
+
"config": {"home_chat": "project"}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Field | Notes |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `schema` | Optional on input; always `querri.role/v1` on output. Any other value is refused (422 `unsupported_schema`). |
|
|
71
|
+
| `key` | `^[a-z0-9][a-z0-9-]{0,47}$`. The built-in keys `guest`, `creator`, `admin` cannot be applied or deleted (405 `system_role_immutable`). |
|
|
72
|
+
| `name` | Required, 1-80 characters, unique per org ignoring case (409 `name_taken`). |
|
|
73
|
+
| `base` | `guest`, `creator` or `admin` (default `creator`). The role starts from this and adds `grants` / removes `denies`. |
|
|
74
|
+
| `enabled` | Default `true`. |
|
|
75
|
+
| `grants` / `denies` | Permission keys from `querri role catalog`. Normalised by the server: a grant the base already has, or a deny the base lacks, is dropped. |
|
|
76
|
+
| `settings` | `{permission_key: {"default": ..., "user_can_change": bool}}` for setting-kind permissions. |
|
|
77
|
+
| `config` | `home_chat`, `landing`, `credits_monthly`, `context_overrides`, `theme`, `support_contact`, `access_policy_ids`, with the same validation as Settings → Roles. |
|
|
78
|
+
|
|
79
|
+
The API also returns `id`, `system`, `seat_class`, `user_count` and `updated_at`. They are output only: the server ignores them on input, and `get` and `export` leave them out of what they write.
|
|
80
|
+
|
|
81
|
+
### File formats
|
|
82
|
+
|
|
83
|
+
`querri role apply -f` reads JSON that holds any of:
|
|
84
|
+
|
|
85
|
+
- one definition: applied with `PUT /roles/{key}`
|
|
86
|
+
- a list of definitions, or `{"roles": [...]}` (what `export` writes): applied with one `POST /roles/import` call, in order. Each role succeeds or fails on its own.
|
|
87
|
+
|
|
88
|
+
YAML is not supported. The SDK does not depend on PyYAML.
|
|
89
|
+
|
|
90
|
+
`apply` exits non-zero when any role failed, so it is safe to use in CI.
|
|
91
|
+
|
|
92
|
+
## CLI reference
|
|
93
|
+
|
|
94
|
+
| Command | What it does |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `querri role list` | Table of every role: key, name, base, enabled, users, system. |
|
|
97
|
+
| `querri role get REF` | The role's definition as JSON (`REF` is a key or ID). |
|
|
98
|
+
| `querri role export [-o PATH] [--include-system]` | `{"roles": [...]}` for the custom roles. |
|
|
99
|
+
| `querri role apply -f PATH [--dry-run] [--watch]` | Create or update roles from a file. |
|
|
100
|
+
| `querri role delete REF [--reassign-to KEY] [--yes]` | Delete a role. Asks first unless `--yes`. |
|
|
101
|
+
| `querri role catalog [--area AREA]` | Permission keys: key, area, kind, default per base role, requires. |
|
|
102
|
+
|
|
103
|
+
Deleting a role that some people hold as their only role needs `--reassign-to` (409 `role_in_use` otherwise).
|
|
104
|
+
|
|
105
|
+
All commands take the global `--json` flag.
|
|
106
|
+
|
|
107
|
+
## SDK reference
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from querri import Querri
|
|
111
|
+
|
|
112
|
+
client = Querri()
|
|
113
|
+
|
|
114
|
+
# Read
|
|
115
|
+
roles = client.roles.list() # every role, system ones included
|
|
116
|
+
role = client.roles.get("ev-datapro") # RoleDefinition
|
|
117
|
+
catalog = client.roles.catalog() # PermissionCatalog
|
|
118
|
+
|
|
119
|
+
# Export what you would save to a file and re-apply
|
|
120
|
+
definitions = client.roles.export() # list[dict], output-only fields stripped
|
|
121
|
+
|
|
122
|
+
# Apply one (dict or RoleDefinition); the key comes from the definition
|
|
123
|
+
result = client.roles.apply(
|
|
124
|
+
{"key": "ev-datapro", "name": "EV DataPro", "denies": ["chat.code.view"]},
|
|
125
|
+
dry_run=True,
|
|
126
|
+
)
|
|
127
|
+
print(result.action) # would_create / would_update / unchanged
|
|
128
|
+
for change in result.changes:
|
|
129
|
+
print(change.field, change.before, "->", change.after)
|
|
130
|
+
|
|
131
|
+
# Apply several in one call
|
|
132
|
+
out = client.roles.import_roles(definitions)
|
|
133
|
+
if out.failed:
|
|
134
|
+
for r in out.results:
|
|
135
|
+
if r.failed:
|
|
136
|
+
print(r.key, r.error.code, r.error.message)
|
|
137
|
+
|
|
138
|
+
# Delete
|
|
139
|
+
client.roles.delete("ev-datapro", reassign_to="creator")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`AsyncQuerri` has the same methods on `client.roles`, awaited.
|
|
143
|
+
|
|
144
|
+
`apply` raises `ValueError` when the definition has no `key`. Errors from the server raise the usual `APIError` subclasses. For example, `escalation_denied` raises `PermissionError`, and `name_taken` and `role_in_use` raise `ConflictError`.
|
|
145
|
+
|
|
146
|
+
| `result.action` | Meaning |
|
|
147
|
+
|---|---|
|
|
148
|
+
| `created` / `updated` | Written. |
|
|
149
|
+
| `unchanged` | The stored role already matches. |
|
|
150
|
+
| `would_create` / `would_update` | Dry run: what an apply would do. |
|
|
151
|
+
| `failed` | Import only: `result.error` has `code` and `message`. |
|