querri 1.0.2__py3-none-any.whl

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 (64) hide show
  1. querri/__init__.py +51 -0
  2. querri/_auth.py +818 -0
  3. querri/_base_client.py +379 -0
  4. querri/_client.py +395 -0
  5. querri/_config.py +140 -0
  6. querri/_convenience.py +538 -0
  7. querri/_exceptions.py +180 -0
  8. querri/_pagination.py +278 -0
  9. querri/_streaming.py +663 -0
  10. querri/_user_client.py +193 -0
  11. querri/_version.py +1 -0
  12. querri/cli/__init__.py +21 -0
  13. querri/cli/_app.py +246 -0
  14. querri/cli/_context.py +216 -0
  15. querri/cli/_image.py +234 -0
  16. querri/cli/_output.py +293 -0
  17. querri/cli/audit.py +68 -0
  18. querri/cli/auth.py +556 -0
  19. querri/cli/chat.py +1255 -0
  20. querri/cli/chats.py +358 -0
  21. querri/cli/dashboards.py +294 -0
  22. querri/cli/embed.py +220 -0
  23. querri/cli/files.py +214 -0
  24. querri/cli/keys.py +304 -0
  25. querri/cli/policies.py +424 -0
  26. querri/cli/projects.py +916 -0
  27. querri/cli/sharing.py +403 -0
  28. querri/cli/sources.py +532 -0
  29. querri/cli/steps.py +123 -0
  30. querri/cli/usage.py +99 -0
  31. querri/cli/users.py +223 -0
  32. querri/cli/views.py +449 -0
  33. querri/cli/whoami.py +78 -0
  34. querri/py.typed +0 -0
  35. querri/resources/__init__.py +1 -0
  36. querri/resources/audit.py +119 -0
  37. querri/resources/dashboards.py +295 -0
  38. querri/resources/embed.py +268 -0
  39. querri/resources/files.py +145 -0
  40. querri/resources/keys.py +191 -0
  41. querri/resources/policies.py +492 -0
  42. querri/resources/projects.py +808 -0
  43. querri/resources/sharing.py +330 -0
  44. querri/resources/sources.py +511 -0
  45. querri/resources/usage.py +106 -0
  46. querri/resources/users.py +303 -0
  47. querri/resources/views.py +347 -0
  48. querri/types/__init__.py +104 -0
  49. querri/types/audit.py +21 -0
  50. querri/types/chat.py +51 -0
  51. querri/types/dashboard.py +51 -0
  52. querri/types/data.py +61 -0
  53. querri/types/embed.py +38 -0
  54. querri/types/file.py +18 -0
  55. querri/types/key.py +36 -0
  56. querri/types/policy.py +105 -0
  57. querri/types/project.py +158 -0
  58. querri/types/sharing.py +19 -0
  59. querri/types/usage.py +39 -0
  60. querri/types/user.py +33 -0
  61. querri-1.0.2.dist-info/METADATA +310 -0
  62. querri-1.0.2.dist-info/RECORD +64 -0
  63. querri-1.0.2.dist-info/WHEEL +4 -0
  64. querri-1.0.2.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,191 @@
