blackant-sdk 1.0.2__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.
Files changed (70) hide show
  1. blackant/__init__.py +31 -0
  2. blackant/auth/__init__.py +10 -0
  3. blackant/auth/blackant_auth.py +518 -0
  4. blackant/auth/keycloak_manager.py +363 -0
  5. blackant/auth/request_id.py +52 -0
  6. blackant/auth/role_assignment.py +443 -0
  7. blackant/auth/tokens.py +57 -0
  8. blackant/client.py +400 -0
  9. blackant/config/__init__.py +0 -0
  10. blackant/config/docker_config.py +457 -0
  11. blackant/config/keycloak_admin_config.py +107 -0
  12. blackant/docker/__init__.py +12 -0
  13. blackant/docker/builder.py +616 -0
  14. blackant/docker/client.py +983 -0
  15. blackant/docker/dao.py +462 -0
  16. blackant/docker/registry.py +172 -0
  17. blackant/exceptions.py +111 -0
  18. blackant/http/__init__.py +8 -0
  19. blackant/http/client.py +125 -0
  20. blackant/patterns/__init__.py +1 -0
  21. blackant/patterns/singleton.py +20 -0
  22. blackant/services/__init__.py +10 -0
  23. blackant/services/dao.py +414 -0
  24. blackant/services/registry.py +635 -0
  25. blackant/utils/__init__.py +8 -0
  26. blackant/utils/initialization.py +32 -0
  27. blackant/utils/logging.py +337 -0
  28. blackant/utils/request_id.py +13 -0
  29. blackant/utils/store.py +50 -0
  30. blackant_sdk-1.0.2.dist-info/METADATA +117 -0
  31. blackant_sdk-1.0.2.dist-info/RECORD +70 -0
  32. blackant_sdk-1.0.2.dist-info/WHEEL +5 -0
  33. blackant_sdk-1.0.2.dist-info/top_level.txt +5 -0
  34. calculation/__init__.py +0 -0
  35. calculation/base.py +26 -0
  36. calculation/errors.py +2 -0
  37. calculation/impl/__init__.py +0 -0
  38. calculation/impl/my_calculation.py +144 -0
  39. calculation/impl/simple_calc.py +53 -0
  40. calculation/impl/test.py +1 -0
  41. calculation/impl/test_calc.py +36 -0
  42. calculation/loader.py +227 -0
  43. notifinations/__init__.py +8 -0
  44. notifinations/mail_sender.py +212 -0
  45. storage/__init__.py +0 -0
  46. storage/errors.py +10 -0
  47. storage/factory.py +26 -0
  48. storage/interface.py +19 -0
  49. storage/minio.py +106 -0
  50. task/__init__.py +0 -0
  51. task/dao.py +38 -0
  52. task/errors.py +10 -0
  53. task/log_adapter.py +11 -0
  54. task/parsers/__init__.py +0 -0
  55. task/parsers/base.py +13 -0
  56. task/parsers/callback.py +40 -0
  57. task/parsers/cmd_args.py +52 -0
  58. task/parsers/freetext.py +19 -0
  59. task/parsers/objects.py +50 -0
  60. task/parsers/request.py +56 -0
  61. task/resource.py +84 -0
  62. task/states/__init__.py +0 -0
  63. task/states/base.py +14 -0
  64. task/states/error.py +47 -0
  65. task/states/idle.py +12 -0
  66. task/states/ready.py +51 -0
  67. task/states/running.py +21 -0
  68. task/states/set_up.py +40 -0
  69. task/states/tear_down.py +29 -0
  70. task/task.py +358 -0
