keyrunes-python-sdk 0.3.0__tar.gz → 0.4.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,71 @@ 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.4.0] - 2026-09-15
9
+
10
+ ### Added
11
+
12
+ - `KeyrunesClient.change_password(current_password, new_password)`, wrapping
13
+ `POST /api/user/change-password`. The server has served this endpoint all
14
+ along; the SDK exposed `login`, `refresh_token`, `register_user` and the group
15
+ queries, and not this one. Anyone who needed it built the HTTP call by hand —
16
+ which is what happened downstream, after first concluding from the SDK's
17
+ surface that the operation did not exist at all.
18
+
19
+ Three details the endpoint carries, and each one bites if read wrong:
20
+
21
+ - it lives at `/api/user/change-password`, **not** `/api/change-password` —
22
+ the router registers the first, and the handler behind the second is dead
23
+ code marked `#[allow(dead_code)]`, so pointing at it answers 404;
24
+ - it answers errors as **plain text** (`e.to_string()`), not JSON, and with
25
+ status **400** rather than 401 — so this client raises `NetworkError`, the
26
+ same class it already uses for every non-401/403/404 status, and preserves
27
+ the server's own wording, because "invalid current password" and "password
28
+ too short" call for opposite fixes;
29
+ - changing the password clears the account's `first_login` flag server-side,
30
+ which is what `requires_password_change` reports at login. There is no
31
+ second call to make.
32
+
33
+ Without a token the client refuses before sending: the server decides whose
34
+ password it is from the token, and nothing in the body chooses that.
35
+
36
+ ### Fixed
37
+
38
+ - A malformed `groups` field in a server response no longer crashes the client.
39
+ `get_current_user` and `check_user` called `list(data["groups"])` on whatever
40
+ came back; a JSON `true`, a number, or `null` raised `TypeError: 'bool' object
41
+ is not iterable` out of the SDK, from a line that reads like a safe coercion.
42
+ Anything that is not a JSON array is now read as no groups. Found by the fuzz
43
+ suite, and it predates this release — `git stash` confirmed it on the previous
44
+ tag.
45
+
46
+ ### Security
47
+
48
+ - `pyjwt` 2.10.1 → 2.14.0 and `idna` 3.11 → 3.19 in the lockfile. Both are
49
+ runtime dependencies (`idna` arrives through `httpx`), and OSV reports known
50
+ advisories against the pinned versions — nine for `pyjwt`, four of them high.
51
+ The declared constraint was already `^2.9.0`, so this is a lockfile refresh
52
+ with no API change; the 194 tests pass unchanged.
53
+
54
+ ### Removed
55
+
56
+ - `towncrier` left the dev dependencies. It was declared and never configured:
57
+ no `[tool.towncrier]` section, no `newsfragments/` directory, and this
58
+ changelog was written by hand. A tool nobody runs still gets installed,
59
+ resolved, and audited on every environment build.
60
+
61
+ ## [0.3.1] - 2026-09-04
62
+
63
+ ### Fixed
64
+
65
+ - `mutmut` is no longer a runtime dependency. It was declared in
66
+ `[tool.poetry.dependencies]` instead of the dev group, so installing this SDK
67
+ pulled a mutation-testing tool and its whole tree — `libcst`, `textual`,
68
+ `rich`, `setproctitle`, `pyyaml-ft`, `markdown-it-py`, `mdit-py-plugins`,
69
+ `mdurl`, `linkify-it-py`, `sortedcontainers` — into every consumer, including
70
+ production images. Affected 0.2.0 and 0.3.0; nothing in the library imported
71
+ it, so upgrading only removes packages.
72
+
8
73
  ## [0.3.0] - 2026-09-03
9
74
 
10
75
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: keyrunes-python-sdk
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Python SDK for Keyrunes Authorization System
5
5
  License: AGPL
6
6
  License-File: LICENSE
@@ -24,7 +24,6 @@ Classifier: Topic :: Security
24
24
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
25
  Classifier: Topic :: System :: Systems Administration :: Authentication/Directory
26
26
  Requires-Dist: httpx (>=0.28.1,<0.29.0)
27
- Requires-Dist: mutmut (>=3.7.0,<4.0.0)
28
27
  Requires-Dist: pydantic[email] (>=2.0.0,<3.0.0)
29
28
  Requires-Dist: pyjwt (>=2.9.0,<3.0.0)
30
29
  Project-URL: Documentation, https://github.com/jonatasoli/keyrunes-python-sdk#readme
