miso-client 0.1.0__py3-none-any.whl → 3.7.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 (69) hide show
  1. miso_client/__init__.py +523 -130
  2. miso_client/api/__init__.py +35 -0
  3. miso_client/api/auth_api.py +367 -0
  4. miso_client/api/logs_api.py +91 -0
  5. miso_client/api/permissions_api.py +88 -0
  6. miso_client/api/roles_api.py +88 -0
  7. miso_client/api/types/__init__.py +75 -0
  8. miso_client/api/types/auth_types.py +183 -0
  9. miso_client/api/types/logs_types.py +71 -0
  10. miso_client/api/types/permissions_types.py +31 -0
  11. miso_client/api/types/roles_types.py +31 -0
  12. miso_client/errors.py +30 -4
  13. miso_client/models/__init__.py +4 -0
  14. miso_client/models/config.py +275 -72
  15. miso_client/models/error_response.py +39 -0
  16. miso_client/models/filter.py +255 -0
  17. miso_client/models/pagination.py +44 -0
  18. miso_client/models/sort.py +25 -0
  19. miso_client/services/__init__.py +6 -5
  20. miso_client/services/auth.py +496 -87
  21. miso_client/services/cache.py +42 -41
  22. miso_client/services/encryption.py +18 -17
  23. miso_client/services/logger.py +467 -328
  24. miso_client/services/logger_chain.py +288 -0
  25. miso_client/services/permission.py +130 -67
  26. miso_client/services/redis.py +28 -23
  27. miso_client/services/role.py +145 -62
  28. miso_client/utils/__init__.py +3 -3
  29. miso_client/utils/audit_log_queue.py +222 -0
  30. miso_client/utils/auth_strategy.py +88 -0
  31. miso_client/utils/auth_utils.py +65 -0
  32. miso_client/utils/circuit_breaker.py +125 -0
  33. miso_client/utils/client_token_manager.py +244 -0
  34. miso_client/utils/config_loader.py +88 -17
  35. miso_client/utils/controller_url_resolver.py +80 -0
  36. miso_client/utils/data_masker.py +104 -33
  37. miso_client/utils/environment_token.py +126 -0
  38. miso_client/utils/error_utils.py +216 -0
  39. miso_client/utils/fastapi_endpoints.py +166 -0
  40. miso_client/utils/filter.py +364 -0
  41. miso_client/utils/filter_applier.py +143 -0
  42. miso_client/utils/filter_parser.py +110 -0
  43. miso_client/utils/flask_endpoints.py +169 -0
  44. miso_client/utils/http_client.py +494 -262
  45. miso_client/utils/http_client_logging.py +352 -0
  46. miso_client/utils/http_client_logging_helpers.py +197 -0
  47. miso_client/utils/http_client_query_helpers.py +138 -0
  48. miso_client/utils/http_error_handler.py +92 -0
  49. miso_client/utils/http_log_formatter.py +115 -0
  50. miso_client/utils/http_log_masker.py +203 -0
  51. miso_client/utils/internal_http_client.py +435 -0
  52. miso_client/utils/jwt_tools.py +125 -16
  53. miso_client/utils/logger_helpers.py +206 -0
  54. miso_client/utils/logging_helpers.py +70 -0
  55. miso_client/utils/origin_validator.py +128 -0
  56. miso_client/utils/pagination.py +275 -0
  57. miso_client/utils/request_context.py +285 -0
  58. miso_client/utils/sensitive_fields_loader.py +116 -0
  59. miso_client/utils/sort.py +116 -0
  60. miso_client/utils/token_utils.py +114 -0
  61. miso_client/utils/url_validator.py +66 -0
  62. miso_client/utils/user_token_refresh.py +245 -0
  63. miso_client-3.7.2.dist-info/METADATA +1021 -0
  64. miso_client-3.7.2.dist-info/RECORD +68 -0
  65. miso_client-0.1.0.dist-info/METADATA +0 -551
  66. miso_client-0.1.0.dist-info/RECORD +0 -23
  67. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/WHEEL +0 -0
  68. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/licenses/LICENSE +0 -0
  69. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/top_level.txt +0 -0
@@ -5,18 +5,23 @@ This module provides Redis connectivity with graceful degradation when Redis
5
5
  is unavailable. It handles caching of roles and permissions, and log queuing.