@@ -0,0 +1,363 @@
1
+ """Keycloak Admin API Manager
2
+
3
+ Wrapper around python-keycloak library for role management operations.
4
+ Handles service account authentication, token management, and role assignments.
5
+
6
+ Author: Balázs Milán (milan.balazs@uni-obuda.hu)
7
+ """
8
+
9
+ from typing import Optional, List, Dict, Any
10
+ import logging
11
+ from keycloak import KeycloakAdmin
12
+ from keycloak.exceptions import KeycloakError, KeycloakAuthenticationError
13
+
14
+ from ..exceptions import (
15
+ KeycloakConnectionError,
16
+ KeycloakAuthenticationError as BlackAntKeycloakAuthError,
17
+ RoleAssignmentError
18
+ )
19
+ from ..config.keycloak_admin_config import KeycloakAdminConfig
20
+
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class KeycloakManager:
26
+ """Keycloak Admin API manager for role operations.
27
+
28
+ Provides high-level interface for Keycloak role management using
29
+ service account authentication. Handles token refresh automatically.
30
+
31
+ Args:
32
+ config: KeycloakAdminConfig instance with service account credentials
33
+
34
+ Example:
35
+ >>> config = KeycloakAdminConfig.from_env()
36
+ >>> manager = KeycloakManager(config)
37
+ >>> manager.assign_role_to_user("user-uuid", "science_module")
38
+ """
39
+
40
+ def __init__(self, config: KeycloakAdminConfig):
41
+ """Initialize Keycloak manager with admin config.
42
+
43
+ Args:
44
+ config: Keycloak admin configuration
45
+
46
+ Raises:
47
+ KeycloakConnectionError: If connection to Keycloak fails
48
+ KeycloakAuthenticationError: If service account auth fails
49
+ """
50
+ self.config = config
51
+ self.logger = logger
52
+ self._admin_client: Optional[KeycloakAdmin] = None
53
+ self._initialize_admin_client()
54
+
55
+ def _initialize_admin_client(self) -> None:
56
+ """Initialize Keycloak admin client with service account.
57
+
58
+ Uses client credentials grant flow for service account authentication.
59
+ The python-keycloak library handles token refresh automatically.
60
+
61
+ Raises:
62
+ KeycloakConnectionError: If connection fails
63
+ KeycloakAuthenticationError: If authentication fails
64
+ """
65
+ try:
66
+ self._admin_client = KeycloakAdmin(
67
+ server_url=self.config.server_url,
68
+ realm_name=self.config.realm_name,
69
+ client_id=self.config.client_id,
70
+ client_secret_key=self.config.client_secret,
71
+ verify=self.config.verify_ssl,
72
+ timeout=self.config.timeout
73
+ )
74
+
75
+ # Test connection with a simple API call
76
+ self._admin_client.get_realm_roles()
77
+
78
+ self.logger.info(
79
+ f"Keycloak admin client initialized successfully "
80
+ f"(realm: {self.config.realm_name})"
81
+ )
82
+
83
+ except KeycloakAuthenticationError as e:
84
+ self.logger.error(f"Keycloak authentication failed: {e}")
85
+ raise BlackAntKeycloakAuthError(
86
+ f"Failed to authenticate with Keycloak service account: {e}"
87
+ )
88
+ except Exception as e:
89
+ self.logger.error(f"Failed to connect to Keycloak: {e}")
90
+ raise KeycloakConnectionError(
91
+ f"Could not establish connection to Keycloak server: {e}"
92
+ )
93
+
94
+ def get_user_roles(self, user_id: str) -> List[Dict[str, Any]]:
95
+ """Get all roles assigned to a user.
96
+
97
+ Args:
98
+ user_id: Keycloak user UUID
99
+
100
+ Returns:
101
+ List of role dictionaries with keys: id, name, description
102
+
103
+ Raises:
104
+ RoleAssignmentError: If role query fails
105
+
106
+ Example:
107
+ >>> roles = manager.get_user_roles("user-uuid")
108
+ >>> print([r['name'] for r in roles])
109
+ ['default-roles', 'science_module']
110
+ """
111
+ try:
112
+ # Get realm-level roles
113
+ realm_roles = self._admin_client.get_realm_roles_of_user(user_id)
114
+
115
+ self.logger.debug(
116
+ f"Retrieved {len(realm_roles)} roles for user {user_id}"
117
+ )
118
+
119
+ return realm_roles
120
+
121
+ except KeycloakError as e:
122
+ self.logger.error(f"Failed to get user roles: {e}")
123
+ raise RoleAssignmentError(
124
+ f"Could not retrieve roles for user {user_id}: {e}"
125
+ )
126
+
127
+ def has_role(self, user_id: str, role_name: str) -> bool:
128
+ """Check if user has a specific role.
129
+
130
+ Args:
131
+ user_id: Keycloak user UUID
132
+ role_name: Role name to check (e.g., "science_module")
133
+
134
+ Returns:
135
+ True if user has the role, False otherwise
136
+
137
+ Example:
138
+ >>> if manager.has_role("user-uuid", "science_module"):
139
+ ... print("User has access to science modules")
140
+ """
141
+ try:
142
+ user_roles = self.get_user_roles(user_id)
143
+ role_names = [role['name'] for role in user_roles]
144
+ has_role = role_name in role_names
145
+
146
+ self.logger.debug(
147
+ f"User {user_id} {'has' if has_role else 'does not have'} "
148
+ f"role '{role_name}'"
149
+ )
150
+
151
+ return has_role
152
+
153
+ except RoleAssignmentError:
154
+ # If we can't get roles, assume user doesn't have the role
155
+ self.logger.warning(
156
+ f"Could not check role for user {user_id}, assuming no role"
157
+ )
158
+ return False
159
+
160
+ def assign_role_to_user(
161
+ self,
162
+ user_id: str,
163
+ role_name: str,
164
+ idempotent: bool = True
165
+ ) -> bool:
166
+ """Assign a role to a user.
167
+
168
+ Args:
169
+ user_id: Keycloak user UUID
170
+ role_name: Role name to assign (e.g., "science_module")
171
+ idempotent: If True, skip assignment if user already has role
172
+
173
+ Returns:
174
+ True if role was assigned or already exists, False otherwise
175
+
176
+ Raises:
177
+ RoleAssignmentError: If role assignment fails
178
+
179
+ Example:
180
+ >>> success = manager.assign_role_to_user("user-uuid", "science_module")
181
+ >>> if success:
182
+ ... print("Role assigned successfully")
183
+ """
184
+ try:
185
+ # Check if role already exists (idempotent operation)
186
+ if idempotent and self.has_role(user_id, role_name):
187
+ self.logger.info(
188
+ f"User {user_id} already has role '{role_name}', skipping"
189
+ )
190
+ return True
191
+
192
+ # Get role object
193
+ role = self._admin_client.get_realm_role(role_name)
194
+ if not role:
195
+ raise RoleAssignmentError(
196
+ f"Role '{role_name}' does not exist in realm "
197
+ f"{self.config.realm_name}"
198
+ )
199
+
200
+ # Assign role to user (realm-level role)
201
+ self._admin_client.assign_realm_roles(
202
+ user_id=user_id,
203
+ roles=[role]
204
+ )
205
+
206
+ self.logger.info(
207
+ f"Successfully assigned role '{role_name}' to user {user_id}"
208
+ )
209
+
210
+ return True
211
+
212
+ except KeycloakError as e:
213
+ self.logger.error(
214
+ f"Failed to assign role '{role_name}' to user {user_id}: {e}"
215
+ )
216
+ raise RoleAssignmentError(
217
+ f"Could not assign role '{role_name}': {e}"
218
+ )
219
+
220
+ def remove_role_from_user(self, user_id: str, role_name: str) -> bool:
221
+ """Remove a role from a user.
222
+
223
+ Args:
224
+ user_id: Keycloak user UUID
225
+ role_name: Role name to remove
226
+
227
+ Returns:
228
+ True if role was removed, False if user didn't have the role
229
+
230
+ Raises:
231
+ RoleAssignmentError: If role removal fails
232
+ """
233
+ try:
234
+ # Check if user has the role
235
+ if not self.has_role(user_id, role_name):
236
+ self.logger.info(
237
+ f"User {user_id} doesn't have role '{role_name}', skipping"
238
+ )
239
+ return False
240
+
241
+ # Get role object
242
+ role = self._admin_client.get_realm_role(role_name)
243
+
244
+ # Remove role from user
245
+ self._admin_client.delete_realm_roles_of_user(
246
+ user_id=user_id,
247
+ roles=[role]
248
+ )
249
+
250
+ self.logger.info(
251
+ f"Successfully removed role '{role_name}' from user {user_id}"
252
+ )
253
+
254
+ return True
255
+
256
+ except KeycloakError as e:
257
+ self.logger.error(
258
+ f"Failed to remove role '{role_name}' from user {user_id}: {e}"
259
+ )
260
+ raise RoleAssignmentError(
261
+ f"Could not remove role '{role_name}': {e}"
262
+ )
263
+
264
+ def get_users_with_role(self, role_name: str) -> List[Dict[str, Any]]:
265
+ """Get all users that have a specific role.
266
+
267
+ Args:
268
+ role_name: Role name to query
269
+
270
+ Returns:
271
+ List of user dictionaries
272
+
273
+ Raises:
274
+ RoleAssignmentError: If query fails
275
+ """
276
+ try:
277
+ # Get role object
278
+ role = self._admin_client.get_realm_role(role_name)
279
+
280
+ # Get users with this role
281
+ users = self._admin_client.get_realm_role_members(role['id'])
282
+
283
+ self.logger.debug(
284
+ f"Found {len(users)} users with role '{role_name}'"
285
+ )
286
+
287
+ return users
288
+
289
+ except KeycloakError as e:
290
+ self.logger.error(f"Failed to get users with role '{role_name}': {e}")
291
+ raise RoleAssignmentError(
292
+ f"Could not retrieve users with role '{role_name}': {e}"
293
+ )
294
+
295
+ def health_check(self) -> bool:
296
+ """Check if Keycloak admin connection is healthy.
297
+
298
+ Returns:
299
+ True if connection is healthy, False otherwise
300
+ """
301
+ try:
302
+ # Simple API call to verify connection
303
+ self._admin_client.get_realm_roles()
304
+ return True
305
+ except Exception as e:
306
+ self.logger.error(f"Keycloak health check failed: {e}")
307
+ return False
308
+
309
+ def role_exists(self, role_name) -> bool:
310
+ """Checks if given role exists in the realm
311
+
312
+ Returns:
313
+ True if role exists, false otherwise
314
+ """
315
+ try:
316
+ self._admin_client.get_role(role_name)
317
+ return True
318
+ except Exception:
319
+ return False
320
+
321
+ def create_realm_role(self, role_name, description="") -> bool:
322
+ role = {
323
+ "name": role_name,
324
+ "description": description,
325
+ }
326
+ try:
327
+ self._admin_client.create_realm_role(role)
328
+ print(f"Created realm role: {role_name}")
329
+ return True
330
+ except Exception as e:
331
+ self.logger.error(f"Keycloak realm role creation failed: {e}")
332
+ return False
333
+
334
+ def delete_realm_role(self, role_name) -> bool:
335
+ try:
336
+ self._admin_client.delete_realm_role(role_name)
337
+ print(f"Deleted realm role: {role_name}")
338
+ return True
339
+ except Exception as e:
340
+ self.logger.error(f"Keycloak realm role deletion failed: {e}")
341
+ return False
342
+
343
+ # Singleton instance (optional - can be instantiated per-use)
344
+ _keycloak_manager: Optional[KeycloakManager] = None
345
+
346
+
347
+ def get_keycloak_manager(config: Optional[KeycloakAdminConfig] = None) -> KeycloakManager:
348
+ """Get or create KeycloakManager singleton instance.
349
+
350
+ Args:
351
+ config: Optional config override (uses env config if None)
352
+
353
+ Returns:
354
+ KeycloakManager instance
355
+ """
356
+ global _keycloak_manager
357
+
358
+ if _keycloak_manager is None or config is not None:
359
+ if config is None:
360
+ config = KeycloakAdminConfig.from_env()
361
+ _keycloak_manager = KeycloakManager(config)
362
+
363
+ return _keycloak_manager
@@ -0,0 +1,52 @@
1
+ """Request ID storage and generation module.
2
+
3
+ Provides unique request ID generation and storage for
4
+ distributed tracing and request tracking.
5
+ """
6
+
7
+ import time
8
+ import uuid
9
+
10
+ from ..utils.store import Store
11
+
12
+
13
+ class RequestIdStore(Store):
14
+ """Request ID storage and generator class.
15
+
16
+ Generates and stores unique request identifiers for
17
+ HTTP requests and distributed tracing.
18
+ """
19
+
20
+ def __init__(self, header_name="X-Request-ID"):
21
+ """Initialize request ID store.
22
+
23
+ Args:
24
+ header_name (str): HTTP header name for request ID.
25
+ """
26
+ super().__init__()
27
+ # Use instance variable instead of class variable
28
+ if not hasattr(self, 'header_name'):
29
+ self.header_name = header_name
30
+ if not hasattr(self, '_counter'):
31
+ self._counter = 0
32
+
33
+ def generate_id(self):
34
+ """Generate a new unique request ID.
35
+
36
+ Returns:
37
+ str: Unique request ID.
38
+ """
39
+ self._counter += 1
40
+ timestamp = int(time.time() * 1000)
41
+ request_id = f"req_{timestamp}_{self._counter}_{uuid.uuid4().hex[:8]}"
42
+ self._set("current_request_id", request_id)
43
+ return request_id
44
+
45
+ @property
46
+ def rid(self):
47
+ """Get current request ID.
48
+
49
+ Returns:
50
+ str or None: Current request ID or None if not set.
51
+ """
52
+ return self._get("current_request_id")