@@ -1,6 +1,6 @@
1
1
  """Keyrunes SDK - Python client for Keyrunes Authorization System."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.4.0"
4
4
 
5
5
  from keyrunes_sdk.client import KeyrunesClient
6
6
  from keyrunes_sdk.config import (
@@ -35,6 +35,13 @@ ENDPOINT_ME = "/api/me"
35
35
  #: Exchanges a still-valid token for a fresh one.
36
36
  ENDPOINT_REFRESH_TOKEN = "/api/refresh-token"
37
37
 
38
+ #: Changes the password of whoever the bearer token identifies.
39
+ #:
40
+ #: ``/api/user/change-password``, and not ``/api/change-password``: the router
41
+ #: registers the first, and the handler behind the second is dead code marked
42
+ #: ``#[allow(dead_code)]``. Pointing at it answers 404.
43
+ ENDPOINT_CHANGE_PASSWORD = "/api/user/change-password"
44
+
38
45
 
39
46
  class KeyrunesClient:
40
47
  """
@@ -175,7 +182,14 @@ class KeyrunesClient:
175
182
  )
176
183
  normalized["username"] = data.get("username", "")
177
184
  normalized["email"] = data.get("email", "")
178
- normalized["groups"] = data.get("groups", []) or []
185
+ # `or []` trata nulo e vazio, e **não** trata o resto: com
186
+ # ``{"groups": true}`` o valor sobrevivia como ``True``, e o
187
+ # ``"admins" in groups`` logo abaixo estourava ``TypeError`` — o cliente
188
+ # caía em vez de conter uma resposta malformada. Achado pelo fuzzer.
189
+ raw_groups = data.get("groups")
190
+ normalized["groups"] = (
191
+ list(raw_groups) if isinstance(raw_groups, list) else []
192
+ )
179
193
  normalized["attributes"] = data.get("attributes", {})
180
194
  normalized["is_active"] = data.get("is_active", True)
181
195
  groups = normalized["groups"]
@@ -627,6 +641,49 @@ class KeyrunesClient:
627
641
  self.set_token(refreshed.access_token)
628
642
  return refreshed
629
643
 
644
+ def change_password(self, current_password: str, new_password: str) -> None:
645
+ """
646
+ Change the password of whoever the current token identifies.
647
+
648
+ The server also clears the account's ``first_login`` flag, which is
649
+ what ``requires_password_change`` reports at login: changing the
650
+ password here puts that warning away without a second call.
651
+
652
+ Nothing in the body says whose password it is — the token does. A body
653
+ that chose would make "change my password" mean "change anyone's".
654
+
655
+ Args:
656
+ current_password: The password being replaced. The server checks
657
+ it before accepting the change.
658
+ new_password: The new password. The server requires at least 8
659
+ characters and answers 400 when it is shorter.
660
+
661
+ Raises:
662
+ InvalidTokenError: If the client has no token set
663
+ NetworkError: If the server refuses — wrong current password, or a
664
+ new password it will not accept. The server answers **400** for
665
+ both, which this client maps to ``NetworkError`` like any
666
+ other non-401/403/404 status. The server's wording survives,
667
+ because "invalid current password" and "password too short" call
668
+ for opposite fixes.
669
+
670
+ Example:
671
+ >>> client = KeyrunesClient("https://keyrunes.example.com")
672
+ >>> client.set_token("eyJhbGciOiJIUzI1NiIs...")
673
+ >>> client.change_password("old-secret", "a-longer-new-secret")
674
+ """
675
+ if not self._token:
676
+ raise InvalidTokenError("No token available to change a password.")
677
+
678
+ self._make_request(
679
+ "POST",
680
+ ENDPOINT_CHANGE_PASSWORD,
681
+ data={
682
+ "current_password": current_password,
683
+ "new_password": new_password,
684
+ },
685
+ )
686
+
630
687
  def clear_token(self) -> None:
631
688
  """
632
689
  Clear authentication token.
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "keyrunes-python-sdk"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "Python SDK for Keyrunes Authorization System"
5
5
  authors = ["keyrunes <contact@singularjourney.host>"]
6
6
  maintainers = ["jonatasoli <contact@jonatasoli.dev>"]
@@ -35,7 +35,6 @@ python = ">=3.10.1,<4.0"
35
35
  pydantic = {extras = ["email"], version = "^2.0.0"}
36
36
  pyjwt = "^2.9.0"
37
37
  httpx = "^0.28.1"
38
- mutmut = "^3.7.0"
39
38
 
40
39
  [tool.poetry.group.dev.dependencies]
41
40
  pytest = "^7.4.0"
@@ -48,10 +47,10 @@ isort = "^5.12.0"
48
47
  flake8 = "^6.1.0"
49
48
  mypy = "^1.5.0"
50
49
  taskipy = "^1.12.0"
51
- towncrier = "^25.8.0"
52
50
  pre-commit = "^4.5.1"
53
51
  safety = "^3.7.0"
54
52
  hypothesis = "^6.0"
53
+ mutmut = "^3.7.0"
55
54
 
56
55
  [build-system]
57
56
  requires = ["poetry-core>=1.0.0"]