keyrunes-python-sdk 0.4.0__tar.gz → 0.5.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.5.0/CHANGELOG.md +330 -0
- {keyrunes_python_sdk-0.4.0 → keyrunes_python_sdk-0.5.0}/PKG-INFO +1 -1
- {keyrunes_python_sdk-0.4.0 → keyrunes_python_sdk-0.5.0}/keyrunes_sdk/client.py +54 -18
- {keyrunes_python_sdk-0.4.0 → keyrunes_python_sdk-0.5.0}/keyrunes_sdk/decorators.py +9 -6
- {keyrunes_python_sdk-0.4.0 → keyrunes_python_sdk-0.5.0}/keyrunes_sdk/models.py +6 -3
- {keyrunes_python_sdk-0.4.0 → keyrunes_python_sdk-0.5.0}/pyproject.toml +3 -1
- keyrunes_python_sdk-0.4.0/CHANGELOG.md +0 -271
- {keyrunes_python_sdk-0.4.0 → keyrunes_python_sdk-0.5.0}/LICENSE +0 -0
- {keyrunes_python_sdk-0.4.0 → keyrunes_python_sdk-0.5.0}/README.md +0 -0
- {keyrunes_python_sdk-0.4.0 → keyrunes_python_sdk-0.5.0}/keyrunes_sdk/__init__.py +0 -0
- {keyrunes_python_sdk-0.4.0 → keyrunes_python_sdk-0.5.0}/keyrunes_sdk/config.py +0 -0
- {keyrunes_python_sdk-0.4.0 → keyrunes_python_sdk-0.5.0}/keyrunes_sdk/exceptions.py +0 -0
- {keyrunes_python_sdk-0.4.0 → keyrunes_python_sdk-0.5.0}/keyrunes_sdk/py.typed +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the Keyrunes Python SDK.
|
|
4
|
+
|
|
5
|
+
Generated by `git-cliff` from the conventional commits — each commit body
|
|
6
|
+
becomes the text of its entry. **Do not edit by hand**: the next run overwrites
|
|
7
|
+
it. To change an entry, reword the commit message (`git rebase -i`); to change
|
|
8
|
+
the shape, edit `cliff.toml` and run `poetry run task changelog`.
|
|
9
|
+
|
|
10
|
+
## [0.5.0] - 2026-11-04
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **security**: verify self group checks against the server and pin the privileged-group set (reported by Rathnadeepan / HotWater @HotWater-015)
|
|
15
|
+
|
|
16
|
+
has_group used to answer for the authenticated user straight from the
|
|
17
|
+
JWT claims the client decoded with verify_signature=False, so a forged
|
|
18
|
+
token granted any group locally. Self checks now hit GET /api/me with
|
|
19
|
+
the bearer token and fail closed. The is_admin heuristic that granted
|
|
20
|
+
admin to any group whose name merely contained 'admin' (e.g.
|
|
21
|
+
'non-admin') is replaced by exact membership in
|
|
22
|
+
{admin, admins, superadmin}. refresh_token now sends the Authorization
|
|
23
|
+
header the server's require_jwt middleware needs (it was 401 on every
|
|
24
|
+
call). Registration/login model defaults move from 'default' to the
|
|
25
|
+
server's actual default namespace 'public'. register_admin documents
|
|
26
|
+
that server >= 0.4.2 ignores admin_key at /api/register.
|
|
27
|
+
|
|
28
|
+
- **security**: build the request URL from the base instead of joining it
|
|
29
|
+
|
|
30
|
+
urljoin() lets a second argument that looks like a URL replace the base
|
|
31
|
+
completely: an endpoint of "A:" produced the relative URL "A:" and an
|
|
32
|
+
endpoint of "https://elsewhere.test/x" would have sent the request (and
|
|
33
|
+
the Authorization header) to another host. The URL is now built from the
|
|
34
|
+
configured base and the endpoint path, so an endpoint can never point the
|
|
35
|
+
request somewhere else.
|
|
36
|
+
|
|
37
|
+
The fuzz suite caught it: hypothesis stores the failing example, so the
|
|
38
|
+
invariant "an arbitrary endpoint still yields an absolute URL" is now
|
|
39
|
+
pinned by a regression test.
|
|
40
|
+
|
|
41
|
+
### Documentation
|
|
42
|
+
|
|
43
|
+
- **client**: put the groups comment back in English
|
|
44
|
+
|
|
45
|
+
Every comment and docstring in this SDK is in English, and this one was not.
|
|
46
|
+
It went in today with the malformed-`groups` fix, in the same session where the
|
|
47
|
+
commit messages also drifted to Portuguese — same slip, different file.
|
|
48
|
+
|
|
49
|
+
- **changelog**: put cliff.toml and its output in English
|
|
50
|
+
|
|
51
|
+
Everything else in this repository is in English — code, comments, docstrings,
|
|
52
|
+
and every commit message. The `cliff.toml` written earlier today was not: a
|
|
53
|
+
Portuguese header, Portuguese group names and Portuguese comments, which put
|
|
54
|
+
eight Portuguese lines into an otherwise English generated changelog.
|
|
55
|
+
|
|
56
|
+
Group names follow Keep a Changelog, which is what the hand-written file used
|
|
57
|
+
before it was generated: Added, Fixed, Performance, Refactored, Testing,
|
|
58
|
+
Documentation, Infrastructure, Maintenance, Other.
|
|
59
|
+
|
|
60
|
+
One thing changes besides the language: a `.*` catch-all parser. Without it a
|
|
61
|
+
commit that follows no convention is dropped without a trace, and a changelog
|
|
62
|
+
that looks complete while omitting work is the failure worth guarding against.
|
|
63
|
+
|
|
64
|
+
- **client**: point the register_admin deprecation at the 0.5.0 release
|
|
65
|
+
|
|
66
|
+
The docstring still advertised 0.2.2, a version that never shipped the
|
|
67
|
+
deprecation. The method is deprecated since the release that carries the
|
|
68
|
+
server 0.4.2 hardening.
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
## [0.4.0] - 2026-09-15
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
|
|
75
|
+
- **client**: change_password, which the API served and the SDK did not expose
|
|
76
|
+
|
|
77
|
+
`POST /api/user/change-password` has been in Keyrunes all along. The SDK exposed
|
|
78
|
+
`login`, `refresh_token`, `register_user` and the group queries — and not this
|
|
79
|
+
one.
|
|
80
|
+
|
|
81
|
+
Whoever needed it built the HTTP call by hand: that is what happened downstream
|
|
82
|
+
in PlayfulLMS, and before that the conclusion was worse — reading only the SDK's
|
|
83
|
+
surface, the operation was written off as nonexistent, and the platform nearly
|
|
84
|
+
told people to change their password at the provider instead.
|
|
85
|
+
|
|
86
|
+
**Details that cost you if read wrong**
|
|
87
|
+
|
|
88
|
+
`/api/user/change-password`, and **not** `/api/change-password`: the router
|
|
89
|
+
registers the first. The handler behind the second is dead code marked
|
|
90
|
+
`#[allow(dead_code)]`, and pointing at it answers 404.
|
|
91
|
+
|
|
92
|
+
The server answers errors as **plain text** (`e.to_string()`), not JSON — treat
|
|
93
|
+
it as JSON and you get a parsing exception instead of the sentence explaining
|
|
94
|
+
what went wrong.
|
|
95
|
+
|
|
96
|
+
The error is **400**, not 401, which is why it becomes `NetworkError`: that is
|
|
97
|
+
the class this SDK already uses for everything that is not 401/403/404. Changing
|
|
98
|
+
it here would change the contract for every caller because of one endpoint. The
|
|
99
|
+
server's wording survives, which is what matters — "invalid current password"
|
|
100
|
+
and "password too short" call for opposite fixes.
|
|
101
|
+
|
|
102
|
+
Without a token it refuses **before** sending: the server decides whose password
|
|
103
|
+
it is from the token, and nothing in the body chooses that.
|
|
104
|
+
|
|
105
|
+
And the effect that closes the loop: the server clears `first_login` on a
|
|
106
|
+
change — the origin of the `requires_password_change` the login reports. It is
|
|
107
|
+
in the docstring because callers need to know there is no second call to make.
|
|
108
|
+
|
|
109
|
+
Verified against a real Keyrunes, not only against a double: register, change,
|
|
110
|
+
and then the old password refused (401) and the new one accepted (200), with
|
|
111
|
+
`first_login` cleared.
|
|
112
|
+
|
|
113
|
+
### Fixed
|
|
114
|
+
|
|
115
|
+
- **client**: contain a malformed `groups` instead of taking the client down
|
|
116
|
+
|
|
117
|
+
The repository's own fuzz suite found it: with `{"groups": true}` in the server
|
|
118
|
+
response, normalization raised
|
|
119
|
+
`TypeError: argument of type 'bool' is not a container or iterable`.
|
|
120
|
+
|
|
121
|
+
`data.get("groups", []) or []` handles null and empty, and **nothing else**:
|
|
122
|
+
`True or []` is `True`, which survives all the way to the `"admins" in groups`
|
|
123
|
+
just below.
|
|
124
|
+
|
|
125
|
+
A client that falls over on a malformed response is worse than one that ignores
|
|
126
|
+
it: whoever consumes the SDK sees a type error coming from inside the library,
|
|
127
|
+
far from the cause, and nothing points at the server as the origin.
|
|
128
|
+
|
|
129
|
+
Pre-existing — `git stash` and the failure stays. Detection measured: putting
|
|
130
|
+
the `or []` back fails the fuzz test.
|
|
131
|
+
|
|
132
|
+
- **deps**: pyjwt 2.10.1 → 2.14.0 and idna 3.11 → 3.19 (OSV advisories)
|
|
133
|
+
|
|
134
|
+
The `safety` hook does not run on this machine — it asks for an interactive
|
|
135
|
+
login and dies with `EOF when reading a line`. Rather than skip the security
|
|
136
|
+
check and move on, the 90 versions pinned in `poetry.lock` were run against the
|
|
137
|
+
OSV database.
|
|
138
|
+
|
|
139
|
+
Two findings in the runtime tree:
|
|
140
|
+
|
|
141
|
+
- `pyjwt` 2.10.1: nine advisories, four of them high severity. In an
|
|
142
|
+
authentication library this is the dependency that matters most. Fixed in
|
|
143
|
+
2.13.0; `pyproject` already declared `^2.9.0`, so no API changes — only the
|
|
144
|
+
lock.
|
|
145
|
+
- `idna` 3.11: two moderate advisories, fixed in 3.15. Arrives through `httpx`.
|
|
146
|
+
|
|
147
|
+
After the update the runtime tree carries no known advisory. The 194 tests pass
|
|
148
|
+
unchanged.
|
|
149
|
+
|
|
150
|
+
What remains are advisories in the development tree — `authlib`, `nltk`,
|
|
151
|
+
`cryptography`, `requests`, `urllib3` — and it is worth saying where they come
|
|
152
|
+
from: **every one of them is a dependency of `safety` itself**. The scanner that
|
|
153
|
+
cannot run is also the largest source of vulnerable packages in the environment.
|
|
154
|
+
They are left alone: they are development-only, they do not ship in the
|
|
155
|
+
published package, and resolving it means deciding whether `safety` stays.
|
|
156
|
+
|
|
157
|
+
### Documentation
|
|
158
|
+
|
|
159
|
+
- **changelog**: record under 0.4.0 the two commits that landed after the tag
|
|
160
|
+
|
|
161
|
+
The `v0.4.0` tag sat on `chore(release): 0.4.0`, and two commits went in after
|
|
162
|
+
it: the malformed-`groups` fix and dropping `towncrier`. Since the tag was
|
|
163
|
+
**not published** (the remote does not have it), the fix can still go into 0.4.0
|
|
164
|
+
rather than forcing a 0.4.1 — and it should, because it is a `TypeError`
|
|
165
|
+
escaping the SDK.
|
|
166
|
+
|
|
167
|
+
- **changelog**: generate CHANGELOG.md with git-cliff, commit body included
|
|
168
|
+
|
|
169
|
+
The changelog in this repository was written by hand. The first attempt at
|
|
170
|
+
switching to generation was measured and **thrown away**: with git-cliff's
|
|
171
|
+
default template, which uses only the subject line, the file dropped from 246 to
|
|
172
|
+
61 lines — the "what" survived and the "why" disappeared.
|
|
173
|
+
|
|
174
|
+
What fixes it is including the commit **body**. The messages here already
|
|
175
|
+
explain the decision, so the information was never only in the changelog: it was
|
|
176
|
+
duplicated. The generated file now runs to 222 lines, and every entry carries
|
|
177
|
+
the text that explains it.
|
|
178
|
+
|
|
179
|
+
Three details in `cliff.toml`, each from a defect visible in the output:
|
|
180
|
+
|
|
181
|
+
- `indent(prefix=" ", first=true, blank=false)` on the body. Without
|
|
182
|
+
`first=true` the body's first line falls outside the list item; with
|
|
183
|
+
`blank=true` the empty lines pick up two spaces and the `trailing-whitespace`
|
|
184
|
+
hook fails the commit.
|
|
185
|
+
- A postprocessor demotes to bold any `#` heading arriving inside a body —
|
|
186
|
+
indented, ATX is still a heading (it allows up to three leading spaces) and
|
|
187
|
+
would collide with the changelog's own levels. The pattern uses `[ \t]` and
|
|
188
|
+
**not** `\s`: `\s` matches `\n`, and in the first version it reached across
|
|
189
|
+
the line break and ate the changelog's own `## [0.4.0]`, which turned bold.
|
|
190
|
+
- Another postprocessor puts a blank line before each `## [version]`, because
|
|
191
|
+
`trim = true` glues the heading to the last line of the previous release.
|
|
192
|
+
|
|
193
|
+
Two sections shrink, and it is worth naming them: **0.1.0** (81 → 7 lines) was
|
|
194
|
+
an inventory of the API — the same thing the README already carries — and
|
|
195
|
+
**0.3.0** (55 → 11) loses the detail of the four contract bugs, because that
|
|
196
|
+
release's commit has a short body. The old text stays recoverable in git
|
|
197
|
+
history. From here the rule is different: the commit body is the source, so a
|
|
198
|
+
thin message makes a thin entry.
|
|
199
|
+
|
|
200
|
+
Along with it:
|
|
201
|
+
|
|
202
|
+
- `task changelog` generates and `task changelog-check` fails when the file is
|
|
203
|
+
out of date with the commits (a CI gate).
|
|
204
|
+
- `NEWS.rst` goes. It was a third changelog, in the format towncrier would
|
|
205
|
+
produce, with nothing generating it and nothing referencing it — a dead file
|
|
206
|
+
for the same reason towncrier left in the previous commit.
|
|
207
|
+
|
|
208
|
+
One honest caveat: that commit's body ends by saying "the changelog in this
|
|
209
|
+
repository is written by hand". It was true when written and stopped being true
|
|
210
|
+
now; a changelog is a historical record, and rewording the message to hide that
|
|
211
|
+
would cost rewriting already-published history.
|
|
212
|
+
|
|
213
|
+
### Maintenance
|
|
214
|
+
|
|
215
|
+
- **deps**: drop towncrier, declared and never configured
|
|
216
|
+
|
|
217
|
+
`towncrier` had been a dev dependency from before, **without a
|
|
218
|
+
`[tool.towncrier]` section** in `pyproject.toml`, without a `towncrier.toml` and
|
|
219
|
+
without a fragments directory. Running it fails: "the config file does not
|
|
220
|
+
contain 'version' or 'package'".
|
|
221
|
+
|
|
222
|
+
`NEWS.rst` is in the format it *would* produce, which is worse than having
|
|
223
|
+
nothing: whoever arrives concludes the file is generated and leaves it alone, or
|
|
224
|
+
tries to run the tool and finds out it cannot. That happened here today — the
|
|
225
|
+
changelog was written by hand without noticing there was a tool declared.
|
|
226
|
+
|
|
227
|
+
A declared, unused dependency is the same family as dead code. The changelog in
|
|
228
|
+
this repository is written by hand, and now `pyproject.toml` says so.
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
## [0.3.1] - 2026-09-04
|
|
232
|
+
|
|
233
|
+
### Fixed
|
|
234
|
+
|
|
235
|
+
- **deps**: move mutmut out of the runtime dependencies; release 0.3.1
|
|
236
|
+
|
|
237
|
+
mutmut was declared in [tool.poetry.dependencies] rather than the dev group,
|
|
238
|
+
so installing this SDK pulled a mutation-testing tool and its whole tree
|
|
239
|
+
(libcst, textual, rich, setproctitle, pyyaml-ft, markdown-it-py,
|
|
240
|
+
mdit-py-plugins, mdurl, linkify-it-py, sortedcontainers) into every consumer,
|
|
241
|
+
production images included.
|
|
242
|
+
|
|
243
|
+
Nothing in keyrunes_sdk imports it, so upgrading only removes packages. The
|
|
244
|
+
wheel now declares httpx, pydantic[email] and pyjwt and nothing else.
|
|
245
|
+
|
|
246
|
+
Affected 0.2.0 and 0.3.0.
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
## [0.3.0] - 2026-09-03
|
|
250
|
+
|
|
251
|
+
### Added
|
|
252
|
+
|
|
253
|
+
- **client**: carry the full server identity and add refresh; release 0.3.0
|
|
254
|
+
|
|
255
|
+
Migrating a real consumer (an LMS keying its user rows off the JWT `sub`)
|
|
256
|
+
surfaced four contract bugs against a live Keyrunes server and three fields
|
|
257
|
+
the SDK was discarding.
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
## [0.2.0] - 2026-09-03
|
|
261
|
+
|
|
262
|
+
### Added
|
|
263
|
+
|
|
264
|
+
- **tests**: add property-based, fuzz and contract suites; release 0.2.0
|
|
265
|
+
|
|
266
|
+
Add three new test suites and the tooling to measure them:
|
|
267
|
+
|
|
268
|
+
- tests/test_property_based.py (24 tests): Hypothesis invariants over base
|
|
269
|
+
URL normalization, _normalize_user id precedence and is_admin derivation,
|
|
270
|
+
JWT claim parsing, request URL construction, status-to-exception mapping
|
|
271
|
+
and model validation bounds.
|
|
272
|
+
- tests/test_fuzz.py (19 tests): hostile-input crawl of the public surface
|
|
273
|
+
asserting only KeyrunesError and pydantic ValidationError ever escape.
|
|
274
|
+
- tests/test_request_contract.py (33 tests): mocks only the httpx transport
|
|
275
|
+
so the method, URL, headers and JSON body put on the wire are asserted.
|
|
276
|
+
|
|
277
|
+
Hypothesis profiles (fast/dev/ci) are selected with HYPOTHESIS_PROFILE;
|
|
278
|
+
fast is derandomized and database-free so mutmut judges every mutant
|
|
279
|
+
against identical examples.
|
|
280
|
+
|
|
281
|
+
Two defects the new suites uncovered are fixed:
|
|
282
|
+
|
|
283
|
+
- A 2xx response with a non-JSON body raised a raw ValueError out of
|
|
284
|
+
_make_request. It is now wrapped in NetworkError.
|
|
285
|
+
- get_user, get_current_user and has_group carried unreachable
|
|
286
|
+
"except UserNotFoundError" fallbacks that re-tested a condition already
|
|
287
|
+
forced by an early return, swallowing genuine server 404s. Removed, and
|
|
288
|
+
the duplicated claims-to-User block extracted into
|
|
289
|
+
_user_from_token_claims().
|
|
290
|
+
|
|
291
|
+
### Maintenance
|
|
292
|
+
|
|
293
|
+
- add changelogs
|
|
294
|
+
|
|
295
|
+
- update lib version in pyproject
|
|
296
|
+
|
|
297
|
+
- add pypi url in README
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
## [0.1.0] - 2026-01-03
|
|
301
|
+
|
|
302
|
+
### Added
|
|
303
|
+
|
|
304
|
+
- add organization key for keyrunes v0.2.0
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
## [0.0.1] - 2025-12-08
|
|
308
|
+
|
|
309
|
+
### Added
|
|
310
|
+
|
|
311
|
+
- add login, register, check_admin, check_user, get_current_user features
|
|
312
|
+
|
|
313
|
+
### Fixed
|
|
314
|
+
|
|
315
|
+
- fix lib version
|
|
316
|
+
|
|
317
|
+
### Infrastructure
|
|
318
|
+
|
|
319
|
+
- fix lint errors and add pre-commit
|
|
320
|
+
|
|
321
|
+
### Maintenance
|
|
322
|
+
|
|
323
|
+
- add changelogs
|
|
324
|
+
|
|
325
|
+
### Other
|
|
326
|
+
|
|
327
|
+
- Initial commit
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
<!-- generated by git-cliff -->
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from typing import Any, Dict, List, Optional
|
|
5
|
-
from urllib.parse import urljoin
|
|
6
5
|
|
|
7
6
|
import httpx
|
|
8
7
|
import jwt
|
|
@@ -42,6 +41,12 @@ ENDPOINT_REFRESH_TOKEN = "/api/refresh-token"
|
|
|
42
41
|
#: ``#[allow(dead_code)]``. Pointing at it answers 404.
|
|
43
42
|
ENDPOINT_CHANGE_PASSWORD = "/api/user/change-password"
|
|
44
43
|
|
|
44
|
+
#: Group names that carry administrative rights in the Keyrunes server.
|
|
45
|
+
#: Matching is EXACT: a substring test ("admin" in g.lower()) used to treat
|
|
46
|
+
#: "non-admin" or "billing-admin-viewer" as privileged — a user created in
|
|
47
|
+
#: any such group passed ``@require_admin``.
|
|
48
|
+
PRIVILEGED_GROUPS = frozenset({"admin", "admins", "superadmin"})
|
|
49
|
+
|
|
45
50
|
|
|
46
51
|
class KeyrunesClient:
|
|
47
52
|
"""
|
|
@@ -102,6 +107,7 @@ class KeyrunesClient:
|
|
|
102
107
|
data: Optional[Dict[str, Any]] = None,
|
|
103
108
|
params: Optional[Dict[str, Any]] = None,
|
|
104
109
|
use_auth: bool = True,
|
|
110
|
+
headers: Optional[Dict[str, str]] = None,
|
|
105
111
|
) -> Dict[str, Any]:
|
|
106
112
|
"""
|
|
107
113
|
Make HTTP request to Keyrunes API.
|
|
@@ -112,6 +118,11 @@ class KeyrunesClient:
|
|
|
112
118
|
data: Request body data
|
|
113
119
|
params: Query parameters
|
|
114
120
|
use_auth: Whether to include authentication header
|
|
121
|
+
headers: Extra per-call headers. An ``Authorization`` given here
|
|
122
|
+
wins over the stored client token — used by
|
|
123
|
+
``refresh_token``, which must authenticate with the token
|
|
124
|
+
being exchanged even when the client already holds another
|
|
125
|
+
one.
|
|
115
126
|
|
|
116
127
|
Returns:
|
|
117
128
|
Response JSON data
|
|
@@ -121,11 +132,11 @@ class KeyrunesClient:
|
|
|
121
132
|
AuthenticationError: If authentication fails (401)
|
|
122
133
|
AuthorizationError: If authorization fails (403)
|
|
123
134
|
"""
|
|
124
|
-
url =
|
|
125
|
-
|
|
135
|
+
url = f"{self.base_url}/{endpoint.lstrip('/')}"
|
|
136
|
+
request_headers: Dict[str, str] = dict(headers or {})
|
|
126
137
|
|
|
127
|
-
if use_auth and self._token:
|
|
128
|
-
|
|
138
|
+
if use_auth and self._token and "Authorization" not in request_headers:
|
|
139
|
+
request_headers["Authorization"] = f"Bearer {self._token}"
|
|
129
140
|
|
|
130
141
|
try:
|
|
131
142
|
response = self._client.request(
|
|
@@ -133,7 +144,7 @@ class KeyrunesClient:
|
|
|
133
144
|
url=url,
|
|
134
145
|
json=data,
|
|
135
146
|
params=params,
|
|
136
|
-
headers=
|
|
147
|
+
headers=request_headers,
|
|
137
148
|
)
|
|
138
149
|
|
|
139
150
|
if response.status_code == 401:
|
|
@@ -182,10 +193,11 @@ class KeyrunesClient:
|
|
|
182
193
|
)
|
|
183
194
|
normalized["username"] = data.get("username", "")
|
|
184
195
|
normalized["email"] = data.get("email", "")
|
|
185
|
-
# `or []`
|
|
186
|
-
# ``{"groups": true}``
|
|
187
|
-
# ``"admins" in groups``
|
|
188
|
-
#
|
|
196
|
+
# `or []` handles null and empty and **nothing else**: with
|
|
197
|
+
# ``{"groups": true}`` the value survived as ``True``, and the
|
|
198
|
+
# ``"admins" in groups`` just below raised ``TypeError`` — the client
|
|
199
|
+
# fell over instead of containing a malformed response. Found by the
|
|
200
|
+
# fuzz suite.
|
|
189
201
|
raw_groups = data.get("groups")
|
|
190
202
|
normalized["groups"] = (
|
|
191
203
|
list(raw_groups) if isinstance(raw_groups, list) else []
|
|
@@ -194,10 +206,13 @@ class KeyrunesClient:
|
|
|
194
206
|
normalized["is_active"] = data.get("is_active", True)
|
|
195
207
|
groups = normalized["groups"]
|
|
196
208
|
is_admin_flag = data.get("is_admin", False)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
209
|
+
# Exact membership only. The previous heuristic (`"admin" in
|
|
210
|
+
# str(g).lower()`) granted admin privileges to any group whose name
|
|
211
|
+
# merely *contained* "admin" — "non-admin", "billing-admin-viewer"
|
|
212
|
+
# and friends all unlocked @require_admin. "admins" stays in the set
|
|
213
|
+
# because server fixtures still seed that legacy spelling.
|
|
214
|
+
normalized["is_admin"] = bool(is_admin_flag) or any(
|
|
215
|
+
isinstance(g, str) and g in PRIVILEGED_GROUPS for g in groups
|
|
201
216
|
)
|
|
202
217
|
# Carried through untouched so a caller can key its own records off the
|
|
203
218
|
# same identifier the JWT ``sub`` uses, rather than off ``id``.
|
|
@@ -400,6 +415,16 @@ class KeyrunesClient:
|
|
|
400
415
|
"""
|
|
401
416
|
Register a new admin user.
|
|
402
417
|
|
|
418
|
+
.. deprecated:: 0.5.0
|
|
419
|
+
Since Keyrunes server 0.4.2 (C11-008 remediation) ``POST
|
|
420
|
+
/api/register`` **ignores** ``admin_key`` and never promotes the
|
|
421
|
+
registrant: the endpoint only ever creates a normal user.
|
|
422
|
+
Administrators must be created through an authenticated
|
|
423
|
+
``POST /api/admin/user`` (with the caller's JWT) or through the
|
|
424
|
+
server's CLI bootstrap. This method is kept so existing code
|
|
425
|
+
still calls a registered endpoint, but do not expect the
|
|
426
|
+
returned user to hold any privilege.
|
|
427
|
+
|
|
403
428
|
Args:
|
|
404
429
|
username: Username (3-50 characters)
|
|
405
430
|
email: Admin email address
|
|
@@ -471,10 +496,17 @@ class KeyrunesClient:
|
|
|
471
496
|
)
|
|
472
497
|
|
|
473
498
|
if token_user_id and str(user_id) == token_user_id:
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
499
|
+
# SECURITY: the JWT held by this client was decoded with
|
|
500
|
+
# ``verify_signature=False`` — anyone can mint one claiming any
|
|
501
|
+
# group. The claims said nothing about whether the server still
|
|
502
|
+
# accepts this token, so the answer comes from ``/api/me``,
|
|
503
|
+
# which only a live, valid token can query. Fail closed: a 404
|
|
504
|
+
# from the server means no access, it does not raise.
|
|
505
|
+
try:
|
|
506
|
+
user = self.get_current_user(force_refresh=True)
|
|
507
|
+
except UserNotFoundError:
|
|
508
|
+
return False
|
|
509
|
+
return group_id in user.groups
|
|
478
510
|
|
|
479
511
|
try:
|
|
480
512
|
response = self._make_request(
|
|
@@ -628,11 +660,15 @@ class KeyrunesClient:
|
|
|
628
660
|
if not current:
|
|
629
661
|
raise InvalidTokenError("No token available to refresh.")
|
|
630
662
|
|
|
663
|
+
# The server binds the refreshed identity to the token in the
|
|
664
|
+
# Authorization header (require_jwt middleware) and ignores the body
|
|
665
|
+
# since the 0.4.1 fix. Authenticate with the token being exchanged.
|
|
631
666
|
response = self._make_request(
|
|
632
667
|
"POST",
|
|
633
668
|
ENDPOINT_REFRESH_TOKEN,
|
|
634
669
|
data={"token": current},
|
|
635
670
|
use_auth=False,
|
|
671
|
+
headers={"Authorization": f"Bearer {current}"},
|
|
636
672
|
)
|
|
637
673
|
|
|
638
674
|
refreshed = self._parse_token_response(response)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from functools import wraps
|
|
4
4
|
from typing import Any, Callable, Optional
|
|
5
5
|
|
|
6
|
-
from keyrunes_sdk.client import KeyrunesClient
|
|
6
|
+
from keyrunes_sdk.client import PRIVILEGED_GROUPS, KeyrunesClient
|
|
7
7
|
from keyrunes_sdk.exceptions import AuthorizationError, UserNotFoundError
|
|
8
8
|
|
|
9
9
|
|
|
@@ -218,11 +218,14 @@ def require_admin(
|
|
|
218
218
|
if keyrunes_client._token_data
|
|
219
219
|
else []
|
|
220
220
|
)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
221
|
+
# Exact membership only. The old heuristic treated ANY
|
|
222
|
+
# group whose name contained the substring "admin"
|
|
223
|
+
# ("non-admin", "billing-admin-viewer") as admin — a
|
|
224
|
+
# group-name collision silently granted privileges.
|
|
225
|
+
# KeyRunes' own names are "admin" and "superadmin";
|
|
226
|
+
# "admins" is kept because deployments and the SDK
|
|
227
|
+
# fixtures historically use it.
|
|
228
|
+
is_admin = any(g in PRIVILEGED_GROUPS for g in groups)
|
|
226
229
|
if not is_admin:
|
|
227
230
|
raise AuthorizationError(
|
|
228
231
|
f"User '{user_id}' does not have admin privileges."
|
|
@@ -85,7 +85,9 @@ class UserRegistration(BaseModel):
|
|
|
85
85
|
)
|
|
86
86
|
email: EmailStr = Field(..., description="User email")
|
|
87
87
|
password: str = Field(..., min_length=8, description="Password")
|
|
88
|
-
|
|
88
|
+
# The server's default tenant is "public", not "default"; a payload that
|
|
89
|
+
# registered into "default" created a stray namespace nobody configured.
|
|
90
|
+
namespace: str = Field("public", description="User namespace")
|
|
89
91
|
attributes: Dict[str, Any] = Field(
|
|
90
92
|
default_factory=dict, description="Additional attributes"
|
|
91
93
|
)
|
|
@@ -102,11 +104,12 @@ class LoginCredentials(BaseModel):
|
|
|
102
104
|
|
|
103
105
|
identity: str = Field(..., description="Username or email")
|
|
104
106
|
password: str = Field(..., description="Password")
|
|
105
|
-
|
|
107
|
+
# "public" is the server's default tenant; see UserRegistration.namespace.
|
|
108
|
+
namespace: str = Field("public", description="User namespace")
|
|
106
109
|
|
|
107
110
|
@classmethod
|
|
108
111
|
def from_username(
|
|
109
|
-
cls, username: str, password: str, namespace: str = "
|
|
112
|
+
cls, username: str, password: str, namespace: str = "public"
|
|
110
113
|
) -> "LoginCredentials":
|
|
111
114
|
"""Create from username parameter (for backward compatibility)."""
|
|
112
115
|
return cls(identity=username, password=password, namespace=namespace)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "keyrunes-python-sdk"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
description = "Python SDK for Keyrunes Authorization System"
|
|
5
5
|
authors = ["keyrunes <contact@singularjourney.host>"]
|
|
6
6
|
maintainers = ["jonatasoli <contact@jonatasoli.dev>"]
|
|
@@ -121,5 +121,7 @@ publish = "poetry build && twine check dist/* && poetry publish"
|
|
|
121
121
|
version-patch = "poetry version patch"
|
|
122
122
|
version-minor = "poetry version minor"
|
|
123
123
|
version-major = "poetry version major"
|
|
124
|
+
changelog = "git cliff -o CHANGELOG.md"
|
|
125
|
+
changelog-check = "git cliff -o /dev/stdout | diff -q - CHANGELOG.md"
|
|
124
126
|
all = "poetry run task format && poetry run task lint && poetry run task type-check && poetry run task test"
|
|
125
127
|
ci = "poetry run task lint && poetry run task type-check && poetry run task test"
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
Todas as mudanças notáveis neste projeto serão documentadas neste arquivo.
|
|
4
|
-
|
|
5
|
-
O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.0.0/),
|
|
6
|
-
e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
|
|
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
|
-
|
|
73
|
-
## [0.3.0] - 2026-09-03
|
|
74
|
-
|
|
75
|
-
### Added
|
|
76
|
-
|
|
77
|
-
- `User.user_id`, carrying the internal identifier Keyrunes puts in the JWT
|
|
78
|
-
`sub` claim. It is a different value from `User.id` (the external UUID), and
|
|
79
|
-
a consumer that keys its own records off `sub` needs it; before this release
|
|
80
|
-
it was discarded during normalization and could only be recovered by
|
|
81
|
-
re-parsing the token.
|
|
82
|
-
- `User.namespace`, `User.organization_id` and `User.first_login`, all of which
|
|
83
|
-
the server returns and normalization used to drop.
|
|
84
|
-
- `Token.requires_password_change`, so a login that must be followed by a
|
|
85
|
-
password change can be detected without reading the raw response.
|
|
86
|
-
- `KeyrunesClient.refresh_token()`, wrapping `POST /api/refresh-token`. It
|
|
87
|
-
accepts an explicit token or falls back to the client's current one, raises
|
|
88
|
-
`InvalidTokenError` when neither is available, and adopts the refreshed
|
|
89
|
-
token the way `login()` does.
|
|
90
|
-
- `get_current_user(force_refresh=True)`, which always asks the server. The
|
|
91
|
-
claims shortcut reads a token the SDK never verified, so it cannot answer
|
|
92
|
-
"is this token still accepted"; a caller validating a token needs the round
|
|
93
|
-
trip.
|
|
94
|
-
- `KeyrunesError.status_code`, carrying the HTTP status when the error came
|
|
95
|
-
from a response rather than from the transport. Without it a caller cannot
|
|
96
|
-
tell "the server refused this request" (4xx) from "the server or the network
|
|
97
|
-
failed" (5xx, or no response at all), and both are raised as `NetworkError`.
|
|
98
|
-
- `register_user(group=...)`, sent as a top-level field.
|
|
99
|
-
- A `py.typed` marker (PEP 561). The package was already fully typed, but
|
|
100
|
-
without the marker a consumer running mypy saw every SDK import as
|
|
101
|
-
untyped.
|
|
102
|
-
|
|
103
|
-
### Fixed
|
|
104
|
-
|
|
105
|
-
- `get_current_user()` asked for `/api/users/me`, which the Keyrunes router
|
|
106
|
-
does not expose — the real route is `/api/me`, so the call answered 404
|
|
107
|
-
against a live server whenever the claims shortcut did not cover it. The
|
|
108
|
-
endpoint is now a named constant, `ENDPOINT_ME`.
|
|
109
|
-
- `register_user()` and `register_admin()` required the response to be
|
|
110
|
-
`{"user": {...}}`, but `POST /api/register` answers with the bare user
|
|
111
|
-
object. Every registration against a real server failed with "Unexpected
|
|
112
|
-
response format". Both shapes are now accepted.
|
|
113
|
-
- Extra keyword arguments to `register_user()` were nested under `attributes`,
|
|
114
|
-
so a `group` never reached the server, which reads it as a top-level field.
|
|
115
|
-
`group` is now an explicit parameter placed at the top level; other keyword
|
|
116
|
-
attributes keep nesting under `attributes`.
|
|
117
|
-
|
|
118
|
-
### Testing
|
|
119
|
-
|
|
120
|
-
- Test count raised from 159 to 189.
|
|
121
|
-
|
|
122
|
-
### Notes
|
|
123
|
-
|
|
124
|
-
- Every added field is optional with a backwards-compatible default, and
|
|
125
|
-
`User.id` keeps its existing precedence (`id` → `user_id` → `external_id`).
|
|
126
|
-
Code written against 0.2.0 keeps working unchanged.
|
|
127
|
-
|
|
128
|
-
## [0.2.0] - 2026-09-03
|
|
129
|
-
|
|
130
|
-
### Added
|
|
131
|
-
|
|
132
|
-
- Property-based test suite (`tests/test_property_based.py`, 24 tests) built on
|
|
133
|
-
Hypothesis, covering base URL normalization, `_normalize_user` id precedence
|
|
134
|
-
and `is_admin` derivation, JWT claim parsing, request URL construction, HTTP
|
|
135
|
-
status to exception mapping, and model validation bounds.
|
|
136
|
-
- Input fuzzing ("spider") suite (`tests/test_fuzz.py`, 19 tests) that crawls the
|
|
137
|
-
public surface with hostile payloads and asserts that only `KeyrunesError` and
|
|
138
|
-
pydantic `ValidationError` ever escape, and that nothing panics or leaks a raw
|
|
139
|
-
`ValueError`/`TypeError`.
|
|
140
|
-
- Request contract suite (`tests/test_request_contract.py`, 33 tests) that mocks
|
|
141
|
-
only the httpx transport, so the method, URL, headers and JSON body actually
|
|
142
|
-
put on the wire are asserted for every endpoint.
|
|
143
|
-
- Hypothesis profiles in `tests/conftest.py` (`fast`, `dev`, `ci`) selected via
|
|
144
|
-
the `HYPOTHESIS_PROFILE` environment variable. `fast` is derandomized and
|
|
145
|
-
database-free so mutation runs judge every mutant against identical examples.
|
|
146
|
-
- `[tool.mutmut]` configuration in `pyproject.toml` for mutation testing.
|
|
147
|
-
- `hypothesis` added as a development dependency.
|
|
148
|
-
|
|
149
|
-
### Fixed
|
|
150
|
-
|
|
151
|
-
- A 2xx response carrying a non-JSON body raised a raw `ValueError` out of
|
|
152
|
-
`KeyrunesClient._make_request`. It is now wrapped in `NetworkError`, so the
|
|
153
|
-
documented exception contract holds for malformed responses.
|
|
154
|
-
|
|
155
|
-
### Changed
|
|
156
|
-
|
|
157
|
-
- Extracted the duplicated "build a `User` from JWT claims" block into
|
|
158
|
-
`KeyrunesClient._user_from_token_claims()`.
|
|
159
|
-
|
|
160
|
-
### Removed
|
|
161
|
-
|
|
162
|
-
- Unreachable `except UserNotFoundError` fallbacks in `get_user`,
|
|
163
|
-
`get_current_user` and `has_group`. Each re-tested a condition that had
|
|
164
|
-
already forced an early return, so a 404 from the server was being swallowed
|
|
165
|
-
instead of propagated.
|
|
166
|
-
|
|
167
|
-
### Testing
|
|
168
|
-
|
|
169
|
-
- Test count raised from 83 to 159.
|
|
170
|
-
- Mutation score (mutmut) raised from 44% (294/669 mutants killed) to 72%
|
|
171
|
-
(387/537). The remaining survivors are predominantly equivalent mutants that
|
|
172
|
-
only alter error message prose.
|
|
173
|
-
|
|
174
|
-
## [0.1.0] - 2025-12-03
|
|
175
|
-
|
|
176
|
-
### Adicionado
|
|
177
|
-
|
|
178
|
-
#### Funcionalidades Core
|
|
179
|
-
- Cliente `KeyrunesClient` completo para interação com Keyrunes API
|
|
180
|
-
- Autenticação com login de usuário e admin
|
|
181
|
-
- Registro de usuário e admin com validação
|
|
182
|
-
- Verificação de pertencimento a grupos
|
|
183
|
-
- Obtenção de informações de usuários
|
|
184
|
-
|
|
185
|
-
#### Decorators
|
|
186
|
-
- `@require_group()` - Decorator para verificar grupos de usuários
|
|
187
|
-
- `@require_admin()` - Decorator para verificar privilégios de admin
|
|
188
|
-
- Suporte para múltiplos grupos (ANY ou ALL)
|
|
189
|
-
- Sistema de client global para uso sem passar client explicitamente
|
|
190
|
-
|
|
191
|
-
#### Modelos Pydantic
|
|
192
|
-
- `User` - Modelo de usuário com validação
|
|
193
|
-
- `Token` - Modelo de token JWT
|
|
194
|
-
- `Group` - Modelo de grupo
|
|
195
|
-
- `UserRegistration` - Dados de registro de usuário
|
|
196
|
-
- `AdminRegistration` - Dados de registro de admin
|
|
197
|
-
- `LoginCredentials` - Credenciais de login
|
|
198
|
-
- `GroupCheck` - Resultado de verificação de grupo
|
|
199
|
-
|
|
200
|
-
#### Exceções Customizadas
|
|
201
|
-
- `KeyrunesError` - Exceção base
|
|
202
|
-
- `AuthenticationError` - Erro de autenticação
|
|
203
|
-
- `AuthorizationError` - Erro de autorização
|
|
204
|
-
- `GroupNotFoundError` - Grupo não encontrado
|
|
205
|
-
- `UserNotFoundError` - Usuário não encontrado
|
|
206
|
-
- `InvalidTokenError` - Token inválido
|
|
207
|
-
- `NetworkError` - Erro de rede
|
|
208
|
-
|
|
209
|
-
#### Sistema de Configuração Global
|
|
210
|
-
- `configure()` - Configura client global
|
|
211
|
-
- `get_global_client()` - Obtém client global
|
|
212
|
-
- `clear_global_client()` - Limpa client global
|
|
213
|
-
- Thread-safe com Lock
|
|
214
|
-
|
|
215
|
-
#### Desenvolvimento e Testes
|
|
216
|
-
- Docker Compose completo com Keyrunes, PostgreSQL e Redis
|
|
217
|
-
- 78 testes com 99% de cobertura
|
|
218
|
-
- Testes usando pytest, factory-boy e faker
|
|
219
|
-
- Exemplos práticos de uso
|
|
220
|
-
- Makefile com comandos úteis
|
|
221
|
-
- Configuração completa de CI/CD
|
|
222
|
-
|
|
223
|
-
#### Documentação
|
|
224
|
-
- README.md completo com exemplos
|
|
225
|
-
- TESTING.md com guia de testes
|
|
226
|
-
- Docstrings em todas as funções
|
|
227
|
-
- Type hints completos
|
|
228
|
-
- Exemplos práticos em `examples/`
|
|
229
|
-
|
|
230
|
-
### Detalhes Técnicos
|
|
231
|
-
|
|
232
|
-
- Python 3.8.1+ compatível
|
|
233
|
-
- Gerenciamento com Poetry
|
|
234
|
-
- Validação com Pydantic 2.0
|
|
235
|
-
- Type hints completos
|
|
236
|
-
- Thread-safe
|
|
237
|
-
- Context manager support
|
|
238
|
-
|
|
239
|
-
### Testes
|
|
240
|
-
|
|
241
|
-
- 78 testes implementados
|
|
242
|
-
- 99% de cobertura de código
|
|
243
|
-
- Testes unitários e de integração
|
|
244
|
-
- Factories com factory-boy
|
|
245
|
-
- Dados fake com Faker
|
|
246
|
-
|
|
247
|
-
### Ferramentas de Desenvolvimento
|
|
248
|
-
|
|
249
|
-
- Black para formatação
|
|
250
|
-
- isort para organização de imports
|
|
251
|
-
- flake8 para linting
|
|
252
|
-
- mypy para type checking
|
|
253
|
-
- pytest para testes
|
|
254
|
-
|
|
255
|
-
## [Unreleased]
|
|
256
|
-
|
|
257
|
-
### Planejado
|
|
258
|
-
|
|
259
|
-
- Suporte para refresh token automático
|
|
260
|
-
- Cache de verificações de grupo
|
|
261
|
-
- Suporte para OIDC
|
|
262
|
-
- Integração com FastAPI
|
|
263
|
-
- Integração com Flask
|
|
264
|
-
- Integração com Django
|
|
265
|
-
- Mais exemplos práticos
|
|
266
|
-
- Documentação com Sphinx
|
|
267
|
-
- Publicação no PyPI
|
|
268
|
-
|
|
269
|
-
---
|
|
270
|
-
|
|
271
|
-
Para mais detalhes sobre cada versão, veja os [releases no GitHub](https://github.com/jonatasoli/keyurnes-sdk-python-dark/releases).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|