keyrunes-python-sdk 0.0.1__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.0.1 → keyrunes_python_sdk-0.2.0}/CHANGELOG.md +46 -0
- {keyrunes_python_sdk-0.0.1 → keyrunes_python_sdk-0.2.0}/PKG-INFO +7 -4
- {keyrunes_python_sdk-0.0.1 → keyrunes_python_sdk-0.2.0}/README.md +2 -1
- {keyrunes_python_sdk-0.0.1 → keyrunes_python_sdk-0.2.0}/keyrunes_sdk/__init__.py +1 -1
- {keyrunes_python_sdk-0.0.1 → keyrunes_python_sdk-0.2.0}/keyrunes_sdk/client.py +68 -60
- {keyrunes_python_sdk-0.0.1 → keyrunes_python_sdk-0.2.0}/keyrunes_sdk/config.py +9 -1
- {keyrunes_python_sdk-0.0.1 → keyrunes_python_sdk-0.2.0}/keyrunes_sdk/models.py +6 -2
- {keyrunes_python_sdk-0.0.1 → keyrunes_python_sdk-0.2.0}/pyproject.toml +17 -2
- {keyrunes_python_sdk-0.0.1 → keyrunes_python_sdk-0.2.0}/LICENSE +0 -0
- {keyrunes_python_sdk-0.0.1 → keyrunes_python_sdk-0.2.0}/keyrunes_sdk/decorators.py +0 -0
- {keyrunes_python_sdk-0.0.1 → keyrunes_python_sdk-0.2.0}/keyrunes_sdk/exceptions.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.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
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Keyrunes API Client."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from typing import Any, Dict, List, Optional
|
|
4
5
|
from urllib.parse import urljoin
|
|
5
6
|
|
|
@@ -38,6 +39,8 @@ class KeyrunesClient:
|
|
|
38
39
|
base_url: Base URL of the Keyrunes API
|
|
39
40
|
(e.g., "https://keyrunes.example.com")
|
|
40
41
|
api_key: Optional API key for authentication
|
|
42
|
+
organization_key: Optional Organization Key (required for v0.2.0+)
|
|
43
|
+
If not provided, looks for KEYRUNES_ORG_KEY env var
|
|
41
44
|
timeout: Request timeout in seconds (default: 30)
|
|
42
45
|
|
|
43
46
|
Example:
|
|
@@ -50,11 +53,16 @@ class KeyrunesClient:
|
|
|
50
53
|
self,
|
|
51
54
|
base_url: str,
|
|
52
55
|
api_key: Optional[str] = None,
|
|
56
|
+
organization_key: Optional[str] = None,
|
|
53
57
|
timeout: int = 30,
|
|
54
58
|
) -> None:
|
|
55
59
|
"""Initialize Keyrunes client."""
|
|
56
60
|
self.base_url = base_url.rstrip("/")
|
|
57
61
|
self.api_key = api_key
|
|
62
|
+
# Prioritize explicit argument, then env var
|
|
63
|
+
self.organization_key = organization_key or os.getenv(
|
|
64
|
+
"KEYRUNES_ORG_KEY"
|
|
65
|
+
)
|
|
58
66
|
self.timeout = timeout
|
|
59
67
|
self._token: Optional[str] = None
|
|
60
68
|
self._token_data: Optional[Dict[str, Any]] = None
|
|
@@ -63,6 +71,11 @@ class KeyrunesClient:
|
|
|
63
71
|
if api_key:
|
|
64
72
|
self._client.headers.update({"X-API-Key": api_key})
|
|
65
73
|
|
|
74
|
+
if self.organization_key:
|
|
75
|
+
self._client.headers.update(
|
|
76
|
+
{"X-Organization-Key": self.organization_key}
|
|
77
|
+
)
|
|
78
|
+
|
|
66
79
|
def _make_request(
|
|
67
80
|
self,
|
|
68
81
|
method: str,
|
|
@@ -124,7 +137,12 @@ class KeyrunesClient:
|
|
|
124
137
|
)
|
|
125
138
|
raise NetworkError(f"Request failed: {error_msg}")
|
|
126
139
|
|
|
127
|
-
|
|
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
|
+
)
|
|
128
146
|
return result
|
|
129
147
|
|
|
130
148
|
except httpx.RequestError as e:
|
|
@@ -154,6 +172,21 @@ class KeyrunesClient:
|
|
|
154
172
|
)
|
|
155
173
|
return User(**normalized)
|
|
156
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
|
+
|
|
157
190
|
def _parse_token_response(self, payload: Dict[str, Any]) -> Token:
|
|
158
191
|
"""
|
|
159
192
|
Accept both legacy and current API token responses.
|
|
@@ -190,13 +223,16 @@ class KeyrunesClient:
|
|
|
190
223
|
user=user_model,
|
|
191
224
|
)
|
|
192
225
|
|
|
193
|
-
def login(
|
|
226
|
+
def login(
|
|
227
|
+
self, username: str, password: str, namespace: str = "public"
|
|
228
|
+
) -> Token:
|
|
194
229
|
"""
|
|
195
230
|
Authenticate user and obtain access token.
|
|
196
231
|
|
|
197
232
|
Args:
|
|
198
233
|
username: Username or email
|
|
199
234
|
password: User password
|
|
235
|
+
namespace: User namespace (default: "public")
|
|
200
236
|
|
|
201
237
|
Returns:
|
|
202
238
|
Token object containing access token and user info
|
|
@@ -206,10 +242,14 @@ class KeyrunesClient:
|
|
|
206
242
|
|
|
207
243
|
Example:
|
|
208
244
|
>>> client = KeyrunesClient("https://keyrunes.example.com")
|
|
209
|
-
>>> token = client.login(
|
|
245
|
+
>>> token = client.login(
|
|
246
|
+
... "user@example.com", "password123", namespace="public"
|
|
247
|
+
... )
|
|
210
248
|
>>> client.set_token(token.access_token)
|
|
211
249
|
"""
|
|
212
|
-
credentials = LoginCredentials(
|
|
250
|
+
credentials = LoginCredentials(
|
|
251
|
+
identity=username, password=password, namespace=namespace
|
|
252
|
+
)
|
|
213
253
|
response = self._make_request(
|
|
214
254
|
"POST",
|
|
215
255
|
"/api/login",
|
|
@@ -228,7 +268,12 @@ class KeyrunesClient:
|
|
|
228
268
|
return token
|
|
229
269
|
|
|
230
270
|
def register_user(
|
|
231
|
-
self,
|
|
271
|
+
self,
|
|
272
|
+
username: str,
|
|
273
|
+
email: str,
|
|
274
|
+
password: str,
|
|
275
|
+
namespace: str = "public",
|
|
276
|
+
**attributes: Any,
|
|
232
277
|
) -> User:
|
|
233
278
|
"""
|
|
234
279
|
Register a new user.
|
|
@@ -237,6 +282,7 @@ class KeyrunesClient:
|
|
|
237
282
|
username: Username (3-50 characters)
|
|
238
283
|
email: User email address
|
|
239
284
|
password: Password (minimum 8 characters)
|
|
285
|
+
namespace: User namespace (default: "public")
|
|
240
286
|
**attributes: Additional user attributes
|
|
241
287
|
|
|
242
288
|
Returns:
|
|
@@ -251,6 +297,7 @@ class KeyrunesClient:
|
|
|
251
297
|
... username="newuser",
|
|
252
298
|
... email="newuser@example.com",
|
|
253
299
|
... password="securepass123",
|
|
300
|
+
... namespace="my-app",
|
|
254
301
|
... department="Engineering"
|
|
255
302
|
... )
|
|
256
303
|
"""
|
|
@@ -258,6 +305,7 @@ class KeyrunesClient:
|
|
|
258
305
|
username=username,
|
|
259
306
|
email=email,
|
|
260
307
|
password=password,
|
|
308
|
+
namespace=namespace,
|
|
261
309
|
attributes=attributes,
|
|
262
310
|
)
|
|
263
311
|
response = self._make_request(
|
|
@@ -283,6 +331,7 @@ class KeyrunesClient:
|
|
|
283
331
|
email: str,
|
|
284
332
|
password: str,
|
|
285
333
|
admin_key: str,
|
|
334
|
+
namespace: str = "public",
|
|
286
335
|
**attributes: Any,
|
|
287
336
|
) -> User:
|
|
288
337
|
"""
|
|
@@ -293,6 +342,7 @@ class KeyrunesClient:
|
|
|
293
342
|
email: Admin email address
|
|
294
343
|
password: Password (minimum 8 characters)
|
|
295
344
|
admin_key: Admin registration key
|
|
345
|
+
namespace: User namespace (default: "public")
|
|
296
346
|
**attributes: Additional user attributes
|
|
297
347
|
|
|
298
348
|
Returns:
|
|
@@ -316,6 +366,7 @@ class KeyrunesClient:
|
|
|
316
366
|
email=email,
|
|
317
367
|
password=password,
|
|
318
368
|
admin_key=admin_key,
|
|
369
|
+
namespace=namespace,
|
|
319
370
|
attributes=attributes,
|
|
320
371
|
)
|
|
321
372
|
response = self._make_request(
|
|
@@ -378,13 +429,8 @@ class KeyrunesClient:
|
|
|
378
429
|
check = GroupCheck(**response)
|
|
379
430
|
return check.has_access
|
|
380
431
|
except UserNotFoundError:
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
self._token_data.get("groups", [])
|
|
384
|
-
if self._token_data
|
|
385
|
-
else []
|
|
386
|
-
)
|
|
387
|
-
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.
|
|
388
434
|
raise GroupNotFoundError(
|
|
389
435
|
f"Group '{group_id}' not found or user not in group"
|
|
390
436
|
)
|
|
@@ -417,33 +463,12 @@ class KeyrunesClient:
|
|
|
417
463
|
)
|
|
418
464
|
|
|
419
465
|
if token_user_id and str(user_id) == token_user_id and self._token_data:
|
|
420
|
-
|
|
421
|
-
user_data = {
|
|
422
|
-
"id": str(token_data.get("sub", "")),
|
|
423
|
-
"username": token_data.get("username", ""),
|
|
424
|
-
"email": token_data.get("email", ""),
|
|
425
|
-
"groups": token_data.get("groups", []),
|
|
426
|
-
}
|
|
427
|
-
return self._normalize_user(user_data)
|
|
466
|
+
return self._user_from_token_claims()
|
|
428
467
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (
|
|
434
|
-
token_user_id
|
|
435
|
-
and str(user_id) == token_user_id
|
|
436
|
-
and self._token_data
|
|
437
|
-
):
|
|
438
|
-
token_data = self._token_data
|
|
439
|
-
user_data = {
|
|
440
|
-
"id": str(token_data.get("sub", "")),
|
|
441
|
-
"username": token_data.get("username", ""),
|
|
442
|
-
"email": token_data.get("email", ""),
|
|
443
|
-
"groups": token_data.get("groups", []),
|
|
444
|
-
}
|
|
445
|
-
return self._normalize_user(user_data)
|
|
446
|
-
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)
|
|
447
472
|
|
|
448
473
|
def get_current_user(self) -> User:
|
|
449
474
|
"""
|
|
@@ -465,29 +490,12 @@ class KeyrunesClient:
|
|
|
465
490
|
raise AuthenticationError("Not authenticated. Please login first.")
|
|
466
491
|
|
|
467
492
|
if self._token_data:
|
|
468
|
-
|
|
469
|
-
user_data = {
|
|
470
|
-
"id": str(token_data.get("sub", "")),
|
|
471
|
-
"username": token_data.get("username", ""),
|
|
472
|
-
"email": token_data.get("email", ""),
|
|
473
|
-
"groups": token_data.get("groups", []),
|
|
474
|
-
}
|
|
475
|
-
return self._normalize_user(user_data)
|
|
493
|
+
return self._user_from_token_claims()
|
|
476
494
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if self._token_data:
|
|
482
|
-
token_data = self._token_data
|
|
483
|
-
user_data = {
|
|
484
|
-
"id": str(token_data.get("sub", "")),
|
|
485
|
-
"username": token_data.get("username", ""),
|
|
486
|
-
"email": token_data.get("email", ""),
|
|
487
|
-
"groups": token_data.get("groups", []),
|
|
488
|
-
}
|
|
489
|
-
return self._normalize_user(user_data)
|
|
490
|
-
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)
|
|
491
499
|
|
|
492
500
|
def get_user_groups(self, user_id: Optional[str] = None) -> List[str]:
|
|
493
501
|
"""
|
|
@@ -69,6 +69,7 @@ class _GlobalConfig:
|
|
|
69
69
|
self,
|
|
70
70
|
base_url: str,
|
|
71
71
|
api_key: Optional[str] = None,
|
|
72
|
+
organization_key: Optional[str] = None,
|
|
72
73
|
timeout: int = 30,
|
|
73
74
|
) -> KeyrunesClient:
|
|
74
75
|
"""
|
|
@@ -77,6 +78,7 @@ class _GlobalConfig:
|
|
|
77
78
|
Args:
|
|
78
79
|
base_url: Base URL of the Keyrunes API
|
|
79
80
|
api_key: Optional API key for authentication
|
|
81
|
+
organization_key: Optional Organization Key (required for v0.2.0+)
|
|
80
82
|
timeout: Request timeout in seconds (default: 30)
|
|
81
83
|
|
|
82
84
|
Returns:
|
|
@@ -91,7 +93,10 @@ class _GlobalConfig:
|
|
|
91
93
|
... )
|
|
92
94
|
"""
|
|
93
95
|
client = KeyrunesClient(
|
|
94
|
-
base_url=base_url,
|
|
96
|
+
base_url=base_url,
|
|
97
|
+
api_key=api_key,
|
|
98
|
+
organization_key=organization_key,
|
|
99
|
+
timeout=timeout,
|
|
95
100
|
)
|
|
96
101
|
self.set_client(client)
|
|
97
102
|
return client
|
|
@@ -133,6 +138,7 @@ def get_config() -> _GlobalConfig:
|
|
|
133
138
|
def configure(
|
|
134
139
|
base_url: str,
|
|
135
140
|
api_key: Optional[str] = None,
|
|
141
|
+
organization_key: Optional[str] = None,
|
|
136
142
|
timeout: int = 30,
|
|
137
143
|
) -> KeyrunesClient:
|
|
138
144
|
"""
|
|
@@ -145,6 +151,7 @@ def configure(
|
|
|
145
151
|
Args:
|
|
146
152
|
base_url: Base URL of the Keyrunes API
|
|
147
153
|
api_key: Optional API key for authentication
|
|
154
|
+
organization_key: Optional Organization Key (required for v0.2.0+)
|
|
148
155
|
timeout: Request timeout in seconds (default: 30)
|
|
149
156
|
|
|
150
157
|
Returns:
|
|
@@ -167,6 +174,7 @@ def configure(
|
|
|
167
174
|
return _config.configure(
|
|
168
175
|
base_url=base_url,
|
|
169
176
|
api_key=api_key,
|
|
177
|
+
organization_key=organization_key,
|
|
170
178
|
timeout=timeout,
|
|
171
179
|
)
|
|
172
180
|
|
|
@@ -64,6 +64,7 @@ class UserRegistration(BaseModel):
|
|
|
64
64
|
)
|
|
65
65
|
email: EmailStr = Field(..., description="User email")
|
|
66
66
|
password: str = Field(..., min_length=8, description="Password")
|
|
67
|
+
namespace: str = Field("default", description="User namespace")
|
|
67
68
|
attributes: Dict[str, Any] = Field(
|
|
68
69
|
default_factory=dict, description="Additional attributes"
|
|
69
70
|
)
|
|
@@ -80,11 +81,14 @@ class LoginCredentials(BaseModel):
|
|
|
80
81
|
|
|
81
82
|
identity: str = Field(..., description="Username or email")
|
|
82
83
|
password: str = Field(..., description="Password")
|
|
84
|
+
namespace: str = Field("default", description="User namespace")
|
|
83
85
|
|
|
84
86
|
@classmethod
|
|
85
|
-
def from_username(
|
|
87
|
+
def from_username(
|
|
88
|
+
cls, username: str, password: str, namespace: str = "default"
|
|
89
|
+
) -> "LoginCredentials":
|
|
86
90
|
"""Create from username parameter (for backward compatibility)."""
|
|
87
|
-
return cls(identity=username, password=password)
|
|
91
|
+
return cls(identity=username, password=password, namespace=namespace)
|
|
88
92
|
|
|
89
93
|
|
|
90
94
|
class GroupCheck(BaseModel):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "keyrunes-python-sdk"
|
|
3
|
-
version = "0.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"
|
|
@@ -40,12 +41,15 @@ pytest-cov = "^4.1.0"
|
|
|
40
41
|
pytest-mock = "^3.11.1"
|
|
41
42
|
factory-boy = "^3.3.0"
|
|
42
43
|
faker = "^19.0.0"
|
|
43
|
-
black = "^
|
|
44
|
+
black = "^25.12.0"
|
|
44
45
|
isort = "^5.12.0"
|
|
45
46
|
flake8 = "^6.1.0"
|
|
46
47
|
mypy = "^1.5.0"
|
|
47
48
|
taskipy = "^1.12.0"
|
|
48
49
|
towncrier = "^25.8.0"
|
|
50
|
+
pre-commit = "^4.5.1"
|
|
51
|
+
safety = "^3.7.0"
|
|
52
|
+
hypothesis = "^6.0"
|
|
49
53
|
|
|
50
54
|
[build-system]
|
|
51
55
|
requires = ["poetry-core>=1.0.0"]
|
|
@@ -64,6 +68,17 @@ addopts = [
|
|
|
64
68
|
"-v",
|
|
65
69
|
]
|
|
66
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
|
+
|
|
67
82
|
[tool.black]
|
|
68
83
|
line-length = 80
|
|
69
84
|
target-version = ['py312']
|
|
File without changes
|
|
File without changes
|
|
File without changes
|