1
+ """API key management resource."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins
6
+ from typing import Any
7
+
8
+ from .._base_client import AsyncHTTPClient, SyncHTTPClient
9
+ from ..types.key import ApiKey, ApiKeyCreated
10
+
11
+
12
+ class Keys:
13
+ """Synchronous API key management resource.
14
+
15
+ Usage::
16
+
17
+ new_key = client.keys.create(name="CI Pipeline", scopes=["data:read"])
18
+ keys = client.keys.list()
19
+ """
20
+
21
+ def __init__(self, http: SyncHTTPClient) -> None:
22
+ self._http = http
23
+
24
+ def create(
25
+ self,
26
+ *,
27
+ name: str,
28
+ scopes: builtins.list[str],
29
+ expires_in_days: int | None = None,
30
+ source_scope: dict[str, Any] | None = None,
31
+ access_policy_ids: builtins.list[str] | None = None,
32
+ bound_user_id: str | None = None,
33
+ rate_limit_per_minute: int | None = None,
34
+ ip_allowlist: builtins.list[str] | None = None,
35
+ ) -> ApiKeyCreated:
36
+ """Create a new API key. Returns the plaintext secret once.
37
+
38
+ Args:
39
+ name: Display name for the key.
40
+ scopes: List of permission scopes.
41
+ expires_in_days: Days until expiration.
42
+ source_scope: Source access scope configuration.
43
+ access_policy_ids: Bound access policy UUIDs.
44
+ bound_user_id: User ID to bind RLS to.
45
+ rate_limit_per_minute: Rate limit override.
46
+ ip_allowlist: Allowed IP addresses.
47
+
48
+ Returns:
49
+ ApiKeyCreated with id, name, key_prefix, secret, scopes, status, etc.
50
+ The ``secret`` field is only returned on creation.
51
+ """
52
+ payload: dict[str, Any] = {"name": name, "scopes": scopes}
53
+ if expires_in_days is not None:
54
+ payload["expires_in_days"] = expires_in_days
55
+ if source_scope is not None:
56
+ payload["source_scope"] = source_scope
57
+ if access_policy_ids is not None:
58
+ payload["access_policy_ids"] = access_policy_ids
59
+ if bound_user_id is not None:
60
+ payload["bound_user_id"] = bound_user_id
61
+ if rate_limit_per_minute is not None:
62
+ payload["rate_limit_per_minute"] = rate_limit_per_minute
63
+ if ip_allowlist is not None:
64
+ payload["ip_allowlist"] = ip_allowlist
65
+ resp = self._http.post("/keys", json=payload)
66
+ return ApiKeyCreated.model_validate(resp.json())
67
+
68
+ def get(self, key_id: str) -> ApiKey:
69
+ """Get API key details (never returns the secret).
70
+
71
+ Args:
72
+ key_id: The key UUID.
73
+
74
+ Returns:
75
+ ApiKey object.
76
+ """
77
+ resp = self._http.get(f"/keys/{key_id}")
78
+ return ApiKey.model_validate(resp.json())
79
+
80
+ def list(self) -> builtins.list[ApiKey]:
81
+ """List API keys for the organization.
82
+
83
+ Returns:
84
+ List of ApiKey objects (secrets are never included).
85
+ """
86
+ resp = self._http.get("/keys")
87
+ body = resp.json()
88
+ return [ApiKey.model_validate(k) for k in body.get("data", [])]
89
+
90
+ def delete(self, key_id: str) -> dict[str, Any]:
91
+ """Revoke an API key.
92
+
93
+ Args:
94
+ key_id: The key UUID.
95
+
96
+ Returns:
97
+ Dict with id and status ("revoked").
98
+ """
99
+ resp = self._http.delete(f"/keys/{key_id}")
100
+ return resp.json() # type: ignore[no-any-return]
101
+
102
+
103
+ class AsyncKeys:
104
+ """Asynchronous API key management resource.
105
+
106
+ Usage::
107
+
108
+ new_key = await client.keys.create(name="CI Pipeline", scopes=["data:read"])
109
+ keys = await client.keys.list()
110
+ """
111
+
112
+ def __init__(self, http: AsyncHTTPClient) -> None:
113
+ self._http = http
114
+
115
+ async def create(
116
+ self,
117
+ *,
118
+ name: str,
119
+ scopes: builtins.list[str],
120
+ expires_in_days: int | None = None,
121
+ source_scope: dict[str, Any] | None = None,
122
+ access_policy_ids: builtins.list[str] | None = None,
123
+ bound_user_id: str | None = None,
124
+ rate_limit_per_minute: int | None = None,
125
+ ip_allowlist: builtins.list[str] | None = None,
126
+ ) -> ApiKeyCreated:
127
+ """Create a new API key. Returns the plaintext secret once.
128
+
129
+ Args:
130
+ name: Display name for the key.
131
+ scopes: List of permission scopes.
132
+ expires_in_days: Days until expiration.
133
+ source_scope: Source access scope configuration.
134
+ access_policy_ids: Bound access policy UUIDs.
135
+ bound_user_id: User ID to bind RLS to.
136
+ rate_limit_per_minute: Rate limit override.
137
+ ip_allowlist: Allowed IP addresses.
138
+
139
+ Returns:
140
+ ApiKeyCreated with id, name, key_prefix, secret, scopes, status, etc.
141
+ The ``secret`` field is only returned on creation.
142
+ """
143
+ payload: dict[str, Any] = {"name": name, "scopes": scopes}
144
+ if expires_in_days is not None:
145
+ payload["expires_in_days"] = expires_in_days
146
+ if source_scope is not None:
147
+ payload["source_scope"] = source_scope
148
+ if access_policy_ids is not None:
149
+ payload["access_policy_ids"] = access_policy_ids
150
+ if bound_user_id is not None:
151
+ payload["bound_user_id"] = bound_user_id
152
+ if rate_limit_per_minute is not None:
153
+ payload["rate_limit_per_minute"] = rate_limit_per_minute
154
+ if ip_allowlist is not None:
155
+ payload["ip_allowlist"] = ip_allowlist
156
+ resp = await self._http.post("/keys", json=payload)
157
+ return ApiKeyCreated.model_validate(resp.json())
158
+
159
+ async def get(self, key_id: str) -> ApiKey:
160
+ """Get API key details (never returns the secret).
161
+
162
+ Args:
163
+ key_id: The key UUID.
164
+
165
+ Returns:
166
+ ApiKey object.
167
+ """
168
+ resp = await self._http.get(f"/keys/{key_id}")
169
+ return ApiKey.model_validate(resp.json())
170
+
171
+ async def list(self) -> builtins.list[ApiKey]:
172
+ """List API keys for the organization.
173
+
174
+ Returns:
175
+ List of ApiKey objects (secrets are never included).
176
+ """
177
+ resp = await self._http.get("/keys")
178
+ body = resp.json()
179
+ return [ApiKey.model_validate(k) for k in body.get("data", [])]
180
+
181
+ async def delete(self, key_id: str) -> dict[str, Any]:
182
+ """Revoke an API key.
183
+
184
+ Args:
185
+ key_id: The key UUID.
186
+
187
+ Returns:
188
+ Dict with id and status ("revoked").
189
+ """
190
+ resp = await self._http.delete(f"/keys/{key_id}")
191
+ return resp.json() # type: ignore[no-any-return]
@@ -0,0 +1,492 @@
1
+ """Access policy resources — maps to /access endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins
6
+ from typing import Any
7
+
8
+ from .._base_client import AsyncHTTPClient, SyncHTTPClient
9
+ from .._convenience import async_setup_policy, sync_setup_policy
10
+ from .._pagination import AsyncCursorPage, SyncCursorPage
11
+ from ..types.policy import (
12
+ Policy,
13
+ PolicyAssignResponse,
14
+ PolicyDeleteResponse,
15
+ PolicyRemoveUserResponse,
16
+ PolicyReplaceResponse,
17
+ PolicyUpdateResponse,
18
+ ResolvedAccess,
19
+ SourceColumns,
20
+ )
21
+
22
+
23
+ class Policies:
24
+ """Synchronous access policy management.
25
+
26
+ Usage::
27
+
28
+ policy = client.policies.create(
29
+ name="Sales Team",
30
+ source_ids=["src_..."],
31
+ row_filters=[{"column": "region", "values": ["US"]}],
32
+ )
33
+ """
34
+
35
+ def __init__(self, http: SyncHTTPClient) -> None:
36
+ self._http = http
37
+
38
+ def create(
39
+ self,
40
+ *,
41
+ name: str,
42
+ description: str | None = None,
43
+ source_ids: builtins.list[str] | None = None,
44
+ row_filters: builtins.list[dict[str, Any]] | None = None,
45
+ ) -> Policy:
46
+ """Create an access policy.
47
+
48
+ Args:
49
+ name: Policy name.
50
+ description: Optional description.
51
+ source_ids: List of source IDs this policy applies to.
52
+ row_filters: List of row filter dicts with ``column`` and ``values`` keys.
53
+ """
54
+ body: dict[str, Any] = {"name": name}
55
+ if description is not None:
56
+ body["description"] = description
57
+ if source_ids is not None:
58
+ body["source_ids"] = source_ids
59
+ if row_filters is not None:
60
+ body["row_filters"] = row_filters
61
+ resp = self._http.post("/access/policies", json=body)
62
+ return Policy.model_validate(resp.json())
63
+
64
+ def get(self, policy_id: str) -> Policy:
65
+ """Get policy details including assigned user IDs.
66
+
67
+ Args:
68
+ policy_id: The policy UUID.
69
+ """
70
+ resp = self._http.get(f"/access/policies/{policy_id}")
71
+ return Policy.model_validate(resp.json())
72
+
73
+ def list(
74
+ self,
75
+ *,
76
+ name: str | None = None,
77
+ limit: int = 25,
78
+ after: str | None = None,
79
+ ) -> SyncCursorPage[Policy]:
80
+ """List access policies for the organization with cursor pagination.
81
+
82
+ Args:
83
+ name: Filter by exact policy name.
84
+ limit: Maximum number of policies per page (1-200).
85
+ after: Cursor for the next page.
86
+ """
87
+ params: dict[str, Any] = {"limit": limit}
88
+ if name is not None:
89
+ params["name"] = name
90
+ if after is not None:
91
+ params["after"] = after
92
+ return SyncCursorPage(self._http, "/access/policies", Policy, params=params)
93
+
94
+ def update(
95
+ self,
96
+ policy_id: str,
97
+ *,
98
+ name: str | None = None,
99
+ description: str | None = None,
100
+ source_ids: builtins.list[str] | None = None,
101
+ row_filters: builtins.list[dict[str, Any]] | None = None,
102
+ ) -> PolicyUpdateResponse:
103
+ """Update an access policy.
104
+
105
+ Args:
106
+ policy_id: The policy UUID.
107
+ name: New name.
108
+ description: New description.
109
+ source_ids: New source IDs.
110
+ row_filters: New row filters.
111
+ """
112
+ body: dict[str, Any] = {}
113
+ if name is not None:
114
+ body["name"] = name
115
+ if description is not None:
116
+ body["description"] = description
117
+ if source_ids is not None:
118
+ body["source_ids"] = source_ids
119
+ if row_filters is not None:
120
+ body["row_filters"] = row_filters
121
+ resp = self._http.patch(f"/access/policies/{policy_id}", json=body)
122
+ return PolicyUpdateResponse.model_validate(resp.json())
123
+
124
+ def delete(self, policy_id: str) -> PolicyDeleteResponse:
125
+ """Delete an access policy and all its user assignments.
126
+
127
+ Args:
128
+ policy_id: The policy UUID.
129
+ """
130
+ resp = self._http.delete(f"/access/policies/{policy_id}")
131
+ return PolicyDeleteResponse.model_validate(resp.json())
132
+
133
+ def assign_users(
134
+ self,
135
+ policy_id: str,
136
+ *,
137
+ user_ids: builtins.list[str],
138
+ ) -> PolicyAssignResponse:
139
+ """Assign users to a policy.
140
+
141
+ Idempotent: already-assigned users are silently skipped.
142
+
143
+ Args:
144
+ policy_id: The policy UUID.
145
+ user_ids: List of WorkOS user IDs to assign.
146
+ """
147
+ resp = self._http.post(
148
+ f"/access/policies/{policy_id}/users",
149
+ json={"user_ids": user_ids},
150
+ )
151
+ return PolicyAssignResponse.model_validate(resp.json())
152
+
153
+ def remove_user(
154
+ self,
155
+ policy_id: str,
156
+ user_id: str,
157
+ ) -> PolicyRemoveUserResponse:
158
+ """Remove a user from a policy.
159
+
160
+ Args:
161
+ policy_id: The policy UUID.
162
+ user_id: The WorkOS user ID to remove.
163
+ """
164
+ resp = self._http.delete(f"/access/policies/{policy_id}/users/{user_id}")
165
+ return PolicyRemoveUserResponse.model_validate(resp.json())
166
+
167
+ def resolve(
168
+ self,
169
+ *,
170
+ user_id: str,
171
+ source_id: str,
172
+ ) -> ResolvedAccess:
173
+ """Preview resolved access for a user+source combination.
174
+
175
+ Returns the effective row filters and SQL WHERE clause that would
176
+ be applied when querying the given source as the given user.
177
+
178
+ Args:
179
+ user_id: WorkOS user ID.
180
+ source_id: Source UUID.
181
+ """
182
+ resp = self._http.post(
183
+ "/access/resolve",
184
+ json={"user_id": user_id, "source_id": source_id},
185
+ )
186
+ return ResolvedAccess.model_validate(resp.json())
187
+
188
+ def columns(
189
+ self,
190
+ *,
191
+ source_id: str | None = None,
192
+ ) -> builtins.list[SourceColumns]:
193
+ """Discover filterable columns for RLS rule building.
194
+
195
+ Returns column names and types from data sources so you can
196
+ construct valid ``row_filters`` for access policies.
197
+
198
+ Args:
199
+ source_id: Optional source ID to filter to a single source.
200
+ """
201
+ params: dict[str, Any] = {}
202
+ if source_id is not None:
203
+ params["source_id"] = source_id
204
+ resp = self._http.get("/access/columns", params=params)
205
+ body = resp.json()
206
+ return [SourceColumns.model_validate(s) for s in body.get("data", [])]
207
+
208
+ def replace_user_policies(
209
+ self, user_id: str, *, policy_ids: builtins.list[str]
210
+ ) -> PolicyReplaceResponse:
211
+ """Atomically replace all policy assignments for a user.
212
+
213
+ Removes any existing policy assignments and replaces them with the
214
+ provided set. This prevents policy accumulation from repeated
215
+ ``get_session()`` calls.
216
+
217
+ Args:
218
+ user_id: The user ID whose policies to replace.
219
+ policy_ids: Complete set of policy IDs to assign. Pass ``[]`` to
220
+ remove all policies.
221
+ """
222
+ resp = self._http.put(
223
+ f"/access/users/{user_id}/policies",
224
+ json={"policy_ids": policy_ids},
225
+ )
226
+ return PolicyReplaceResponse.model_validate(resp.json())
227
+
228
+ def setup(
229
+ self,
230
+ *,
231
+ name: str,
232
+ sources: builtins.list[str] | None = None,
233
+ row_filters: dict[str, Any] | None = None,
234
+ users: builtins.list[str] | None = None,
235
+ description: str | None = None,
236
+ ) -> Policy:
237
+ """Create a policy and assign users in one call.
238
+
239
+ Args:
240
+ name: Policy name.
241
+ sources: Source IDs this policy applies to.
242
+ row_filters: Dict of column -> values for row filtering.
243
+ users: User IDs to assign to the policy.
244
+ description: Optional policy description.
245
+
246
+ Returns:
247
+ Created Policy object.
248
+ """
249
+ return sync_setup_policy(
250
+ self._http,
251
+ name=name,
252
+ sources=sources,
253
+ row_filters=row_filters,
254
+ users=users,
255
+ description=description,
256
+ )
257
+
258
+
259
+ class AsyncPolicies:
260
+ """Asynchronous access policy management.
261
+
262
+ Usage::
263
+
264
+ policy = await client.policies.create(
265
+ name="Sales Team",
266
+ source_ids=["src_..."],
267
+ row_filters=[{"column": "region", "values": ["US"]}],
268
+ )
269
+ """
270
+
271
+ def __init__(self, http: AsyncHTTPClient) -> None:
272
+ self._http = http
273
+
274
+ async def create(
275
+ self,
276
+ *,
277
+ name: str,
278
+ description: str | None = None,
279
+ source_ids: builtins.list[str] | None = None,
280
+ row_filters: builtins.list[dict[str, Any]] | None = None,
281
+ ) -> Policy:
282
+ """Create an access policy.
283
+
284
+ Args:
285
+ name: Policy name.
286
+ description: Optional description.
287
+ source_ids: List of source IDs this policy applies to.
288
+ row_filters: List of row filter dicts with ``column`` and ``values`` keys.
289
+ """
290
+ body: dict[str, Any] = {"name": name}
291
+ if description is not None:
292
+ body["description"] = description
293
+ if source_ids is not None:
294
+ body["source_ids"] = source_ids
295
+ if row_filters is not None:
296
+ body["row_filters"] = row_filters
297
+ resp = await self._http.post("/access/policies", json=body)
298
+ return Policy.model_validate(resp.json())
299
+
300
+ async def get(self, policy_id: str) -> Policy:
301
+ """Get policy details including assigned user IDs.
302
+
303
+ Args:
304
+ policy_id: The policy UUID.
305
+ """
306
+ resp = await self._http.get(f"/access/policies/{policy_id}")
307
+ return Policy.model_validate(resp.json())
308
+
309
+ def list(
310
+ self,
311
+ *,
312
+ name: str | None = None,
313
+ limit: int = 25,
314
+ after: str | None = None,
315
+ ) -> AsyncCursorPage[Policy]:
316
+ """List access policies for the organization with cursor pagination.
317
+
318
+ Args:
319
+ name: Filter by exact policy name.
320
+ limit: Maximum number of policies per page (1-200).
321
+ after: Cursor for the next page.
322
+ """
323
+ params: dict[str, Any] = {"limit": limit}
324
+ if name is not None:
325
+ params["name"] = name
326
+ if after is not None:
327
+ params["after"] = after
328
+ return AsyncCursorPage(self._http, "/access/policies", Policy, params=params)
329
+
330
+ async def update(
331
+ self,
332
+ policy_id: str,
333
+ *,
334
+ name: str | None = None,
335
+ description: str | None = None,
336
+ source_ids: builtins.list[str] | None = None,
337
+ row_filters: builtins.list[dict[str, Any]] | None = None,
338
+ ) -> PolicyUpdateResponse:
339
+ """Update an access policy.
340
+
341
+ Args:
342
+ policy_id: The policy UUID.
343
+ name: New name.
344
+ description: New description.
345
+ source_ids: New source IDs.
346
+ row_filters: New row filters.
347
+ """
348
+ body: dict[str, Any] = {}
349
+ if name is not None:
350
+ body["name"] = name
351
+ if description is not None:
352
+ body["description"] = description
353
+ if source_ids is not None:
354
+ body["source_ids"] = source_ids
355
+ if row_filters is not None:
356
+ body["row_filters"] = row_filters
357
+ resp = await self._http.patch(f"/access/policies/{policy_id}", json=body)
358
+ return PolicyUpdateResponse.model_validate(resp.json())
359
+
360
+ async def delete(self, policy_id: str) -> PolicyDeleteResponse:
361
+ """Delete an access policy and all its user assignments.
362
+
363
+ Args:
364
+ policy_id: The policy UUID.
365
+ """
366
+ resp = await self._http.delete(f"/access/policies/{policy_id}")
367
+ return PolicyDeleteResponse.model_validate(resp.json())
368
+
369
+ async def assign_users(
370
+ self,
371
+ policy_id: str,
372
+ *,
373
+ user_ids: builtins.list[str],
374
+ ) -> PolicyAssignResponse:
375
+ """Assign users to a policy.
376
+
377
+ Idempotent: already-assigned users are silently skipped.
378
+
379
+ Args:
380
+ policy_id: The policy UUID.
381
+ user_ids: List of WorkOS user IDs to assign.
382
+ """
383
+ resp = await self._http.post(
384
+ f"/access/policies/{policy_id}/users",
385
+ json={"user_ids": user_ids},
386
+ )
387
+ return PolicyAssignResponse.model_validate(resp.json())
388
+
389
+ async def remove_user(
390
+ self,
391
+ policy_id: str,
392
+ user_id: str,
393
+ ) -> PolicyRemoveUserResponse:
394
+ """Remove a user from a policy.
395
+
396
+ Args:
397
+ policy_id: The policy UUID.
398
+ user_id: The WorkOS user ID to remove.
399
+ """
400
+ resp = await self._http.delete(f"/access/policies/{policy_id}/users/{user_id}")
401
+ return PolicyRemoveUserResponse.model_validate(resp.json())
402
+
403
+ async def resolve(
404
+ self,
405
+ *,
406
+ user_id: str,
407
+ source_id: str,
408
+ ) -> ResolvedAccess:
409
+ """Preview resolved access for a user+source combination.
410
+
411
+ Returns the effective row filters and SQL WHERE clause that would
412
+ be applied when querying the given source as the given user.
413
+
414
+ Args:
415
+ user_id: WorkOS user ID.
416
+ source_id: Source UUID.
417
+ """
418
+ resp = await self._http.post(
419
+ "/access/resolve",
420
+ json={"user_id": user_id, "source_id": source_id},
421
+ )
422
+ return ResolvedAccess.model_validate(resp.json())
423
+
424
+ async def columns(
425
+ self,
426
+ *,
427
+ source_id: str | None = None,
428
+ ) -> builtins.list[SourceColumns]:
429
+ """Discover filterable columns for RLS rule building.
430
+
431
+ Returns column names and types from data sources so you can
432
+ construct valid ``row_filters`` for access policies.
433
+
434
+ Args:
435
+ source_id: Optional source ID to filter to a single source.
436
+ """
437
+ params: dict[str, Any] = {}
438
+ if source_id is not None:
439
+ params["source_id"] = source_id
440
+ resp = await self._http.get("/access/columns", params=params)
441
+ body = resp.json()
442
+ return [SourceColumns.model_validate(s) for s in body.get("data", [])]
443
+
444
+ async def replace_user_policies(
445
+ self, user_id: str, *, policy_ids: builtins.list[str]
446
+ ) -> PolicyReplaceResponse:
447
+ """Atomically replace all policy assignments for a user.
448
+
449
+ Removes any existing policy assignments and replaces them with the
450
+ provided set. This prevents policy accumulation from repeated
451
+ ``get_session()`` calls.
452
+
453
+ Args:
454
+ user_id: The user ID whose policies to replace.
455
+ policy_ids: Complete set of policy IDs to assign. Pass ``[]`` to
456
+ remove all policies.
457
+ """
458
+ resp = await self._http.put(
459
+ f"/access/users/{user_id}/policies",
460
+ json={"policy_ids": policy_ids},
461
+ )
462
+ return PolicyReplaceResponse.model_validate(resp.json())
463
+
464
+ async def setup(
465
+ self,
466
+ *,
467
+ name: str,
468
+ sources: builtins.list[str] | None = None,
469
+ row_filters: dict[str, Any] | None = None,
470
+ users: builtins.list[str] | None = None,
471
+ description: str | None = None,
472
+ ) -> Policy:
473
+ """Create a policy and assign users in one call.
474
+
475
+ Args:
476
+ name: Policy name.
477
+ sources: Source IDs this policy applies to.
478
+ row_filters: Dict of column -> values for row filtering.
479
+ users: User IDs to assign to the policy.
480
+ description: Optional policy description.
481
+
482
+ Returns:
483
+ Created Policy object.
484
+ """
485
+ return await async_setup_policy(
486
+ self._http,
487
+ name=name,
488
+ sources=sources,
489
+ row_filters=row_filters,
490
+ users=users,
491
+ description=description,
492
+ )