6
6
  """
7
7
 
8
- import redis.asyncio as redis
8
+ import logging
9
9
  from typing import Optional
10
+
11
+ import redis.asyncio as redis
12
+
10
13
  from ..models.config import RedisConfig
11
14
 
15
+ logger = logging.getLogger(__name__)
16
+
12
17
 
13
18
  class RedisService:
14
19
  """Redis service for caching and log queuing."""
15
-
20
+
16
21
  def __init__(self, config: Optional[RedisConfig] = None):
17
22
  """
18
23
  Initialize Redis service.
19
-
24
+
20
25
  Args:
21
26
  config: Optional Redis configuration
22
27
  """
@@ -27,12 +32,12 @@ class RedisService:
27
32
  async def connect(self) -> None:
28
33
  """
29
34
  Connect to Redis.
30
-
35
+
31
36
  Raises:
32
37
  Exception: If connection fails and config is provided
33
38
  """
34
39
  if not self.config:
35
- print("Redis not configured, using controller fallback")
40
+ logger.info("Redis not configured, using controller fallback")
36
41
  return
37
42
 
38
43
  try:
@@ -46,17 +51,17 @@ class RedisService:
46
51
  socket_connect_timeout=5,
47
52
  socket_timeout=5,
48
53
  )
49
-
54
+
50
55
  # Test connection
51
56
  # Some redis stubs type ping as possibly non-awaitable; support both
52
57
  resp = self.redis.ping()
53
58
  if hasattr(resp, "__await__"):
54
59
  await resp # type: ignore[misc]
55
60
  self.connected = True
56
- print("Connected to Redis")
57
-
61
+ logger.info("Connected to Redis")
62
+
58
63
  except Exception as error:
59
- print(f"Failed to connect to Redis: {error}")
64
+ logger.error(f"Failed to connect to Redis: {error}", exc_info=error)
60
65
  self.connected = False
61
66
  if self.config: # Only raise if Redis was configured
62
67
  raise error
@@ -66,12 +71,12 @@ class RedisService:
66
71
  if self.redis:
67
72
  await self.redis.aclose()
68
73
  self.connected = False
69
- print("Disconnected from Redis")
74
+ logger.info("Disconnected from Redis")
70
75
 
71
76
  def is_connected(self) -> bool:
72
77
  """
73
78
  Check if Redis is connected.
74
-
79
+
75
80
  Returns:
76
81
  True if connected, False otherwise
77
82
  """
@@ -80,10 +85,10 @@ class RedisService:
80
85
  async def get(self, key: str) -> Optional[str]:
81
86
  """
82
87
  Get value from Redis.
83
-
88
+
84
89
  Args:
85
90
  key: Redis key
86
-
91
+
87
92
  Returns:
88
93
  Value if found, None otherwise
89
94
  """
@@ -100,18 +105,18 @@ class RedisService:
100
105
  result = resp
101
106
  return None if result is None else str(result)
102
107
  except Exception as error:
103
- print(f"Redis get error: {error}")
108
+ logger.error("Redis get error", exc_info=error)
104
109
  return None
105
110
 
106
111
  async def set(self, key: str, value: str, ttl: int) -> bool:
107
112
  """
108
113
  Set value in Redis with TTL.
109
-
114
+
110
115
  Args:
111
116
  key: Redis key
112
117
  value: Value to store
113
118
  ttl: Time to live in seconds
114
-
119
+
115
120
  Returns:
116
121
  True if successful, False otherwise
117
122
  """
@@ -126,16 +131,16 @@ class RedisService:
126
131
  await resp # type: ignore[misc]
127
132
  return True
128
133
  except Exception as error:
129
- print(f"Redis set error: {error}")
134
+ logger.error("Redis set error", exc_info=error)
130
135
  return False
131
136
 
132
137
  async def delete(self, key: str) -> bool:
133
138
  """
134
139
  Delete key from Redis.
135
-
140
+
136
141
  Args:
137
142
  key: Redis key
138
-
143
+
139
144
  Returns:
140
145
  True if successful, False otherwise
141
146
  """
@@ -150,17 +155,17 @@ class RedisService:
150
155
  await resp # type: ignore[misc]
151
156
  return True
152
157
  except Exception as error:
153
- print(f"Redis delete error: {error}")
158
+ logger.error("Redis delete error", exc_info=error)
154
159
  return False
155
160
 
156
161
  async def rpush(self, queue: str, value: str) -> bool:
157
162
  """
