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.
@@ -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.3
1
+ Metadata-Version: 2.4
2
2
  Name: keyrunes-python-sdk
3
- Version: 0.1.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
  [![Tests](https://github.com/Keyrunes/keyrunes-python-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/Keyrunes/keyrunes-python-sdk/actions/workflows/ci.yml)
36
- [![Coverage](https://codecov.io/gh/Keyrunes/keyrunes-python-sdk/branch/main/graph/badge.svg)](https://codecov.io/gh/Keyrunes/keyrunes-python-sdk)
37
38
  [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
38
39
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](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
  [![Tests](https://github.com/Keyrunes/keyrunes-python-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/Keyrunes/keyrunes-python-sdk/actions/workflows/ci.yml)
4
- [![Coverage](https://codecov.io/gh/Keyrunes/keyrunes-python-sdk/branch/main/graph/badge.svg)](https://codecov.io/gh/Keyrunes/keyrunes-python-sdk)
5
4
  [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
6
5
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](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
 
@@ -1,6 +1,6 @@
1
1
  """Keyrunes SDK - Python client for Keyrunes Authorization System."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.0"
4
4
 
5
5
  from keyrunes_sdk.client import KeyrunesClient
6
6
  from keyrunes_sdk.config import (
@@ -137,7 +137,12 @@ class KeyrunesClient:
137
137
  )
138
138
  raise NetworkError(f"Request failed: {error_msg}")
139
139
 
140
- result: Dict[str, Any] = response.json()
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
- if token_user_id and str(user_id) == token_user_id:
413
- groups = (
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
- token_data = self._token_data
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
- try:
461
- response = self._make_request("GET", f"/api/users/{user_id}")
462
- return self._normalize_user(response)
463
- except UserNotFoundError:
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
- token_data = self._token_data
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
- try:
509
- response = self._make_request("GET", "/api/users/me")
510
- return self._normalize_user(response)
511
- except UserNotFoundError:
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.1.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']