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.
Files changed (132) hide show
  1. querri-2.2.0/CHANGELOG.md +238 -0
  2. {querri-2.1.0 → querri-2.2.0}/CLAUDE.md +4 -4
  3. {querri-2.1.0 → querri-2.2.0}/PKG-INFO +58 -2
  4. {querri-2.1.0 → querri-2.2.0}/README.md +57 -1
  5. {querri-2.1.0 → querri-2.2.0}/docs/data-library.md +6 -1
  6. querri-2.2.0/docs/roles.md +151 -0
  7. {querri-2.1.0 → querri-2.2.0}/docs/server-sdk.md +212 -5
  8. {querri-2.1.0 → querri-2.2.0}/examples/05_get_session.py +29 -1
  9. {querri-2.1.0 → querri-2.2.0}/examples/10_error_handling.py +14 -0
  10. {querri-2.1.0 → querri-2.2.0}/examples/web-embed/server.py +46 -3
  11. querri-2.2.0/querri/__init__.py +110 -0
  12. {querri-2.1.0 → querri-2.2.0}/querri/_auth.py +12 -1
  13. {querri-2.1.0 → querri-2.2.0}/querri/_base_client.py +42 -4
  14. {querri-2.1.0 → querri-2.2.0}/querri/_client.py +29 -0
  15. {querri-2.1.0 → querri-2.2.0}/querri/_config.py +11 -0
  16. {querri-2.1.0 → querri-2.2.0}/querri/_convenience.py +25 -1
  17. querri-2.2.0/querri/_exceptions.py +440 -0
  18. {querri-2.1.0 → querri-2.2.0}/querri/_user_client.py +6 -2
  19. querri-2.2.0/querri/_version.py +1 -0
  20. {querri-2.1.0 → querri-2.2.0}/querri/cli/_app.py +6 -0
  21. {querri-2.1.0 → querri-2.2.0}/querri/cli/_context.py +20 -6
  22. {querri-2.1.0 → querri-2.2.0}/querri/cli/_output.py +63 -4
  23. {querri-2.1.0 → querri-2.2.0}/querri/cli/auth.py +6 -1
  24. {querri-2.1.0 → querri-2.2.0}/querri/cli/embed.py +21 -3
  25. {querri-2.1.0 → querri-2.2.0}/querri/cli/keys.py +2 -1
  26. querri-2.2.0/querri/cli/roles.py +411 -0
  27. {querri-2.1.0 → querri-2.2.0}/querri/cli/sources.py +1 -0
  28. {querri-2.1.0 → querri-2.2.0}/querri/cli/views.py +2 -1
  29. {querri-2.1.0 → querri-2.2.0}/querri/resources/embed.py +66 -6
  30. querri-2.2.0/querri/resources/roles.py +324 -0
  31. {querri-2.1.0 → querri-2.2.0}/querri/resources/users.py +16 -0
  32. {querri-2.1.0 → querri-2.2.0}/querri/types/__init__.py +19 -0
  33. {querri-2.1.0 → querri-2.2.0}/querri/types/embed.py +2 -0
  34. querri-2.2.0/querri/types/role.py +136 -0
  35. {querri-2.1.0 → querri-2.2.0}/skills/querri-cli/SKILL.md +55 -5
  36. {querri-2.1.0 → querri-2.2.0}/skills/querri-cli/references/json-shapes.md +71 -8
  37. {querri-2.1.0 → querri-2.2.0}/tests/test_cli.py +58 -3
  38. {querri-2.1.0 → querri-2.2.0}/tests/test_cli_commands.py +222 -0
  39. querri-2.2.0/tests/test_cli_json_errors.py +402 -0
  40. querri-2.2.0/tests/test_client_header.py +62 -0
  41. {querri-2.1.0 → querri-2.2.0}/tests/test_convenience.py +149 -0
  42. querri-2.2.0/tests/test_exceptions.py +977 -0
  43. {querri-2.1.0 → querri-2.2.0}/tests/test_resources.py +199 -1
  44. querri-2.2.0/tests/test_roles.py +529 -0
  45. {querri-2.1.0 → querri-2.2.0}/tests/test_security.py +49 -0
  46. querri-2.1.0/CHANGELOG.md +0 -109
  47. querri-2.1.0/querri/__init__.py +0 -53
  48. querri-2.1.0/querri/_exceptions.py +0 -220
  49. querri-2.1.0/querri/_version.py +0 -1
  50. querri-2.1.0/tests/test_exceptions.py +0 -196
  51. {querri-2.1.0 → querri-2.2.0}/.github/CONTRIBUTING.md +0 -0
  52. {querri-2.1.0 → querri-2.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  53. {querri-2.1.0 → querri-2.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  54. {querri-2.1.0 → querri-2.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  55. {querri-2.1.0 → querri-2.2.0}/.github/SECURITY.md +0 -0
  56. {querri-2.1.0 → querri-2.2.0}/.github/workflows/ci.yml +0 -0
  57. {querri-2.1.0 → querri-2.2.0}/.github/workflows/release.yml +0 -0
  58. {querri-2.1.0 → querri-2.2.0}/.gitignore +0 -0
  59. {querri-2.1.0 → querri-2.2.0}/LICENSE +0 -0
  60. {querri-2.1.0 → querri-2.2.0}/TODO.md +0 -0
  61. {querri-2.1.0 → querri-2.2.0}/docs/MIGRATION.md +0 -0
  62. {querri-2.1.0 → querri-2.2.0}/examples/01_quick_start.py +0 -0
  63. {querri-2.1.0 → querri-2.2.0}/examples/02_user_management.py +0 -0
  64. {querri-2.1.0 → querri-2.2.0}/examples/03_access_policies.py +0 -0
  65. {querri-2.1.0 → querri-2.2.0}/examples/04_embed_session.py +0 -0
  66. {querri-2.1.0 → querri-2.2.0}/examples/06_projects.py +0 -0
  67. {querri-2.1.0 → querri-2.2.0}/examples/07_dashboards.py +0 -0
  68. {querri-2.1.0 → querri-2.2.0}/examples/08_chat_streaming.py +0 -0
  69. {querri-2.1.0 → querri-2.2.0}/examples/09_async_client.py +0 -0
  70. {querri-2.1.0 → querri-2.2.0}/examples/11_user_scoped_client.py +0 -0
  71. {querri-2.1.0 → querri-2.2.0}/examples/12_data_operations.py +0 -0
  72. {querri-2.1.0 → querri-2.2.0}/examples/web-embed/index.html +0 -0
  73. {querri-2.1.0 → querri-2.2.0}/pyproject.toml +0 -0
  74. {querri-2.1.0 → querri-2.2.0}/querri/_pagination.py +0 -0
  75. {querri-2.1.0 → querri-2.2.0}/querri/_streaming.py +0 -0
  76. {querri-2.1.0 → querri-2.2.0}/querri/cli/__init__.py +0 -0
  77. {querri-2.1.0 → querri-2.2.0}/querri/cli/_image.py +0 -0
  78. {querri-2.1.0 → querri-2.2.0}/querri/cli/audit.py +0 -0
  79. {querri-2.1.0 → querri-2.2.0}/querri/cli/chat.py +0 -0
  80. {querri-2.1.0 → querri-2.2.0}/querri/cli/chats.py +0 -0
  81. {querri-2.1.0 → querri-2.2.0}/querri/cli/dashboards.py +0 -0
  82. {querri-2.1.0 → querri-2.2.0}/querri/cli/files.py +0 -0
  83. {querri-2.1.0 → querri-2.2.0}/querri/cli/library.py +0 -0
  84. {querri-2.1.0 → querri-2.2.0}/querri/cli/policies.py +0 -0
  85. {querri-2.1.0 → querri-2.2.0}/querri/cli/projects.py +0 -0
  86. {querri-2.1.0 → querri-2.2.0}/querri/cli/sharing.py +0 -0
  87. {querri-2.1.0 → querri-2.2.0}/querri/cli/steps.py +0 -0
  88. {querri-2.1.0 → querri-2.2.0}/querri/cli/usage.py +0 -0
  89. {querri-2.1.0 → querri-2.2.0}/querri/cli/users.py +0 -0
  90. {querri-2.1.0 → querri-2.2.0}/querri/cli/whoami.py +0 -0
  91. {querri-2.1.0 → querri-2.2.0}/querri/py.typed +0 -0
  92. {querri-2.1.0 → querri-2.2.0}/querri/resources/__init__.py +0 -0
  93. {querri-2.1.0 → querri-2.2.0}/querri/resources/audit.py +0 -0
  94. {querri-2.1.0 → querri-2.2.0}/querri/resources/dashboards.py +0 -0
  95. {querri-2.1.0 → querri-2.2.0}/querri/resources/files.py +0 -0
  96. {querri-2.1.0 → querri-2.2.0}/querri/resources/keys.py +0 -0
  97. {querri-2.1.0 → querri-2.2.0}/querri/resources/library.py +0 -0
  98. {querri-2.1.0 → querri-2.2.0}/querri/resources/policies.py +0 -0
  99. {querri-2.1.0 → querri-2.2.0}/querri/resources/projects.py +0 -0
  100. {querri-2.1.0 → querri-2.2.0}/querri/resources/sharing.py +0 -0
  101. {querri-2.1.0 → querri-2.2.0}/querri/resources/sources.py +0 -0
  102. {querri-2.1.0 → querri-2.2.0}/querri/resources/usage.py +0 -0
  103. {querri-2.1.0 → querri-2.2.0}/querri/resources/views.py +0 -0
  104. {querri-2.1.0 → querri-2.2.0}/querri/types/audit.py +0 -0
  105. {querri-2.1.0 → querri-2.2.0}/querri/types/chat.py +0 -0
  106. {querri-2.1.0 → querri-2.2.0}/querri/types/dashboard.py +0 -0
  107. {querri-2.1.0 → querri-2.2.0}/querri/types/data.py +0 -0
  108. {querri-2.1.0 → querri-2.2.0}/querri/types/file.py +0 -0
  109. {querri-2.1.0 → querri-2.2.0}/querri/types/key.py +0 -0
  110. {querri-2.1.0 → querri-2.2.0}/querri/types/library.py +0 -0
  111. {querri-2.1.0 → querri-2.2.0}/querri/types/policy.py +0 -0
  112. {querri-2.1.0 → querri-2.2.0}/querri/types/project.py +0 -0
  113. {querri-2.1.0 → querri-2.2.0}/querri/types/sharing.py +0 -0
  114. {querri-2.1.0 → querri-2.2.0}/querri/types/usage.py +0 -0
  115. {querri-2.1.0 → querri-2.2.0}/querri/types/user.py +0 -0
  116. {querri-2.1.0 → querri-2.2.0}/scripts/release.sh +0 -0
  117. {querri-2.1.0 → querri-2.2.0}/skills/README.md +0 -0
  118. {querri-2.1.0 → querri-2.2.0}/skills/querri-cli/references/troubleshooting.md +0 -0
  119. {querri-2.1.0 → querri-2.2.0}/tests/__init__.py +0 -0
  120. {querri-2.1.0 → querri-2.2.0}/tests/conftest.py +0 -0
  121. {querri-2.1.0 → querri-2.2.0}/tests/rls-integration-results.md +0 -0
  122. {querri-2.1.0 → querri-2.2.0}/tests/test_client.py +0 -0
  123. {querri-2.1.0 → querri-2.2.0}/tests/test_config.py +0 -0
  124. {querri-2.1.0 → querri-2.2.0}/tests/test_integration.py +0 -0
  125. {querri-2.1.0 → querri-2.2.0}/tests/test_library.py +0 -0
  126. {querri-2.1.0 → querri-2.2.0}/tests/test_library_streaming.py +0 -0
  127. {querri-2.1.0 → querri-2.2.0}/tests/test_new_features.py +0 -0
  128. {querri-2.1.0 → querri-2.2.0}/tests/test_pagination.py +0 -0
  129. {querri-2.1.0 → querri-2.2.0}/tests/test_rls_integration.py +0 -0
  130. {querri-2.1.0 → querri-2.2.0}/tests/test_streaming.py +0 -0
  131. {querri-2.1.0 → querri-2.2.0}/tests/test_streaming_v2.py +0 -0
  132. {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.9.
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 internal API (`/api` instead of `/api/v1`). Only expose FGA-filtered resources: `projects`, `dashboards`, `sources`, `data`, `chats`.
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.9
92
- - **ruff**: Rules E, F, I, N, UP, B, SIM (target Python 3.9)
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.1.0
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 gives both scopes to `member` role.
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`. |