158
163
  Push value to Redis list (for log queuing).
159
-
164
+
160
165
  Args:
161
166
  queue: Queue name
162
167
  value: Value to push
163
-
168
+
164
169
  Returns:
165
170
  True if successful, False otherwise
166
171
  """
@@ -175,5 +180,5 @@ class RedisService:
175
180
  await resp # type: ignore[misc]
176
181
  return True
177
182
  except Exception as error:
178
- print(f"Redis rpush error: {error}")
183
+ logger.error("Redis rpush error", exc_info=error)
179
184
  return False
@@ -6,39 +6,55 @@ Roles are cached with Redis and in-memory fallback using CacheService.
6
6
  Optimized to extract userId from JWT token before API calls for cache optimization.
7
7
  """
8
8
 
9
+ import logging
9
10
  import time
10
- from typing import List, cast
11
- from ..models.config import RoleResult
11
+ from typing import TYPE_CHECKING, List, Optional, cast
12
+
13
+ from ..models.config import AuthStrategy, RoleResult
12
14
  from ..services.cache import CacheService
15
+ from ..utils.auth_utils import validate_token_request
16
+ from ..utils.error_utils import extract_correlation_id_from_error
13
17
  from ..utils.http_client import HttpClient
14
18
  from ..utils.jwt_tools import extract_user_id
15
19
 
20
+ if TYPE_CHECKING:
21
+ from ..api import ApiClient
22
+
23
+ logger = logging.getLogger(__name__)
24
+
16
25
 
17
26
  class RoleService:
18
27
  """Role service for user authorization with caching."""
19
-
20
- def __init__(self, http_client: HttpClient, cache: CacheService):
28
+
29
+ def __init__(
30
+ self, http_client: HttpClient, cache: CacheService, api_client: Optional["ApiClient"] = None
31
+ ):
21
32
  """
22
33
  Initialize role service.
23
-
34
+
24
35
  Args:
25
- http_client: HTTP client instance
36
+ http_client: HTTP client instance (for backward compatibility)
26
37
  cache: Cache service instance (handles Redis + in-memory fallback)
38
+ api_client: Optional API client instance (for typed API calls)
27
39
  """
28
40
  self.config = http_client.config
29
41
  self.http_client = http_client
30
42
  self.cache = cache
43
+ self.api_client = api_client
31
44
  self.role_ttl = self.config.role_ttl
32
45
 
33
- async def get_roles(self, token: str) -> List[str]:
46
+ async def get_roles(
47
+ self, token: str, auth_strategy: Optional[AuthStrategy] = None
48
+ ) -> List[str]:
34
49
  """
35
50
  Get user roles with Redis caching.
36
-
51
+
37
52
  Optimized to extract userId from token first to check cache before API call.
38
-
53
+
39
54
  Args:
40
55
  token: JWT token
41
-
56
+ auth_strategy: Optional authentication strategy
57
+
42
58
  Returns:
43
59
  List of user roles
44
60
  """
@@ -56,10 +72,8 @@ class RoleService:
56
72
  # Cache miss or no userId in token - fetch from controller
57
73
  # If we don't have userId, get it from validate endpoint
58
74
  if not user_id:
