keyrunes-python-sdk 0.1.0__tar.gz → 0.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.
- {keyrunes_python_sdk-0.1.0 → keyrunes_python_sdk-0.2.0}/CHANGELOG.md +46 -0
- {keyrunes_python_sdk-0.1.0 → keyrunes_python_sdk-0.2.0}/PKG-INFO +7 -4
- {keyrunes_python_sdk-0.1.0 → keyrunes_python_sdk-0.2.0}/README.md +2 -1
- {keyrunes_python_sdk-0.1.0 → keyrunes_python_sdk-0.2.0}/keyrunes_sdk/__init__.py +1 -1
- {keyrunes_python_sdk-0.1.0 → keyrunes_python_sdk-0.2.0}/keyrunes_sdk/client.py +33 -56
- {keyrunes_python_sdk-0.1.0 → keyrunes_python_sdk-0.2.0}/pyproject.toml +14 -1
- {keyrunes_python_sdk-0.1.0 → keyrunes_python_sdk-0.2.0}/LICENSE +0 -0
- {keyrunes_python_sdk-0.1.0 → keyrunes_python_sdk-0.2.0}/keyrunes_sdk/config.py +0 -0
- {keyrunes_python_sdk-0.1.0 → keyrunes_python_sdk-0.2.0}/keyrunes_sdk/decorators.py +0 -0
- {keyrunes_python_sdk-0.1.0 → keyrunes_python_sdk-0.2.0}/keyrunes_sdk/exceptions.py +0 -0
- {keyrunes_python_sdk-0.1.0 → keyrunes_python_sdk-0.2.0}/keyrunes_sdk/models.py +0 -0
|
@@ -5,6 +5,52 @@ Todas as mudanças notáveis neste projeto serão documentadas neste arquivo.
|
|
|
5
5
|
O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.0.0/),
|
|
6
6
|
e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
|
|
7
7
|
|
|
8
|
+
## [0.2.0] - 2026-09-03
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Property-based test suite (`tests/test_property_based.py`, 24 tests) built on
|
|
13
|
+
Hypothesis, covering base URL normalization, `_normalize_user` id precedence
|
|
14
|
+
and `is_admin` derivation, JWT claim parsing, request URL construction, HTTP
|
|
15
|
+
status to exception mapping, and model validation bounds.
|
|
16
|
+
- Input fuzzing ("spider") suite (`tests/test_fuzz.py`, 19 tests) that crawls the
|
|
17
|
+
public surface with hostile payloads and asserts that only `KeyrunesError` and
|
|
18
|
+
pydantic `ValidationError` ever escape, and that nothing panics or leaks a raw
|
|
19
|
+
`ValueError`/`TypeError`.
|
|
20
|
+
- Request contract suite (`tests/test_request_contract.py`, 33 tests) that mocks
|
|
21
|
+
only the httpx transport, so the method, URL, headers and JSON body actually
|
|
22
|
+
put on the wire are asserted for every endpoint.
|
|
23
|
+
- Hypothesis profiles in `tests/conftest.py` (`fast`, `dev`, `ci`) selected via
|
|
24
|
+
the `HYPOTHESIS_PROFILE` environment variable. `fast` is derandomized and
|
|
25
|
+
database-free so mutation runs judge every mutant against identical examples.
|
|
26
|
+
- `[tool.mutmut]` configuration in `pyproject.toml` for mutation testing.
|
|
27
|
+
- `hypothesis` added as a development dependency.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- A 2xx response carrying a non-JSON body raised a raw `ValueError` out of
|
|
32
|
+
`KeyrunesClient._make_request`. It is now wrapped in `NetworkError`, so the
|
|
33
|
+
documented exception contract holds for malformed responses.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Extracted the duplicated "build a `User` from JWT claims" block into
|
|
38
|
+
`KeyrunesClient._user_from_token_claims()`.
|
|
39
|
+
|
|
40
|
+
### Removed
|
|
41
|
+
|
|
42
|
+
- Unreachable `except UserNotFoundError` fallbacks in `get_user`,
|
|
43
|
+
`get_current_user` and `has_group`. Each re-tested a condition that had
|
|
44
|
+
already forced an early return, so a 404 from the server was being swallowed
|
|
45
|
+
instead of propagated.
|
|
46
|
+
|
|
47
|
+
### Testing
|
|
48
|
+
|
|
49
|
+
- Test count raised from 83 to 159.
|
|
50
|
+
- Mutation score (mutmut) raised from 44% (294/669 mutants killed) to 72%
|
|
51
|
+
(387/537). The remaining survivors are predominantly equivalent mutants that
|
|
52
|
+
only alter error message prose.
|
|
53
|
+
|
|
8
54
|
## [0.1.0] - 2025-12-03
|
|
9
55
|
|
|
10
56
|
### Adicionado
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: keyrunes-python-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Python SDK for Keyrunes Authorization System
|
|
5
5
|
License: AGPL
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Keywords: keyrunes,authorization,rbac,abac,security,authentication,permissions
|
|
7
8
|
Author: keyrunes
|
|
8
9
|
Author-email: contact@singularjourney.host
|
|
@@ -17,12 +18,13 @@ Classifier: Programming Language :: Python :: 3
|
|
|
17
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
23
|
Classifier: Topic :: Security
|
|
23
24
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
25
|
Classifier: Topic :: System :: Systems Administration :: Authentication/Directory
|
|
25
26
|
Requires-Dist: httpx (>=0.28.1,<0.29.0)
|
|
27
|
+
Requires-Dist: mutmut (>=3.7.0,<4.0.0)
|
|
26
28
|
Requires-Dist: pydantic[email] (>=2.0.0,<3.0.0)
|
|
27
29
|
Requires-Dist: pyjwt (>=2.9.0,<3.0.0)
|
|
28
30
|
Project-URL: Documentation, https://github.com/jonatasoli/keyrunes-python-sdk#readme
|
|
@@ -33,11 +35,12 @@ Description-Content-Type: text/markdown
|
|
|
33
35
|
# Keyrunes SDK Python Client
|
|
34
36
|
|
|
35
37
|
[](https://github.com/Keyrunes/keyrunes-python-sdk/actions/workflows/ci.yml)
|
|
36
|
-
[](https://codecov.io/gh/Keyrunes/keyrunes-python-sdk)
|
|
37
38
|
[](https://www.python.org/downloads/)
|
|
38
39
|
[](LICENSE)
|
|
39
40
|
|
|
41
|
+
|
|
40
42
|
Python SDK for integration with the [Keyrunes Authorization System](https://github.com/Keyrunes/keyrunes), a modern high-performance authorization system built in Rust.
|
|
43
|
+
[Pypi](https://pypi.org/project/keyrunes-python-sdk/)
|
|
41
44
|
|
|
42
45
|
## Features
|
|
43
46
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# Keyrunes SDK Python Client
|
|
2
2
|
|
|
3
3
|
[](https://github.com/Keyrunes/keyrunes-python-sdk/actions/workflows/ci.yml)
|
|
4
|
-
[](https://codecov.io/gh/Keyrunes/keyrunes-python-sdk)
|
|
5
4
|
[](https://www.python.org/downloads/)
|
|
6
5
|
[](LICENSE)
|
|
7
6
|
|
|
7
|
+
|
|
8
8
|
Python SDK for integration with the [Keyrunes Authorization System](https://github.com/Keyrunes/keyrunes), a modern high-performance authorization system built in Rust.
|
|
9
|
+
[Pypi](https://pypi.org/project/keyrunes-python-sdk/)
|
|
9
10
|
|
|
10
11
|
## Features
|
|
11
12
|
|
|
@@ -137,7 +137,12 @@ class KeyrunesClient:
|
|
|
137
137
|
)
|
|
138
138
|
raise NetworkError(f"Request failed: {error_msg}")
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
try:
|
|
141
|
+
result: Dict[str, Any] = response.json()
|
|
142
|
+
except (ValueError, TypeError) as e:
|
|
143
|
+
raise NetworkError(
|
|
144
|
+
f"Malformed JSON in response from {url}: {str(e)}"
|
|
145
|
+
)
|
|
141
146
|
return result
|
|
142
147
|
|
|
143
148
|
except httpx.RequestError as e:
|
|
@@ -167,6 +172,21 @@ class KeyrunesClient:
|
|
|
167
172
|
)
|
|
168
173
|
return User(**normalized)
|
|
169
174
|
|
|
175
|
+
def _user_from_token_claims(self) -> User:
|
|
176
|
+
"""Build a :class:`User` out of the claims carried by the JWT.
|
|
177
|
+
|
|
178
|
+
Only called once ``self._token_data`` is known to be populated.
|
|
179
|
+
"""
|
|
180
|
+
token_data = self._token_data or {}
|
|
181
|
+
return self._normalize_user(
|
|
182
|
+
{
|
|
183
|
+
"id": str(token_data.get("sub", "")),
|
|
184
|
+
"username": token_data.get("username", ""),
|
|
185
|
+
"email": token_data.get("email", ""),
|
|
186
|
+
"groups": token_data.get("groups", []),
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
|
|
170
190
|
def _parse_token_response(self, payload: Dict[str, Any]) -> Token:
|
|
171
191
|
"""
|
|
172
192
|
Accept both legacy and current API token responses.
|
|
@@ -409,13 +429,8 @@ class KeyrunesClient:
|
|
|
409
429
|
check = GroupCheck(**response)
|
|
410
430
|
return check.has_access
|
|
411
431
|
except UserNotFoundError:
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
self._token_data.get("groups", [])
|
|
415
|
-
if self._token_data
|
|
416
|
-
else []
|
|
417
|
-
)
|
|
418
|
-
return group_id in groups
|
|
432
|
+
# The self-lookup above already returned for the authenticated
|
|
433
|
+
# user, so reaching here always means a genuine miss.
|
|
419
434
|
raise GroupNotFoundError(
|
|
420
435
|
f"Group '{group_id}' not found or user not in group"
|
|
421
436
|
)
|
|
@@ -448,33 +463,12 @@ class KeyrunesClient:
|
|
|
448
463
|
)
|
|
449
464
|
|
|
450
465
|
if token_user_id and str(user_id) == token_user_id and self._token_data:
|
|
451
|
-
|
|
452
|
-
user_data = {
|
|
453
|
-
"id": str(token_data.get("sub", "")),
|
|
454
|
-
"username": token_data.get("username", ""),
|
|
455
|
-
"email": token_data.get("email", ""),
|
|
456
|
-
"groups": token_data.get("groups", []),
|
|
457
|
-
}
|
|
458
|
-
return self._normalize_user(user_data)
|
|
466
|
+
return self._user_from_token_claims()
|
|
459
467
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if (
|
|
465
|
-
token_user_id
|
|
466
|
-
and str(user_id) == token_user_id
|
|
467
|
-
and self._token_data
|
|
468
|
-
):
|
|
469
|
-
token_data = self._token_data
|
|
470
|
-
user_data = {
|
|
471
|
-
"id": str(token_data.get("sub", "")),
|
|
472
|
-
"username": token_data.get("username", ""),
|
|
473
|
-
"email": token_data.get("email", ""),
|
|
474
|
-
"groups": token_data.get("groups", []),
|
|
475
|
-
}
|
|
476
|
-
return self._normalize_user(user_data)
|
|
477
|
-
raise
|
|
468
|
+
# The claims shortcut above already handled the authenticated user, so
|
|
469
|
+
# a 404 here is always a genuine miss and is propagated as such.
|
|
470
|
+
response = self._make_request("GET", f"/api/users/{user_id}")
|
|
471
|
+
return self._normalize_user(response)
|
|
478
472
|
|
|
479
473
|
def get_current_user(self) -> User:
|
|
480
474
|
"""
|
|
@@ -496,29 +490,12 @@ class KeyrunesClient:
|
|
|
496
490
|
raise AuthenticationError("Not authenticated. Please login first.")
|
|
497
491
|
|
|
498
492
|
if self._token_data:
|
|
499
|
-
|
|
500
|
-
user_data = {
|
|
501
|
-
"id": str(token_data.get("sub", "")),
|
|
502
|
-
"username": token_data.get("username", ""),
|
|
503
|
-
"email": token_data.get("email", ""),
|
|
504
|
-
"groups": token_data.get("groups", []),
|
|
505
|
-
}
|
|
506
|
-
return self._normalize_user(user_data)
|
|
493
|
+
return self._user_from_token_claims()
|
|
507
494
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
if self._token_data:
|
|
513
|
-
token_data = self._token_data
|
|
514
|
-
user_data = {
|
|
515
|
-
"id": str(token_data.get("sub", "")),
|
|
516
|
-
"username": token_data.get("username", ""),
|
|
517
|
-
"email": token_data.get("email", ""),
|
|
518
|
-
"groups": token_data.get("groups", []),
|
|
519
|
-
}
|
|
520
|
-
return self._normalize_user(user_data)
|
|
521
|
-
raise
|
|
495
|
+
# The claims shortcut above already handled every case where the token
|
|
496
|
+
# could answer, so a 404 here is a genuine miss and is propagated.
|
|
497
|
+
response = self._make_request("GET", "/api/users/me")
|
|
498
|
+
return self._normalize_user(response)
|
|
522
499
|
|
|
523
500
|
def get_user_groups(self, user_id: Optional[str] = None) -> List[str]:
|
|
524
501
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "keyrunes-python-sdk"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Python SDK for Keyrunes Authorization System"
|
|
5
5
|
authors = ["keyrunes <contact@singularjourney.host>"]
|
|
6
6
|
maintainers = ["jonatasoli <contact@jonatasoli.dev>"]
|
|
@@ -33,6 +33,7 @@ python = ">=3.10.1,<4.0"
|
|
|
33
33
|
pydantic = {extras = ["email"], version = "^2.0.0"}
|
|
34
34
|
pyjwt = "^2.9.0"
|
|
35
35
|
httpx = "^0.28.1"
|
|
36
|
+
mutmut = "^3.7.0"
|
|
36
37
|
|
|
37
38
|
[tool.poetry.group.dev.dependencies]
|
|
38
39
|
pytest = "^7.4.0"
|
|
@@ -48,6 +49,7 @@ taskipy = "^1.12.0"
|
|
|
48
49
|
towncrier = "^25.8.0"
|
|
49
50
|
pre-commit = "^4.5.1"
|
|
50
51
|
safety = "^3.7.0"
|
|
52
|
+
hypothesis = "^6.0"
|
|
51
53
|
|
|
52
54
|
[build-system]
|
|
53
55
|
requires = ["poetry-core>=1.0.0"]
|
|
@@ -66,6 +68,17 @@ addopts = [
|
|
|
66
68
|
"-v",
|
|
67
69
|
]
|
|
68
70
|
|
|
71
|
+
[tool.mutmut]
|
|
72
|
+
# Mutation testing. Only the library is mutated; the test suite is the oracle.
|
|
73
|
+
source_paths = ["keyrunes_sdk"]
|
|
74
|
+
do_not_mutate = [
|
|
75
|
+
"keyrunes_sdk/__init__.py",
|
|
76
|
+
]
|
|
77
|
+
# The project-wide addopts turn on coverage, which roughly doubles the runtime
|
|
78
|
+
# of every one of the hundreds of per-mutant test runs.
|
|
79
|
+
pytest_add_cli_args = ["--no-cov", "-p", "no:cacheprovider", "-x", "-q"]
|
|
80
|
+
pytest_add_cli_args_test_selection = ["tests/"]
|
|
81
|
+
|
|
69
82
|
[tool.black]
|
|
70
83
|
line-length = 80
|
|
71
84
|
target-version = ['py312']
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|