permio 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. permio-0.1.0/LICENSE +202 -0
  2. permio-0.1.0/PKG-INFO +399 -0
  3. permio-0.1.0/README.md +357 -0
  4. permio-0.1.0/pyproject.toml +120 -0
  5. permio-0.1.0/setup.cfg +4 -0
  6. permio-0.1.0/src/permio/__init__.py +100 -0
  7. permio-0.1.0/src/permio/_access.py +312 -0
  8. permio-0.1.0/src/permio/_apikey.py +73 -0
  9. permio-0.1.0/src/permio/_async_client.py +747 -0
  10. permio-0.1.0/src/permio/_client.py +735 -0
  11. permio-0.1.0/src/permio/_core.py +336 -0
  12. permio-0.1.0/src/permio/_errors.py +268 -0
  13. permio-0.1.0/src/permio/_resource.py +99 -0
  14. permio-0.1.0/src/permio/_transport.py +313 -0
  15. permio-0.1.0/src/permio/_version.py +1 -0
  16. permio-0.1.0/src/permio/organisation/__init__.py +0 -0
  17. permio-0.1.0/src/permio/organisation/v1/__init__.py +0 -0
  18. permio-0.1.0/src/permio/organisation/v1/organisation_pb2.py +233 -0
  19. permio-0.1.0/src/permio/organisation/v1/organisation_pb2.pyi +328 -0
  20. permio-0.1.0/src/permio/organisation/v1/organisation_pb2_grpc.py +802 -0
  21. permio-0.1.0/src/permio/pagination/__init__.py +0 -0
  22. permio-0.1.0/src/permio/pagination/v1/__init__.py +0 -0
  23. permio-0.1.0/src/permio/pagination/v1/pagination_pb2.py +45 -0
  24. permio-0.1.0/src/permio/pagination/v1/pagination_pb2.pyi +23 -0
  25. permio-0.1.0/src/permio/pagination/v1/pagination_pb2_grpc.py +4 -0
  26. permio-0.1.0/src/permio/permissions/__init__.py +0 -0
  27. permio-0.1.0/src/permio/permissions/v1/__init__.py +0 -0
  28. permio-0.1.0/src/permio/permissions/v1/permissions_pb2.py +398 -0
  29. permio-0.1.0/src/permio/permissions/v1/permissions_pb2.pyi +665 -0
  30. permio-0.1.0/src/permio/permissions/v1/permissions_pb2_grpc.py +1466 -0
  31. permio-0.1.0/src/permio/py.typed +0 -0
  32. permio-0.1.0/src/permio/raw.py +34 -0
  33. permio-0.1.0/src/permio/validate/__init__.py +0 -0
  34. permio-0.1.0/src/permio/validate/validate_pb2.py +86 -0
  35. permio-0.1.0/src/permio/validate/validate_pb2.pyi +497 -0
  36. permio-0.1.0/src/permio.egg-info/PKG-INFO +399 -0
  37. permio-0.1.0/src/permio.egg-info/SOURCES.txt +43 -0
  38. permio-0.1.0/src/permio.egg-info/dependency_links.txt +1 -0
  39. permio-0.1.0/src/permio.egg-info/requires.txt +16 -0
  40. permio-0.1.0/src/permio.egg-info/top_level.txt +1 -0
  41. permio-0.1.0/tests/test_async_client.py +191 -0
  42. permio-0.1.0/tests/test_client.py +579 -0
  43. permio-0.1.0/tests/test_groups.py +90 -0
  44. permio-0.1.0/tests/test_helpers.py +225 -0
  45. permio-0.1.0/tests/test_live.py +157 -0
