keyrunes-python-sdk 0.0.1__py3-none-any.whl

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_sdk/config.py ADDED
@@ -0,0 +1,203 @@
1
+ """Global configuration and client management for Keyrunes SDK."""
2
+
3
+ from threading import Lock
4
+ from typing import Optional
5
+
6
+ from keyrunes_sdk.client import KeyrunesClient
7
+
8
+
9
+ class _GlobalConfig:
10
+ """
11
+ Thread-safe global configuration for Keyrunes SDK.
12
+
13
+ This class manages a global KeyrunesClient instance that can be configured
14
+ once and used throughout the application without passing it explicitly
15
+ to decorators and functions.
16
+
17
+ Example:
18
+ >>> from keyrunes_sdk import configure, require_group
19
+ >>>
20
+ >>> # Configure once at application startup
21
+ >>> configure(base_url="https://keyrunes.example.com")
22
+ >>>
23
+ >>> # Use decorators without passing client
24
+ >>> @require_group("admins")
25
+ ... def delete_user(user_id: str):
26
+ ... pass
27
+ """
28
+
29
+ def __init__(self) -> None:
30
+ """Initialize global configuration."""
31
+ self._client: Optional[KeyrunesClient] = None
32
+ self._lock = Lock()
33
+
34
+ def set_client(self, client: KeyrunesClient) -> None:
35
+ """
36
+ Set the global client instance.
37
+
38
+ Args:
39
+ client: KeyrunesClient instance to use globally
40
+
41
+ Example:
42
+ >>> from keyrunes_sdk import get_config
43
+ >>> from keyrunes_sdk.client import KeyrunesClient
44
+ >>>
45
+ >>> client = KeyrunesClient("https://keyrunes.example.com")
46
+ >>> get_config().set_client(client)
47
+ """
48
+ with self._lock:
49
+ self._client = client
50
+
51
+ def get_client(self) -> Optional[KeyrunesClient]:
52
+ """
53
+ Get the global client instance.
54
+
55
+ Returns:
56
+ Global KeyrunesClient instance or None if not configured
57
+
58
+ Example:
59
+ >>> from keyrunes_sdk import get_config
60
+ >>>
61
+ >>> client = get_config().get_client()
62
+ >>> if client:
63
+ ... user = client.get_current_user()
64
+ """
65
+ with self._lock:
66
+ return self._client
67
+
68
+ def configure(
69
+ self,
70
+ base_url: str,
71
+ api_key: Optional[str] = None,
72
+ timeout: int = 30,
73
+ ) -> KeyrunesClient:
74
+ """
75
+ Configure and create global client instance.
76
+
77
+ Args:
78
+ base_url: Base URL of the Keyrunes API
79
+ api_key: Optional API key for authentication
80
+ timeout: Request timeout in seconds (default: 30)
81
+
82
+ Returns:
83
+ Configured KeyrunesClient instance
84
+
85
+ Example:
86
+ >>> from keyrunes_sdk import get_config
87
+ >>>
88
+ >>> client = get_config().configure(
89
+ ... base_url="https://keyrunes.example.com",
90
+ ... api_key="my-api-key"
91
+ ... )
92
+ """
93
+ client = KeyrunesClient(
94
+ base_url=base_url, api_key=api_key, timeout=timeout
95
+ )
96
+ self.set_client(client)
97
+ return client
98
+
99
+ def clear(self) -> None:
100
+ """
101
+ Clear the global client instance.
102
+
103
+ Example:
104
+ >>> from keyrunes_sdk import get_config
105
+ >>>
106
+ >>> get_config().clear()
107
+ """
108
+ with self._lock:
109
+ if self._client:
110
+ self._client.close()
111
+ self._client = None
112
+
113
+
114
+ _config = _GlobalConfig()
115
+
116
+
117
+ def get_config() -> _GlobalConfig:
118
+ """
119
+ Get the global configuration instance.
120
+
121
+ Returns:
122
+ Global configuration instance
123
+
124
+ Example:
125
+ >>> from keyrunes_sdk import get_config
126
+ >>>
127
+ >>> config = get_config()
128
+ >>> config.configure(base_url="https://keyrunes.example.com")
129
+ """
130
+ return _config
131
+
132
+
133
+ def configure(
134
+ base_url: str,
135
+ api_key: Optional[str] = None,
136
+ timeout: int = 30,
137
+ ) -> KeyrunesClient:
138
+ """
139
+ Configure the global Keyrunes client.
140
+
141
+ This is a convenience function that configures the global client instance.
142
+ After calling this function, decorators will automatically use this client
143
+ without needing to pass it explicitly.
144
+
145
+ Args:
146
+ base_url: Base URL of the Keyrunes API
147
+ api_key: Optional API key for authentication
148
+ timeout: Request timeout in seconds (default: 30)
149
+
150
+ Returns:
151
+ Configured KeyrunesClient instance
152
+
153
+ Example:
154
+ >>> from keyrunes_sdk import configure, require_group
155
+ >>>
156
+ >>> # Configure once at app startup
157
+ >>> client = configure("https://keyrunes.example.com")
158
+ >>> client.login("user@example.com", "password")
159
+ >>>
160
+ >>> # Now decorators work without passing client
161
+ >>> @require_group("admins")
162
+ ... def admin_only_function(user_id: str):
163
+ ... print(f"Admin function for {user_id}")
164
+ >>>
165
+ >>> admin_only_function(user_id="user123")
166
+ """
167
+ return _config.configure(
168
+ base_url=base_url,
169
+ api_key=api_key,
170
+ timeout=timeout,
171
+ )
172
+
173
+
174
+ def get_global_client() -> Optional[KeyrunesClient]:
175
+ """
176
+ Get the global client instance.
177
+
178
+ Returns:
179
+ Global KeyrunesClient instance or None if not configured
180
+
181
+ Raises:
182
+ RuntimeError: If client is not configured
183
+
184
+ Example:
185
+ >>> from keyrunes_sdk import configure, get_global_client
186
+ >>>
187
+ >>> configure("https://keyrunes.example.com")
188
+ >>> client = get_global_client()
189
+ >>> user = client.get_current_user()
190
+ """
191
+ return _config.get_client()
192
+
193
+
194
+ def clear_global_client() -> None:
195
+ """
196
+ Clear the global client instance.
197
+
198
+ Example:
199
+ >>> from keyrunes_sdk import clear_global_client
200
+ >>>
201
+ >>> clear_global_client()
202
+ """
203
+ _config.clear()
@@ -0,0 +1,248 @@
1
+ """Decorators for Keyrunes authorization."""
2
+
3
+ from functools import wraps
4
+ from typing import Any, Callable, Optional
5
+
6
+ from keyrunes_sdk.client import KeyrunesClient
7
+ from keyrunes_sdk.exceptions import AuthorizationError, UserNotFoundError
8
+
9
+
10
+ def _get_client(
11
+ client: Optional[KeyrunesClient], kwargs: dict
12
+ ) -> KeyrunesClient:
13
+ """
14
+ Get client from decorator, kwargs, or global config.
15
+
16
+ Args:
17
+ client: Client passed to decorator
18
+ kwargs: Function kwargs that might contain 'client'
19
+
20
+ Returns:
21
+ KeyrunesClient instance
22
+
23
+ Raises:
24
+ ValueError: If no client is available
25
+ """
26
+ if client:
27
+ return client
28
+
29
+ if "client" in kwargs:
30
+ client_from_kwargs = kwargs["client"]
31
+ if isinstance(client_from_kwargs, KeyrunesClient):
32
+ return client_from_kwargs
33
+
34
+ from keyrunes_sdk.config import get_global_client
35
+
36
+ global_client = get_global_client()
37
+ if global_client is not None:
38
+ return global_client
39
+
40
+ raise ValueError(
41
+ "KeyrunesClient not provided. Either:\n"
42
+ "1. Pass 'client' parameter to decorator: "
43
+ "@require_group('admins', client=client)\n"
44
+ "2. Pass 'client' in function kwargs: "
45
+ "func(user_id='123', client=client)\n"
46
+ "3. Configure global client: "
47
+ "configure('https://keyrunes.example.com')"
48
+ )
49
+
50
+
51
+ def require_group(
52
+ *group_ids: str,
53
+ client: Optional[KeyrunesClient] = None,
54
+ user_id_param: str = "user_id",
55
+ all_groups: bool = False,
56
+ ) -> Callable:
57
+ """
58
+ Decorator to require user membership in one or more groups.
59
+
60
+ This decorator checks if a user belongs to the specified group(s) before
61
+ executing the decorated function. If the user doesn't have the required
62
+ group membership, an AuthorizationError is raised.
63
+
64
+ Args:
65
+ *group_ids: One or more group IDs to check
66
+ client: KeyrunesClient instance
67
+ (if None, expects 'client' in kwargs)
68
+ user_id_param: Name of the parameter containing user_id
69
+ (default: 'user_id')
70
+ all_groups: If True, user must belong to ALL groups;
71
+ if False, ANY group (default: False)
72
+
73
+ Returns:
74
+ Decorated function
75
+
76
+ Raises:
77
+ AuthorizationError: If user doesn't have required group membership
78
+ ValueError: If client is not provided
79
+
80
+ Example:
81
+ >>> @require_group("admins")
82
+ ... def delete_user(user_id: str, client: KeyrunesClient):
83
+ ... # This function only executes if user is in 'admins' group
84
+ ... pass
85
+
86
+ >>> @require_group("admins", "moderators", all_groups=False)
87
+ ... def moderate_content(user_id: str, client: KeyrunesClient):
88
+ ... # User needs to be in 'admins' OR 'moderators'
89
+ ... pass
90
+
91
+ >>> @require_group("admins", "verified", all_groups=True)
92
+ ... def sensitive_operation(user_id: str, client: KeyrunesClient):
93
+ ... # User needs to be in BOTH 'admins' AND 'verified'
94
+ ... pass
95
+
96
+ >>> # Using with a custom client
97
+ >>> my_client = KeyrunesClient("https://keyrunes.example.com")
98
+ >>> my_client.login("admin@example.com", "password")
99
+ >>>
100
+ >>> @require_group("admins", client=my_client)
101
+ ... def admin_only_function(user_id: str):
102
+ ... print(f"Admin function for user {user_id}")
103
+ >>>
104
+ >>> admin_only_function(user_id="user123")
105
+ """
106
+
107
+ def decorator(func: Callable) -> Callable:
108
+ @wraps(func)
109
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
110
+ keyrunes_client = _get_client(client, kwargs)
111
+
112
+ user_id = kwargs.get(user_id_param)
113
+
114
+ if not user_id:
115
+ import inspect
116
+
117
+ sig = inspect.signature(func)
118
+ param_names = list(sig.parameters.keys())
119
+
120
+ if user_id_param in param_names:
121
+ param_index = param_names.index(user_id_param)
122
+ if param_index < len(args):
123
+ user_id = args[param_index]
124
+
125
+ if not user_id:
126
+ raise ValueError(
127
+ f"User ID not found. Expected parameter "
128
+ f"'{user_id_param}' in function arguments."
129
+ )
130
+
131
+ if all_groups:
132
+ for group_id in group_ids:
133
+ if not keyrunes_client.has_group(user_id, group_id):
134
+ raise AuthorizationError(
135
+ f"User '{user_id}' does not have required "
136
+ f"group '{group_id}'. All groups required: "
137
+ f"{group_ids}"
138
+ )
139
+ else:
140
+ has_any_group = False
141
+ for group_id in group_ids:
142
+ if keyrunes_client.has_group(user_id, group_id):
143
+ has_any_group = True
144
+ break
145
+
146
+ if not has_any_group:
147
+ raise AuthorizationError(
148
+ f"User '{user_id}' does not belong to any of "
149
+ f"the required groups: {group_ids}"
150
+ )
151
+
152
+ return func(*args, **kwargs)
153
+
154
+ return wrapper
155
+
156
+ return decorator
157
+
158
+
159
+ def require_admin(
160
+ client: Optional[KeyrunesClient] = None,
161
+ user_id_param: str = "user_id",
162
+ ) -> Callable:
163
+ """
164
+ Decorator to require admin privileges.
165
+
166
+ Convenience decorator that checks if user has admin flag set.
167
+
168
+ Args:
169
+ client: KeyrunesClient instance
170
+ (if None, expects 'client' in kwargs)
171
+ user_id_param: Name of the parameter containing user_id
172
+ (default: 'user_id')
173
+
174
+ Returns:
175
+ Decorated function
176
+
177
+ Raises:
178
+ AuthorizationError: If user doesn't have admin privileges
179
+
180
+ Example:
181
+ >>> @require_admin()
182
+ ... def system_config(user_id: str, client: KeyrunesClient):
183
+ ... # Only admins can execute this
184
+ ... pass
185
+ """
186
+
187
+ def decorator(func: Callable) -> Callable:
188
+ @wraps(func)
189
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
190
+ keyrunes_client = _get_client(client, kwargs)
191
+
192
+ user_id = kwargs.get(user_id_param)
193
+ if not user_id:
194
+ import inspect
195
+
196
+ sig = inspect.signature(func)
197
+ param_names = list(sig.parameters.keys())
198
+
199
+ if user_id_param in param_names:
200
+ param_index = param_names.index(user_id_param)
201
+ if param_index < len(args):
202
+ user_id = args[param_index]
203
+
204
+ if not user_id:
205
+ raise ValueError(
206
+ f"User ID not found in parameter '{user_id_param}'"
207
+ )
208
+
209
+ token_user_id = (
210
+ str(keyrunes_client._token_data.get("sub", ""))
211
+ if keyrunes_client._token_data
212
+ else None
213
+ )
214
+
215
+ if token_user_id and str(user_id) == token_user_id:
216
+ groups = (
217
+ keyrunes_client._token_data.get("groups", [])
218
+ if keyrunes_client._token_data
219
+ else []
220
+ )
221
+ is_admin = (
222
+ "admins" in groups
223
+ or "superadmin" in groups
224
+ or any("admin" in str(g).lower() for g in groups)
225
+ )
226
+ if not is_admin:
227
+ raise AuthorizationError(
228
+ f"User '{user_id}' does not have admin privileges."
229
+ )
230
+ else:
231
+ try:
232
+ user = keyrunes_client.get_user(user_id)
233
+ if not user.is_admin:
234
+ raise AuthorizationError(
235
+ f"User '{user_id}' does not have admin "
236
+ f"privileges."
237
+ )
238
+ except UserNotFoundError:
239
+ raise AuthorizationError(
240
+ f"User '{user_id}' not found or does not have "
241
+ f"admin privileges."
242
+ )
243
+
244
+ return func(*args, **kwargs)
245
+
246
+ return wrapper
247
+
248
+ return decorator
@@ -0,0 +1,43 @@
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
keyrunes_sdk/models.py ADDED
@@ -0,0 +1,100 @@
1
+ """Data models for Keyrunes SDK."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from pydantic import BaseModel, EmailStr, Field
7
+
8
+
9
+ class User(BaseModel):
10
+ """User model."""
11
+
12
+ id: str = Field(..., description="User ID")
13
+ username: str = Field(..., description="Username")
14
+ email: EmailStr = Field(..., description="User email")
15
+ groups: List[str] = Field(
16
+ default_factory=list, description="List of group IDs"
17
+ )
18
+ attributes: Dict[str, Any] = Field(
19
+ default_factory=dict, description="User attributes"
20
+ )
21
+ created_at: Optional[datetime] = Field(
22
+ None, description="Creation timestamp"
23
+ )
24
+ updated_at: Optional[datetime] = Field(
25
+ None, description="Last update timestamp"
26
+ )
27
+ is_active: bool = Field(True, description="Whether user is active")
28
+ is_admin: bool = Field(
29
+ False, description="Whether user has admin privileges"
30
+ )
31
+
32
+
33
+ class Group(BaseModel):
34
+ """Group model."""
35
+
36
+ id: str = Field(..., description="Group ID")
37
+ name: str = Field(..., description="Group name")
38
+ description: Optional[str] = Field(None, description="Group description")
39
+ permissions: List[str] = Field(
40
+ default_factory=list, description="List of permissions"
41
+ )
42
+ created_at: Optional[datetime] = Field(
43
+ None, description="Creation timestamp"
44
+ )
45
+
46
+
47
+ class Token(BaseModel):
48
+ """Authentication token model."""
49
+
50
+ access_token: str = Field(..., description="JWT access token")
51
+ token_type: str = Field("bearer", description="Token type")
52
+ expires_in: Optional[int] = Field(
53
+ None, description="Token expiration in seconds"
54
+ )
55
+ refresh_token: Optional[str] = Field(None, description="Refresh token")
56
+ user: Optional[User] = Field(None, description="User information")
57
+
58
+
59
+ class UserRegistration(BaseModel):
60
+ """User registration data."""
61
+
62
+ username: str = Field(
63
+ ..., min_length=3, max_length=50, description="Username"
64
+ )
65
+ email: EmailStr = Field(..., description="User email")
66
+ password: str = Field(..., min_length=8, description="Password")
67
+ attributes: Dict[str, Any] = Field(
68
+ default_factory=dict, description="Additional attributes"
69
+ )
70
+
71
+
72
+ class AdminRegistration(UserRegistration):
73
+ """Admin registration data (extends UserRegistration)."""
74
+
75
+ admin_key: str = Field(..., description="Admin registration key")
76
+
77
+
78
+ class LoginCredentials(BaseModel):
79
+ """Login credentials."""
80
+
81
+ identity: str = Field(..., description="Username or email")
82
+ password: str = Field(..., description="Password")
83
+
84
+ @classmethod
85
+ def from_username(cls, username: str, password: str) -> "LoginCredentials":
86
+ """Create from username parameter (for backward compatibility)."""
87
+ return cls(identity=username, password=password)
88
+
89
+
90
+ class GroupCheck(BaseModel):
91
+ """Group membership check result."""
92
+
93
+ user_id: str = Field(..., description="User ID")
94
+ group_id: str = Field(..., description="Group ID")
95
+ has_access: bool = Field(
96
+ ..., description="Whether user has access to group"
97
+ )
98
+ checked_at: datetime = Field(
99
+ default_factory=datetime.utcnow, description="Check timestamp"
100
+ )