keyrunes-python-sdk 0.2.0__tar.gz → 0.3.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.2.0 → keyrunes_python_sdk-0.3.0}/CHANGELOG.md +55 -0
- {keyrunes_python_sdk-0.2.0 → keyrunes_python_sdk-0.3.0}/PKG-INFO +1 -1
- {keyrunes_python_sdk-0.2.0 → keyrunes_python_sdk-0.3.0}/keyrunes_sdk/__init__.py +1 -1
- {keyrunes_python_sdk-0.2.0 → keyrunes_python_sdk-0.3.0}/keyrunes_sdk/client.py +112 -27
- keyrunes_python_sdk-0.3.0/keyrunes_sdk/exceptions.py +62 -0
- {keyrunes_python_sdk-0.2.0 → keyrunes_python_sdk-0.3.0}/keyrunes_sdk/models.py +21 -0
- keyrunes_python_sdk-0.3.0/keyrunes_sdk/py.typed +0 -0
- {keyrunes_python_sdk-0.2.0 → keyrunes_python_sdk-0.3.0}/pyproject.toml +4 -2
- keyrunes_python_sdk-0.2.0/keyrunes_sdk/exceptions.py +0 -43
- {keyrunes_python_sdk-0.2.0 → keyrunes_python_sdk-0.3.0}/LICENSE +0 -0
- {keyrunes_python_sdk-0.2.0 → keyrunes_python_sdk-0.3.0}/README.md +0 -0
- {keyrunes_python_sdk-0.2.0 → keyrunes_python_sdk-0.3.0}/keyrunes_sdk/config.py +0 -0
- {keyrunes_python_sdk-0.2.0 → keyrunes_python_sdk-0.3.0}/keyrunes_sdk/decorators.py +0 -0
|
@@ -5,6 +5,61 @@ 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.3.0] - 2026-09-03
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `User.user_id`, carrying the internal identifier Keyrunes puts in the JWT
|
|
13
|
+
`sub` claim. It is a different value from `User.id` (the external UUID), and
|
|
14
|
+
a consumer that keys its own records off `sub` needs it; before this release
|
|
15
|
+
it was discarded during normalization and could only be recovered by
|
|
16
|
+
re-parsing the token.
|
|
17
|
+
- `User.namespace`, `User.organization_id` and `User.first_login`, all of which
|
|
18
|
+
the server returns and normalization used to drop.
|
|
19
|
+
- `Token.requires_password_change`, so a login that must be followed by a
|
|
20
|
+
password change can be detected without reading the raw response.
|
|
21
|
+
- `KeyrunesClient.refresh_token()`, wrapping `POST /api/refresh-token`. It
|
|
22
|
+
accepts an explicit token or falls back to the client's current one, raises
|
|
23
|
+
`InvalidTokenError` when neither is available, and adopts the refreshed
|
|
24
|
+
token the way `login()` does.
|
|
25
|
+
- `get_current_user(force_refresh=True)`, which always asks the server. The
|
|
26
|
+
claims shortcut reads a token the SDK never verified, so it cannot answer
|
|
27
|
+
"is this token still accepted"; a caller validating a token needs the round
|
|
28
|
+
trip.
|
|
29
|
+
- `KeyrunesError.status_code`, carrying the HTTP status when the error came
|
|
30
|
+
from a response rather than from the transport. Without it a caller cannot
|
|
31
|
+
tell "the server refused this request" (4xx) from "the server or the network
|
|
32
|
+
failed" (5xx, or no response at all), and both are raised as `NetworkError`.
|
|
33
|
+
- `register_user(group=...)`, sent as a top-level field.
|
|
34
|
+
- A `py.typed` marker (PEP 561). The package was already fully typed, but
|
|
35
|
+
without the marker a consumer running mypy saw every SDK import as
|
|
36
|
+
untyped.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- `get_current_user()` asked for `/api/users/me`, which the Keyrunes router
|
|
41
|
+
does not expose — the real route is `/api/me`, so the call answered 404
|
|
42
|
+
against a live server whenever the claims shortcut did not cover it. The
|
|
43
|
+
endpoint is now a named constant, `ENDPOINT_ME`.
|
|
44
|
+
- `register_user()` and `register_admin()` required the response to be
|
|
45
|
+
`{"user": {...}}`, but `POST /api/register` answers with the bare user
|
|
46
|
+
object. Every registration against a real server failed with "Unexpected
|
|
47
|
+
response format". Both shapes are now accepted.
|
|
48
|
+
- Extra keyword arguments to `register_user()` were nested under `attributes`,
|
|
49
|
+
so a `group` never reached the server, which reads it as a top-level field.
|
|
50
|
+
`group` is now an explicit parameter placed at the top level; other keyword
|
|
51
|
+
attributes keep nesting under `attributes`.
|
|
52
|
+
|
|
53
|
+
### Testing
|
|
54
|
+
|
|
55
|
+
- Test count raised from 159 to 189.
|
|
56
|
+
|
|
57
|
+
### Notes
|
|
58
|
+
|
|
59
|
+
- Every added field is optional with a backwards-compatible default, and
|
|
60
|
+
`User.id` keeps its existing precedence (`id` → `user_id` → `external_id`).
|
|
61
|
+
Code written against 0.2.0 keeps working unchanged.
|
|
62
|
+
|
|
8
63
|
## [0.2.0] - 2026-09-03
|
|
9
64
|
|
|
10
65
|
### Added
|
|
@@ -11,6 +11,7 @@ from keyrunes_sdk.exceptions import (
|
|
|
11
11
|
AuthenticationError,
|
|
12
12
|
AuthorizationError,
|
|
13
13
|
GroupNotFoundError,
|
|
14
|
+
InvalidTokenError,
|
|
14
15
|
NetworkError,
|
|
15
16
|
UserNotFoundError,
|
|
16
17
|
)
|
|
@@ -23,6 +24,17 @@ from keyrunes_sdk.models import (
|
|
|
23
24
|
UserRegistration,
|
|
24
25
|
)
|
|
25
26
|
|
|
27
|
+
#: Where the server answers "who is this token". The Keyrunes router exposes
|
|
28
|
+
#: this as ``/api/me``; there is no ``/api/users/me``.
|
|
29
|
+
#: Creates a user; answers with the bare user object.
|
|
30
|
+
ENDPOINT_REGISTER = "/api/register"
|
|
31
|
+
|
|
32
|
+
#: Where the server answers "who is this token".
|
|
33
|
+
ENDPOINT_ME = "/api/me"
|
|
34
|
+
|
|
35
|
+
#: Exchanges a still-valid token for a fresh one.
|
|
36
|
+
ENDPOINT_REFRESH_TOKEN = "/api/refresh-token"
|
|
37
|
+
|
|
26
38
|
|
|
27
39
|
class KeyrunesClient:
|
|
28
40
|
"""
|
|
@@ -135,7 +147,10 @@ class KeyrunesClient:
|
|
|
135
147
|
error_msg = (
|
|
136
148
|
response.text or f"HTTP {response.status_code} error"
|
|
137
149
|
)
|
|
138
|
-
raise NetworkError(
|
|
150
|
+
raise NetworkError(
|
|
151
|
+
f"Request failed: {error_msg}",
|
|
152
|
+
status_code=response.status_code,
|
|
153
|
+
)
|
|
139
154
|
|
|
140
155
|
try:
|
|
141
156
|
result: Dict[str, Any] = response.json()
|
|
@@ -170,6 +185,15 @@ class KeyrunesClient:
|
|
|
170
185
|
normalized["is_admin"] = (
|
|
171
186
|
is_admin_flag or has_admin_group or has_admin_in_name
|
|
172
187
|
)
|
|
188
|
+
# Carried through untouched so a caller can key its own records off the
|
|
189
|
+
# same identifier the JWT ``sub`` uses, rather than off ``id``.
|
|
190
|
+
raw_user_id = data.get("user_id")
|
|
191
|
+
normalized["user_id"] = (
|
|
192
|
+
str(raw_user_id) if raw_user_id is not None else None
|
|
193
|
+
)
|
|
194
|
+
normalized["namespace"] = data.get("namespace")
|
|
195
|
+
normalized["organization_id"] = data.get("organization_id")
|
|
196
|
+
normalized["first_login"] = bool(data.get("first_login", False))
|
|
173
197
|
return User(**normalized)
|
|
174
198
|
|
|
175
199
|
def _user_from_token_claims(self) -> User:
|
|
@@ -221,6 +245,9 @@ class KeyrunesClient:
|
|
|
221
245
|
expires_in=payload.get("expires_in"),
|
|
222
246
|
refresh_token=payload.get("refresh_token"),
|
|
223
247
|
user=user_model,
|
|
248
|
+
requires_password_change=bool(
|
|
249
|
+
payload.get("requires_password_change", False)
|
|
250
|
+
),
|
|
224
251
|
)
|
|
225
252
|
|
|
226
253
|
def login(
|
|
@@ -267,12 +294,36 @@ class KeyrunesClient:
|
|
|
267
294
|
self._token_data = None
|
|
268
295
|
return token
|
|
269
296
|
|
|
297
|
+
@staticmethod
|
|
298
|
+
def _registration_payload(response: Any) -> Dict[str, Any]:
|
|
299
|
+
"""Pull the user object out of a registration response.
|
|
300
|
+
|
|
301
|
+
``POST /api/register`` answers with the bare user object. Some
|
|
302
|
+
deployments wrap it as ``{"user": {...}}``, so both are accepted.
|
|
303
|
+
"""
|
|
304
|
+
if not isinstance(response, dict):
|
|
305
|
+
raise NetworkError(
|
|
306
|
+
"Unexpected response format for user registration."
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
wrapped = response.get("user")
|
|
310
|
+
if isinstance(wrapped, dict) and wrapped:
|
|
311
|
+
return wrapped
|
|
312
|
+
|
|
313
|
+
# A bare user object is identified by carrying an identifier; anything
|
|
314
|
+
# else is a response shape this SDK does not understand.
|
|
315
|
+
if response.get("id") or response.get("user_id"):
|
|
316
|
+
return response
|
|
317
|
+
|
|
318
|
+
raise NetworkError("Unexpected response format for user registration.")
|
|
319
|
+
|
|
270
320
|
def register_user(
|
|
271
321
|
self,
|
|
272
322
|
username: str,
|
|
273
323
|
email: str,
|
|
274
324
|
password: str,
|
|
275
325
|
namespace: str = "public",
|
|
326
|
+
group: Optional[str] = None,
|
|
276
327
|
**attributes: Any,
|
|
277
328
|
) -> User:
|
|
278
329
|
"""
|
|
@@ -283,6 +334,8 @@ class KeyrunesClient:
|
|
|
283
334
|
email: User email address
|
|
284
335
|
password: Password (minimum 8 characters)
|
|
285
336
|
namespace: User namespace (default: "public")
|
|
337
|
+
group: Group to place the new user in. Sent as a top-level field,
|
|
338
|
+
which is where the server reads it from.
|
|
286
339
|
**attributes: Additional user attributes
|
|
287
340
|
|
|
288
341
|
Returns:
|
|
@@ -308,22 +361,18 @@ class KeyrunesClient:
|
|
|
308
361
|
namespace=namespace,
|
|
309
362
|
attributes=attributes,
|
|
310
363
|
)
|
|
364
|
+
data = registration.model_dump()
|
|
365
|
+
if group is not None:
|
|
366
|
+
data["group"] = group
|
|
367
|
+
|
|
311
368
|
response = self._make_request(
|
|
312
369
|
"POST",
|
|
313
|
-
|
|
314
|
-
data=
|
|
370
|
+
ENDPOINT_REGISTER,
|
|
371
|
+
data=data,
|
|
315
372
|
use_auth=False,
|
|
316
373
|
)
|
|
317
374
|
|
|
318
|
-
|
|
319
|
-
response.get("user") if isinstance(response, dict) else None
|
|
320
|
-
)
|
|
321
|
-
if not user_payload:
|
|
322
|
-
raise NetworkError(
|
|
323
|
-
"Unexpected response format for user registration."
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
return self._normalize_user(user_payload)
|
|
375
|
+
return self._normalize_user(self._registration_payload(response))
|
|
327
376
|
|
|
328
377
|
def register_admin(
|
|
329
378
|
self,
|
|
@@ -371,20 +420,12 @@ class KeyrunesClient:
|
|
|
371
420
|
)
|
|
372
421
|
response = self._make_request(
|
|
373
422
|
"POST",
|
|
374
|
-
|
|
423
|
+
ENDPOINT_REGISTER,
|
|
375
424
|
data=registration.model_dump(),
|
|
376
425
|
use_auth=False,
|
|
377
426
|
)
|
|
378
427
|
|
|
379
|
-
|
|
380
|
-
response.get("user") if isinstance(response, dict) else None
|
|
381
|
-
)
|
|
382
|
-
if not user_payload:
|
|
383
|
-
raise NetworkError(
|
|
384
|
-
"Unexpected response format for admin registration."
|
|
385
|
-
)
|
|
386
|
-
|
|
387
|
-
return self._normalize_user(user_payload)
|
|
428
|
+
return self._normalize_user(self._registration_payload(response))
|
|
388
429
|
|
|
389
430
|
def has_group(self, user_id: str, group_id: str) -> bool:
|
|
390
431
|
"""
|
|
@@ -470,10 +511,18 @@ class KeyrunesClient:
|
|
|
470
511
|
response = self._make_request("GET", f"/api/users/{user_id}")
|
|
471
512
|
return self._normalize_user(response)
|
|
472
513
|
|
|
473
|
-
def get_current_user(self) -> User:
|
|
514
|
+
def get_current_user(self, force_refresh: bool = False) -> User:
|
|
474
515
|
"""
|
|
475
516
|
Get currently authenticated user information.
|
|
476
517
|
|
|
518
|
+
Args:
|
|
519
|
+
force_refresh: Ask the server even when the token's own claims
|
|
520
|
+
could answer. Required whenever the answer is used to decide
|
|
521
|
+
whether the token is still good: the claims shortcut reads a
|
|
522
|
+
token the SDK never verified, so it says nothing about whether
|
|
523
|
+
the server still accepts it (revoked, expired, or signed with
|
|
524
|
+
another key).
|
|
525
|
+
|
|
477
526
|
Returns:
|
|
478
527
|
User object for authenticated user
|
|
479
528
|
|
|
@@ -489,12 +538,10 @@ class KeyrunesClient:
|
|
|
489
538
|
if not self._token:
|
|
490
539
|
raise AuthenticationError("Not authenticated. Please login first.")
|
|
491
540
|
|
|
492
|
-
if self._token_data:
|
|
541
|
+
if self._token_data and not force_refresh:
|
|
493
542
|
return self._user_from_token_claims()
|
|
494
543
|
|
|
495
|
-
|
|
496
|
-
# could answer, so a 404 here is a genuine miss and is propagated.
|
|
497
|
-
response = self._make_request("GET", "/api/users/me")
|
|
544
|
+
response = self._make_request("GET", ENDPOINT_ME)
|
|
498
545
|
return self._normalize_user(response)
|
|
499
546
|
|
|
500
547
|
def get_user_groups(self, user_id: Optional[str] = None) -> List[str]:
|
|
@@ -542,6 +589,44 @@ class KeyrunesClient:
|
|
|
542
589
|
except Exception:
|
|
543
590
|
self._token_data = None
|
|
544
591
|
|
|
592
|
+
def refresh_token(self, token: Optional[str] = None) -> Token:
|
|
593
|
+
"""
|
|
594
|
+
Exchange a still-valid token for a fresh one.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
token: Token to exchange. Defaults to the client's current token.
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
Token object carrying the new access token.
|
|
601
|
+
|
|
602
|
+
Raises:
|
|
603
|
+
InvalidTokenError: If no token is available to exchange
|
|
604
|
+
AuthenticationError: If the server rejects the token
|
|
605
|
+
NetworkError: If the request fails
|
|
606
|
+
|
|
607
|
+
Example:
|
|
608
|
+
>>> client = KeyrunesClient("https://keyrunes.example.com")
|
|
609
|
+
>>> client.set_token("eyJhbGciOiJIUzI1NiIs...")
|
|
610
|
+
>>> refreshed = client.refresh_token()
|
|
611
|
+
>>> client.set_token(refreshed.access_token)
|
|
612
|
+
"""
|
|
613
|
+
current = token or self._token
|
|
614
|
+
if not current:
|
|
615
|
+
raise InvalidTokenError("No token available to refresh.")
|
|
616
|
+
|
|
617
|
+
response = self._make_request(
|
|
618
|
+
"POST",
|
|
619
|
+
ENDPOINT_REFRESH_TOKEN,
|
|
620
|
+
data={"token": current},
|
|
621
|
+
use_auth=False,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
refreshed = self._parse_token_response(response)
|
|
625
|
+
# Refreshing is only ever useful if the client keeps using the result,
|
|
626
|
+
# so adopt it the way login() does.
|
|
627
|
+
self.set_token(refreshed.access_token)
|
|
628
|
+
return refreshed
|
|
629
|
+
|
|
545
630
|
def clear_token(self) -> None:
|
|
546
631
|
"""
|
|
547
632
|
Clear authentication token.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Custom exceptions for Keyrunes SDK."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class KeyrunesError(Exception):
|
|
7
|
+
"""Base exception for all Keyrunes SDK errors.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
message: Human-readable description.
|
|
11
|
+
status_code: HTTP status the server answered with, when the error came
|
|
12
|
+
from a response rather than from the transport. Callers use it to
|
|
13
|
+
tell "the server refused this request" (4xx, the caller's problem)
|
|
14
|
+
from "the server or the network failed" (5xx or no response at
|
|
15
|
+
all), which otherwise look identical.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self, message: str = "", status_code: Optional[int] = None
|
|
20
|
+
) -> None:
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
self.status_code = status_code
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AuthenticationError(KeyrunesError):
|
|
26
|
+
"""Raised when authentication fails."""
|
|
27
|
+
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AuthorizationError(KeyrunesError):
|
|
32
|
+
"""Raised when authorization fails."""
|
|
33
|
+
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class GroupNotFoundError(KeyrunesError):
|
|
38
|
+
"""Raised when a group is not found."""
|
|
39
|
+
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class UserNotFoundError(KeyrunesError):
|
|
44
|
+
"""Raised when a user is not found."""
|
|
45
|
+
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class InvalidTokenError(KeyrunesError):
|
|
50
|
+
"""Raised when token is invalid or expired."""
|
|
51
|
+
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class NetworkError(KeyrunesError):
|
|
56
|
+
"""Raised when a request fails.
|
|
57
|
+
|
|
58
|
+
``status_code`` is set when the server answered and the response was an
|
|
59
|
+
error; it is ``None`` when the request never got a response.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
pass
|
|
@@ -28,6 +28,23 @@ class User(BaseModel):
|
|
|
28
28
|
is_admin: bool = Field(
|
|
29
29
|
False, description="Whether user has admin privileges"
|
|
30
30
|
)
|
|
31
|
+
# Keyrunes answers with two identifiers: ``id`` is the external UUID and
|
|
32
|
+
# ``user_id`` is the internal integer the JWT carries as ``sub``. ``id``
|
|
33
|
+
# above keeps its historical precedence; this field is kept alongside it so
|
|
34
|
+
# a caller that keys its own records off ``sub`` can reach that value
|
|
35
|
+
# without re-parsing the token.
|
|
36
|
+
user_id: Optional[str] = Field(
|
|
37
|
+
None, description="Internal user identifier, as carried by the JWT sub"
|
|
38
|
+
)
|
|
39
|
+
namespace: Optional[str] = Field(
|
|
40
|
+
None, description="Namespace (tenant) the user belongs to"
|
|
41
|
+
)
|
|
42
|
+
organization_id: Optional[Any] = Field(
|
|
43
|
+
None, description="Organization the user belongs to"
|
|
44
|
+
)
|
|
45
|
+
first_login: bool = Field(
|
|
46
|
+
False, description="Whether the user has yet to complete a first login"
|
|
47
|
+
)
|
|
31
48
|
|
|
32
49
|
|
|
33
50
|
class Group(BaseModel):
|
|
@@ -54,6 +71,10 @@ class Token(BaseModel):
|
|
|
54
71
|
)
|
|
55
72
|
refresh_token: Optional[str] = Field(None, description="Refresh token")
|
|
56
73
|
user: Optional[User] = Field(None, description="User information")
|
|
74
|
+
requires_password_change: bool = Field(
|
|
75
|
+
False,
|
|
76
|
+
description="Whether the server wants the password changed before use",
|
|
77
|
+
)
|
|
57
78
|
|
|
58
79
|
|
|
59
80
|
class UserRegistration(BaseModel):
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "keyrunes-python-sdk"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Python SDK for Keyrunes Authorization System"
|
|
5
5
|
authors = ["keyrunes <contact@singularjourney.host>"]
|
|
6
6
|
maintainers = ["jonatasoli <contact@jonatasoli.dev>"]
|
|
@@ -25,7 +25,9 @@ classifiers = [
|
|
|
25
25
|
"Topic :: System :: Systems Administration :: Authentication/Directory",
|
|
26
26
|
]
|
|
27
27
|
packages = [{include = "keyrunes_sdk"}]
|
|
28
|
-
|
|
28
|
+
# `py.typed` marks the package as typed (PEP 561); without it a consumer
|
|
29
|
+
# running mypy sees every SDK import as untyped.
|
|
30
|
+
include = ["LICENSE", "README.md", "CHANGELOG.md", "keyrunes_sdk/py.typed"]
|
|
29
31
|
exclude = ["tests", "examples", "docs", "*.pyc", "__pycache__"]
|
|
30
32
|
|
|
31
33
|
[tool.poetry.dependencies]
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
"""Custom exceptions for Keyrunes SDK."""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class KeyrunesError(Exception):
|
|
5
|
-
"""Base exception for all Keyrunes SDK errors."""
|
|
6
|
-
|
|
7
|
-
pass
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class AuthenticationError(KeyrunesError):
|
|
11
|
-
"""Raised when authentication fails."""
|
|
12
|
-
|
|
13
|
-
pass
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class AuthorizationError(KeyrunesError):
|
|
17
|
-
"""Raised when authorization fails."""
|
|
18
|
-
|
|
19
|
-
pass
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class GroupNotFoundError(KeyrunesError):
|
|
23
|
-
"""Raised when a group is not found."""
|
|
24
|
-
|
|
25
|
-
pass
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class UserNotFoundError(KeyrunesError):
|
|
29
|
-
"""Raised when a user is not found."""
|
|
30
|
-
|
|
31
|
-
pass
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class InvalidTokenError(KeyrunesError):
|
|
35
|
-
"""Raised when token is invalid or expired."""
|
|
36
|
-
|
|
37
|
-
pass
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class NetworkError(KeyrunesError):
|
|
41
|
-
"""Raised when network request fails."""
|
|
42
|
-
|
|
43
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|