permio-0.1.0/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2026 PrivateJAR
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
permio-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,399 @@
1
+ Metadata-Version: 2.4
2
+ Name: permio
3
+ Version: 0.1.0
4
+ Summary: Python SDK for perms.io: permission checks, grants, groups and access queries over gRPC
5
+ Author-email: PrivateJAR <support@perms.io>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://perms.io
8
+ Project-URL: Documentation, https://docs.perms.io
9
+ Project-URL: Source, https://github.com/PrivateJAR/permio-python
10
+ Project-URL: Changelog, https://github.com/PrivateJAR/permio-python/blob/main/CHANGELOG.md
11
+ Keywords: permissions,rbac,authorization,perms.io,grpc
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Security
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: protobuf<8,>=6.33.5
28
+ Requires-Dist: grpcio<2,>=1.76.0
29
+ Requires-Dist: googleapis-common-protos<2,>=1.70.0
30
+ Provides-Extra: test
31
+ Requires-Dist: pytest>=8; extra == "test"
32
+ Requires-Dist: pytest-asyncio>=0.24; extra == "test"
33
+ Provides-Extra: dev
34
+ Requires-Dist: permio[test]; extra == "dev"
35
+ Requires-Dist: ruff>=0.8; extra == "dev"
36
+ Requires-Dist: mypy>=1.13; extra == "dev"
37
+ Requires-Dist: types-protobuf; extra == "dev"
38
+ Requires-Dist: types-grpcio; extra == "dev"
39
+ Requires-Dist: build; extra == "dev"
40
+ Requires-Dist: twine; extra == "dev"
41
+ Dynamic: license-file
42
+
43
+ # permio
44
+
45
+ Python SDK for [perms.io](https://perms.io), permissions as a service with
46
+ hierarchical resource-URI RBAC.
47
+
48
+ - One call to ask "may this principal do X on this resource?"
49
+ - Batch checks, grant and revoke, groups, and "where can they do X?" queries that turn into database filters
50
+ - Sync (`Permio`) and asyncio (`AsyncPermio`) clients with the same surface
51
+ - Typed errors, retries for idempotent reads, iterators over list endpoints, fully typed (`py.typed`)
52
+ - The full generated gRPC API (every RPC of `PermissionsService` and `OrganisationService`) as an escape hatch
53
+ - Python 3.9+, over gRPC (`grpcio`)
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ pip install permio
59
+ ```
60
+
61
+ > **Not on PyPI yet.** The package will be published by the first tagged
62
+ > release (see [Releasing](#releasing)). Until then, install from GitHub:
63
+ >
64
+ > ```bash
65
+ > pip install "git+https://github.com/PrivateJAR/permio-python@main"
66
+ > ```
67
+
68
+ ## Quick start
69
+
70
+ Create an API key in the [dashboard](https://perms.io) (keys look like `perms_`
71
+ followed by 38 letters and digits), create a permission such as `docs.read`,
72
+ and grant it to a principal such as `alice` on `/docs/42` (or from code with
73
+ `permio.grant()`). Principal IDs are your own identifiers: user IDs, service
74
+ names, anything stable.
75
+
76
+ ```python
77
+ from permio import Permio, resource
78
+
79
+ permio = Permio(project="my-app") # API key from $PERMIO_API_KEY
80
+ if permio.check("alice", "docs.read", resource("docs", "42")):
81
+ print("alice may read doc 42")
82
+ ```
83
+
84
+ The same with asyncio:
85
+
86
+ ```python
87
+ from permio import AsyncPermio, resource
88
+
89
+ async with AsyncPermio(project="my-app") as permio:
90
+ allowed = await permio.check("alice", "docs.read", resource("docs", "42"))
91
+ ```
92
+
93
+ Both clients are context managers; `close()` (or `await close()`) closes the
94
+ gRPC channel. Create one client per process and share it.
95
+
96
+ ## Configuration
97
+
98
+ ```python
99
+ from permio import DEV_BASE_URL, Permio, RetryOptions
100
+
101
+ permio = Permio(
102
+ api_key="perms_...", # default: $PERMIO_API_KEY; checked locally for typos
103
+ base_url=DEV_BASE_URL, # default: DEFAULT_BASE_URL (https://api.perms.io, production)
104
+ project="my-app", # default project for every call; each call can pass project=
105
+ timeout=10, # seconds per attempt; 0 disables
106
+ retries=RetryOptions(max_retries=2, base_delay=0.1, max_delay=2.0), # or an int
107
+ )
108
+ ```
109
+
110
+ | Option | Default | Notes |
111
+ | ----------------- | --------------------------- | ----- |
112
+ | `api_key` | `$PERMIO_API_KEY` | A malformed key raises `PermioConfigError` before any request |
113
+ | `base_url` | `https://api.perms.io` | `https://host[:port]`, `http://host:port` (plaintext, e.g. local) or `host:port` (TLS) |
114
+ | `project` | none | Required by every call except `who_am_i()`, here or per call |
115
+ | `timeout` | `10` | Per attempt, in seconds. Per-call `timeout=` is an overall deadline across retries |
116
+ | `retries` | `2` | `int` or `RetryOptions`; see [Retries and timeouts](#retries-and-timeouts) |
117
+ | `channel` | none | Your own `grpc.Channel` (`grpc.aio.Channel` for `AsyncPermio`); not closed by the client |
118
+ | `channel_options` | none | Extra gRPC channel options when the client creates the channel |
119
+
120
+ Every call also accepts `metadata=[("header", "value")]` for extra request headers.
121
+
122
+ ### Dev vs prod
123
+
124
+ `DEFAULT_BASE_URL` (`https://api.perms.io`) is production and the default.
125
+ `DEV_BASE_URL` (`https://dev-api.perms.io`) is the development API; keys are
126
+ per environment. `permio.who_am_i()` tells you which organisation and project a
127
+ key belongs to and whether it can write:
128
+
129
+ ```python
130
+ me = permio.who_am_i()
131
+ print(me.kind, me.project, me.api_key_access) # "api_key", "my-app", "full" (or "check")
132
+ ```
133
+
134
+ ## Checking permissions
135
+
136
+ `permission` and `resource` take one value or a list (up to 100 each). Every
137
+ permission must hold on every resource.
138
+
139
+ ```python
140
+ permio.check("alice", "docs.read", "/docs/42") # -> bool
141
+ permio.check("alice", ["docs.read", "docs.write"], "/docs/42") # both
142
+ permio.check_all("alice", ["docs.read", "docs.write"], "/docs/42") # alias of check
143
+ permio.check_any("alice", ["docs.read", "docs.admin"], "/docs/42") # at least one, per resource
144
+
145
+ result = permio.check_detailed("alice", ["docs.read", "docs.write"], "/docs/42")
146
+ result.allowed # bool
147
+ result.missing # [MissingPermission(resource="/docs/42", permissions=["docs.write"])]
148
+
149
+ permio.assert_allowed("alice", "docs.write", "/docs/42") # raises PermioDeniedError if not
150
+ ```
151
+
152
+ A denial is an answer, not an error: `check()` returns `False`.
153
+ (`assert` is a Python keyword, so the raising variant is `assert_allowed`.)
154
+
155
+ ### Resource URIs
156
+
157
+ Grants on a URI apply to everything below it: `/docs` covers `/docs/42`. A `_`
158
+ segment in a stored grant matches exactly one segment. Build URIs with
159
+ `resource()`, which percent-encodes each segment so IDs containing `/` or
160
+ spaces stay one segment:
161
+
162
+ ```python
163
+ from permio import resource
164
+
165
+ resource("projects", project_id, "docs", doc_id) # "/projects/p1/docs/a%2Fb"
166
+ ```
167
+
168
+ Build URIs the same way when granting and when checking. `assert_resource_uri()`
169
+ checks the basic shape (leading `/`, no empty segments, no whitespace, at most
170
+ 2048 bytes); the SDK runs it before sending.
171
+
172
+ ## Batch checks
173
+
174
+ Check many resources at once; each is answered separately. More than 100 are
175
+ split into several `BatchCheck` calls (sent in parallel by `AsyncPermio`).
176
+
177
+ ```python
178
+ allowed = permio.batch_check("alice", "docs.read", [resource("docs", d.id) for d in docs])
179
+ visible = [d for d in docs if allowed[resource("docs", d.id)]]
180
+
181
+ for entry in permio.batch_check_detailed("alice", ["docs.read", "docs.write"], uris):
182
+ print(entry.resource, entry.allowed, entry.missing)
183
+ ```
184
+
185
+ ## Granting and revoking
186
+
187
+ Needs a full-access API key.
188
+
189
+ ```python
190
+ permio.grant("alice", "/docs/42", permissions="docs.read")
191
+ permio.grant("alice", "/docs", permissions=["docs.read", "docs.write"], roles="role-id")
192
+ permio.revoke("alice", "/docs/42", permissions="docs.read")
193
+ ```
194
+
195
+ Grants are additive. `revoke` removes names from the grant on exactly that URI
196
+ (grants on ancestor URIs stay, so `check()` may still pass) and ignores names
197
+ the grant lacks, so revoking twice is harmless. Roles are referenced by ID
198
+ (`permio.list_roles()`). Neither call is ever retried automatically.
199
+
200
+ ## Groups
201
+
202
+ A group is the principal `group:<slug>`; grant to it and every member holds
203
+ the grant. Groups cannot contain groups. Methods accept the slug or the
204
+ `group:` form.
205
+
206
+ ```python
207
+ from permio import group
208
+
209
+ permio.groups.create("editors", description="Can edit docs")
210
+ permio.groups.add_members("editors", ["alice", "bob"]) # AddMembersResult(added, existing)
211
+ permio.grant(group("editors"), "/docs", permissions="docs.write")
212
+ permio.check("bob", "docs.write", "/docs/42") # True, through the group
213
+
214
+ permio.groups.get("editors")
215
+ permio.groups.update("editors", description="Edits docs")
216
+ for g in permio.groups.list(search="edit"): ...
217
+ for m in permio.groups.list_members("editors"): print(m.principal_id)
218
+ for g in permio.groups.list_for_principal("bob"): print(g.name)
219
+ permio.groups.remove_members("editors", ["bob"]) # RemoveMembersResult(removed, not_members)
220
+ permio.groups.delete("editors") # also removes its grants
221
+ ```
222
+
223
+ ## Filtering lists ("where can they do X?")
224
+
225
+ `list_access` returns where a principal holds a permission at or below a URI,
226
+ following every page, so you can filter in your database instead of checking
227
+ row by row:
228
+
229
+ ```python
230
+ access = permio.list_access("alice", "docs.read", "/docs")
231
+ if access.covers_all:
232
+ rows = all_docs() # no filter needed
233
+ else:
234
+ prefixes = access.to_prefixes() # ["/docs/1", "/docs/7"]: path = p OR path LIKE p || '/%'
235
+ regex = access.to_regex().pattern # for PostgreSQL ~ or MongoDB $regex (covers _ patterns too)
236
+ visible = [d for d in docs if access.allows(d.uri)]
237
+ ```
238
+
239
+ ## Who has access
240
+
241
+ ```python
242
+ for entry in permio.who_has_access("/docs/42", permission="docs.read", expand_groups=True):
243
+ print(entry.principal, entry.is_group, entry.via_group,
244
+ [(g.resource, g.match, g.source.type) for g in entry.via])
245
+ ```
246
+
247
+ ## Listing
248
+
249
+ List helpers return iterators (async iterators on `AsyncPermio`) that fetch
250
+ pages of up to 100 items as you go:
251
+
252
+ ```python
253
+ for p in permio.list_permissions(search="docs."): print(p.name)
254
+ for role in permio.list_roles(): print(role.id, role.name)
255
+ for principal_id in permio.list_principals(): print(principal_id)
256
+ for a in permio.list_assignments("alice"): print(a.resource_uri, a.roles)
257
+
258
+ async for p in async_permio.list_permissions(): ...
259
+ ```
260
+
261
+ `page_size=` (1..100) sets the page size. Items are the generated protobuf
262
+ messages (`permio.raw.permissions_pb2.Permission`, `Role`, `Group`, ...).
263
+
264
+ ## Errors
265
+
266
+ Every error extends `PermioError`, which has `code` (a `grpc.StatusCode`, or
267
+ None for SDK-side errors), `code_name` (for example `"permission_denied"`),
268
+ `message` and `metadata` (response headers and trailers). The original
269
+ `grpc.RpcError` is chained as `__cause__`.
270
+
271
+ | Class | When |
272
+ | ------------------------ | ---- |
273
+ | `PermioConfigError` | Bad client setup: malformed or missing API key, no `project`, invalid `base_url` |
274
+ | `PermioAuthError` | `UNAUTHENTICATED`: the key is unknown, invalidated or missing |
275
+ | `PermioPermissionError` | `PERMISSION_DENIED`: the *API key* lacks rights for this call. `missing` lists what it lacks when the API says |
276
+ | `PermioValidationError` | `INVALID_ARGUMENT`, or the SDK's own input checks (e.g. a URI without a leading `/`) |
277
+ | `PermioNotFoundError` | `NOT_FOUND`, e.g. `groups.get()` for a group that does not exist |
278
+ | `PermioUnavailableError` | `UNAVAILABLE`, including network failures |
279
+ | `PermioTimeoutError` | `DEADLINE_EXCEEDED`, including the SDK's own deadlines |
280
+ | `PermioRateLimitError` | `RESOURCE_EXHAUSTED`: the organisation's rate limit is used up. `retry_after` is the server's requested delay in seconds (or None) |
281
+ | `PermioDeniedError` | Raised only by `assert_allowed()` when the *principal* is not allowed; has `principal` and `missing` |
282
+
283
+ Anything else is a plain `PermioError` with the matching `code`.
284
+
285
+ ```python
286
+ from permio import PermioAuthError, PermioDeniedError, PermioError, PermioPermissionError
287
+
288
+ try:
289
+ permio.assert_allowed(user_id, "docs.write", resource("docs", doc_id))
290
+ except PermioDeniedError:
291
+ ... # the principal may not do this: return 403
292
+ except PermioPermissionError as err:
293
+ print(err.missing) # the API key may not check this (needs permission.check)
294
+ except PermioAuthError:
295
+ ... # rotate or fix the API key
296
+ except PermioError as err:
297
+ print(err.code_name, err.message)
298
+ ```
299
+
300
+ `PermioPermissionError.missing` uses the API's fully qualified URIs
301
+ (`/organisation/{org}/project/{project}/...`), not the URIs you sent.
302
+
303
+ ### Retries and timeouts
304
+
305
+ Idempotent reads (`Check`, `BatchCheck`, `WhoAmI`, and every `Get*` and `List*`
306
+ RPC; see `is_idempotent_method()`) are retried on `UNAVAILABLE` and
307
+ `DEADLINE_EXCEEDED`, twice by default, with exponential backoff and jitter.
308
+ Writes, including `grant` and `revoke`, are never retried: after a timeout the
309
+ first attempt may still have landed.
310
+
311
+ Rate limiting: when the API answers `RESOURCE_EXHAUSTED` with a
312
+ `google.rpc.RetryInfo` delay or a `retry-after` header, an idempotent read is
313
+ retried after that delay (plus a little jitter) if it is within
314
+ `RetryOptions.max_delay`; otherwise `PermioRateLimitError` is raised with
315
+ `retry_after`. A `retry-after` header on `UNAVAILABLE` is honoured as the
316
+ minimum backoff.
317
+
318
+ `timeout` on the client applies to each attempt; `timeout=` on a call is an
319
+ overall deadline across retries. On `AsyncPermio`, cancelling the task cancels
320
+ the call and any pending retry.
321
+
322
+ ## Raw clients (full API)
323
+
324
+ Everything not wrapped above (permission and role CRUD, organisations,
325
+ projects, API keys, usage, ...) is reachable through the generated stubs.
326
+ `permio.permissions` and `permio.organisations` are the generated
327
+ `PermissionsService` / `OrganisationService` stubs with auth, timeouts,
328
+ retries (reads only) and typed errors applied; the messages live in
329
+ `permio.raw`:
330
+
331
+ ```python
332
+ from permio import raw
333
+
334
+ pb = raw.permissions_pb2
335
+ permio.permissions.CreatePermission(pb.CreatePermissionRequest(project_name="my-app", name="docs.read"))
336
+ role = permio.permissions.CreateRole(
337
+ pb.CreateRoleRequest(project_name="my-app", name="Reader", permissions=["docs.read"])
338
+ )
339
+ project = permio.organisations.GetProject(raw.organisation_pb2.GetProjectRequest(project_name="my-app"))
340
+ ```
341
+
342
+ On `AsyncPermio` these methods are coroutines. For the bare stubs with no SDK
343
+ behaviour, use `raw.permissions_pb2_grpc.PermissionsServiceStub(channel)` with
344
+ your own channel and `authorization: Bearer <key>` metadata.
345
+
346
+ The generated modules are `permio.permissions.v1`, `permio.organisation.v1`,
347
+ `permio.pagination.v1` and `permio.validate`. They are produced from
348
+ [PrivateJAR/proto](https://github.com/PrivateJAR/proto) and must not be edited
349
+ by hand.
350
+
351
+ ## API key format
352
+
353
+ `perms_` keys are 44 characters: `perms_`, 32 base62 characters and a 6
354
+ character base62 CRC32 checksum. The client checks the format and checksum
355
+ locally (`validate_api_key()`, `is_valid_api_key()`) so a mistyped key fails
356
+ fast with `PermioConfigError`. Legacy JWT keys are accepted by shape. Only the
357
+ API can tell whether a key exists and is still valid.
358
+
359
+ ## Development
360
+
361
+ ```bash
362
+ python -m venv .venv && . .venv/bin/activate
363
+ pip install -e ".[dev]"
364
+ ruff check src tests && ruff format --check src tests
365
+ mypy # strict, on the hand-written package only
366
+ pytest # unit tests against an in-process gRPC server
367
+ ```
368
+
369
+ ### Integration tests
370
+
371
+ `tests/test_live.py` runs the same scenarios as the other Permio SDKs against a
372
+ real deployment: whoAmI, grant then check, batch check, list access, revoke,
373
+ groups, and a bad key. It is skipped unless `PERMIO_API_KEY` is set, defaults
374
+ to the dev API (`PERMIO_BASE_URL` overrides it) and refuses to run against
375
+ production. The key needs full access to its project (`PERMIO_PROJECT`, default
376
+ the key's own project). It creates and deletes fixed `sdk-it-python-*` IDs.
377
+
378
+ ```bash
379
+ PERMIO_API_KEY=perms_... pytest tests/test_live.py -v
380
+ ```
381
+
382
+ CI runs it in a serialised `integration` job when the `PERMIO_DEV_API_KEY`
383
+ repository secret exists, and skips it with a notice otherwise.
384
+
385
+ ### Releasing
386
+
387
+ `.github/workflows/release.yml` runs on `v*` tags: it checks the tag matches
388
+ `src/permio/_version.py`, runs the tests, builds the sdist and wheel and
389
+ publishes them to PyPI with [trusted publishing](https://docs.pypi.org/trusted-publishers/)
390
+ (no API token). One-time setup on PyPI: add a pending trusted publisher for the
391
+ project `permio` with owner `PrivateJAR`, repository `permio-python`, workflow
392
+ `release.yml` and environment `pypi`, and create the `pypi` environment in the
393
+ repository settings. Every CI job runs on Blacksmith except the publish step,
394
+ which runs on a GitHub-hosted runner because `pypa/gh-action-pypi-publish` only
395
+ supports self-hosted runners on a best-effort basis.
396
+
397
+ ## License
398
+
399
+ [Apache-2.0](LICENSE). Copyright PrivateJAR.