59
- user_info = await self.http_client.authenticated_request(
60
- "POST",
61
- "/api/auth/validate",
62
- token
75
+ user_info = await validate_token_request(
76
+ token, self.http_client, self.api_client, auth_strategy
63
77
  )
64
78
  user_id = user_info.get("user", {}).get("id") if user_info else None
65
79
  if not user_id:
@@ -67,89 +81,110 @@ class RoleService:
67
81
  cache_key = f"roles:{user_id}"
68
82
 
69
83
  # Cache miss - fetch from controller
70
- role_result = await self.http_client.authenticated_request(
71
- "GET",
72
- "/api/auth/roles", # Backend knows app/env from client token
73
- token
74
- )
75
-
76
- role_data = RoleResult(**role_result)
77
- roles = role_data.roles or []
84
+ if self.api_client:
85
+ # Use ApiClient for typed API calls
86
+ response = await self.api_client.roles.get_roles(token, auth_strategy=auth_strategy)
87
+ roles = response.data.roles or []
88
+ else:
89
+ # Fallback to HttpClient for backward compatibility
90
+ if auth_strategy is not None:
91
+ role_result = await self.http_client.authenticated_request(
92
+ "GET", "/api/v1/auth/roles", token, auth_strategy=auth_strategy
93
+ )
94
+ else:
95
+ role_result = await self.http_client.authenticated_request(
96
+ "GET", "/api/v1/auth/roles", token
97
+ )
98
+
99
+ role_data = RoleResult(**role_result)
100
+ roles = role_data.roles or []
78
101
 
79
102
  # Cache the result (CacheService handles Redis + in-memory automatically)
80
103
  assert cache_key is not None
81
104
  await self.cache.set(
82
- cache_key,
83
- {"roles": roles, "timestamp": int(time.time() * 1000)},
84
- self.role_ttl
105
+ cache_key, {"roles": roles, "timestamp": int(time.time() * 1000)}, self.role_ttl
85
106
  )
86
107
 
87
108
  return roles
88
-
89
- except Exception:
90
- # Failed to get roles, return empty list
109
+
110
+ except Exception as error:
111
+ correlation_id = extract_correlation_id_from_error(error)
112
+ logger.error(
113
+ "Failed to get roles",
114
+ exc_info=error,
115
+ extra={"correlationId": correlation_id} if correlation_id else None,
116
+ )
91
117
  return []
92
118
 
93
- async def has_role(self, token: str, role: str) -> bool:
119
+ async def has_role(
120
+ self, token: str, role: str, auth_strategy: Optional[AuthStrategy] = None
121
+ ) -> bool:
94
122
  """
95
123
  Check if user has specific role.
96
-
124
+
97
125
  Args:
98
126
  token: JWT token
99
127
  role: Role to check
100
-
128
+ auth_strategy: Optional authentication strategy
129
+
101
130
  Returns:
102
131
  True if user has the role, False otherwise
103
132
  """
104
- roles = await self.get_roles(token)
133
+ roles = await self.get_roles(token, auth_strategy=auth_strategy)
105
134
  return role in roles
106
135
 
107
- async def has_any_role(self, token: str, roles: List[str]) -> bool:
136
+ async def has_any_role(
137
+ self, token: str, roles: List[str], auth_strategy: Optional[AuthStrategy] = None
138
+ ) -> bool:
108
139
  """
109
140
  Check if user has any of the specified roles.
110
-
141
+
111
142
  Args:
112
143
  token: JWT token
113
144
  roles: List of roles to check
114
-
145
+ auth_strategy: Optional authentication strategy
146
+
115
147
  Returns:
116
148
  True if user has any of the roles, False otherwise
117
149
  """
118
- user_roles = await self.get_roles(token)
150
+ user_roles = await self.get_roles(token, auth_strategy=auth_strategy)
119
151
  return any(role in user_roles for role in roles)
120
152
 
121
- async def has_all_roles(self, token: str, roles: List[str]) -> bool:
153
+ async def has_all_roles(
154
+ self, token: str, roles: List[str], auth_strategy: Optional[AuthStrategy] = None
155
+ ) -> bool:
122
156
  """
123
157
  Check if user has all of the specified roles.
124
-
158
+
125
159
  Args:
126
160
  token: JWT token
127
161
  roles: List of roles to check
128
-
162
+ auth_strategy: Optional authentication strategy
163
+
129
164
  Returns:
130
165
  True if user has all roles, False otherwise
131
166
  """
132
- user_roles = await self.get_roles(token)
167
+ user_roles = await self.get_roles(token, auth_strategy=auth_strategy)
133
168
  return all(role in user_roles for role in roles)
134
169
 
135
- async def refresh_roles(self, token: str) -> List[str]:
170
+ async def refresh_roles(
171
+ self, token: str, auth_strategy: Optional[AuthStrategy] = None
172
+ ) -> List[str]:
136
173
  """
137
174
  Force refresh roles from controller (bypass cache).
138
-
175
+
139
176
  Args:
140
177
  token: JWT token
141
-
178
+ auth_strategy: Optional authentication strategy
179
+
142
180
  Returns:
143
181
  Fresh list of user roles
144
182
  """
145
183
  try:
146
184
  # Get user info to extract userId
147
- user_info = await self.http_client.authenticated_request(
148
- "POST",
149
- "/api/auth/validate",
150
- token
185
+ user_info = await validate_token_request(
186
+ token, self.http_client, self.api_client, auth_strategy
151
187
  )
152
-
153
188
  user_id = user_info.get("user", {}).get("id") if user_info else None
154
189
  if not user_id:
155
190
  return []
@@ -157,24 +192,72 @@ class RoleService:
157
192
  cache_key = f"roles:{user_id}"
158
193
 
159
194
  # Fetch fresh roles from controller using refresh endpoint
160
- role_result = await self.http_client.authenticated_request(
161
- "GET",
162
- "/api/auth/roles/refresh",
163
- token
164
- )
165
-
166
- role_data = RoleResult(**role_result)
167
- roles = role_data.roles or []
195
+ if self.api_client:
196
+ # Use ApiClient for typed API calls
197
+ response = await self.api_client.roles.refresh_roles(
198
+ token, auth_strategy=auth_strategy
199
+ )
200
+ roles = response.data.roles or []
201
+ else:
202
+ # Fallback to HttpClient for backward compatibility
203
+ if auth_strategy is not None:
204
+ role_result = await self.http_client.authenticated_request(
205
+ "GET", "/api/v1/auth/roles/refresh", token, auth_strategy=auth_strategy
206
+ )
207
+ else:
208
+ role_result = await self.http_client.authenticated_request(
209
+ "GET", "/api/v1/auth/roles/refresh", token
210
+ )
211
+
212
+ role_data = RoleResult(**role_result)
213
+ roles = role_data.roles or []
168
214
 
169
215
  # Update cache with fresh data (CacheService handles Redis + in-memory automatically)
170
216
  await self.cache.set(
171
- cache_key,
172
- {"roles": roles, "timestamp": int(time.time() * 1000)},
173
- self.role_ttl
217
+ cache_key, {"roles": roles, "timestamp": int(time.time() * 1000)}, self.role_ttl
174
218
  )
175
219
 
176
220
  return roles
177
-
178
- except Exception:
179
- # Failed to refresh roles, return empty list
221
+
222
+ except Exception as error:
223
+ correlation_id = extract_correlation_id_from_error(error)
224
+ logger.error(
225
+ "Failed to refresh roles",
226
+ exc_info=error,
227
+ extra={"correlationId": correlation_id} if correlation_id else None,
228
+ )
180
229
  return []
230
+
231
+ async def clear_roles_cache(
232
+ self, token: str, auth_strategy: Optional[AuthStrategy] = None
233
+ ) -> None:
234
+ """
235
+ Clear cached roles for a user.
236
+
237
+ Args:
238
+ token: JWT token
239
+ auth_strategy: Optional authentication strategy
240
+ """
241
+ try:
242
+ # Extract userId from token to avoid unnecessary API calls
243
+ user_id = extract_user_id(token)
244
+ if not user_id:
245
+ # If userId not in token, try to get it from validate endpoint
246
+ user_info = await validate_token_request(
247
+ token, self.http_client, self.api_client, auth_strategy
248
+ )
249
+ user_id = user_info.get("user", {}).get("id") if user_info else None
250
+ if not user_id:
251
+ return # Cannot clear cache without userId
252
+
253
+ cache_key = f"roles:{user_id}"
254
+ await self.cache.delete(cache_key)
255
+
256
+ except Exception as error:
257
+ correlation_id = extract_correlation_id_from_error(error)
258
+ logger.error(
259
+ "Failed to clear roles cache",
260
+ exc_info=error,
261
+ extra={"correlationId": correlation_id} if correlation_id else None,
262
+ )
263
+ # Silently continue per service method pattern
@@ -1,9 +1,9 @@
1
1
  """Utility modules for MisoClient SDK."""
2
2
 
3
- from .http_client import HttpClient
4
3
  from .config_loader import load_config
5
4
  from .data_masker import DataMasker
6
- from .jwt_tools import decode_token, extract_user_id, extract_session_id
5
+ from .http_client import HttpClient
6
+ from .jwt_tools import decode_token, extract_session_id, extract_user_id
7
7
 
8
8
  __all__ = [
9
9
  "HttpClient",
@@ -12,4 +12,4 @@ __all__ = [
12
12
  "decode_token",
13
13
  "extract_user_id",
14
14
  "extract_session_id",
15
- ]
15
+ ]