miso-client 0.1.0__py3-none-any.whl → 0.2.0__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.

Potentially problematic release.


This version of miso-client might be problematic. Click here for more details.

@@ -6,36 +6,38 @@ in the application. It supports reading the encryption key from environment vari
6
6
  or accepting it as a constructor parameter.
7
7
  """
8
8
 
9
- import os
10
9
  import base64
10
+ import os
11
11
  from typing import Optional
12
+
12
13
  from cryptography.fernet import Fernet
14
+
13
15
  from ..errors import ConfigurationError
14
16
 
15
17
 
16
18
  class EncryptionService:
17
19
  """Service for encrypting/decrypting sensitive data using Fernet encryption."""
18
-
20
+
19
21
  def __init__(self, encryption_key: Optional[str] = None):
20
22
  """
21
23
  Initialize encryption service with key from environment or parameter.
22
-
24
+
23
25
  Args:
24
26
  encryption_key: Optional encryption key. If not provided, reads from EXTENSION_KEY env var.
25
27
  If provided, overrides environment variable.
26
-
28
+
27
29
  Raises:
28
30
  ConfigurationError: If encryption key is not found or invalid
29
31
  """
30
32
  # Use provided key, or fall back to environment variable
31
33
  key = encryption_key or os.environ.get("ENCRYPTION_KEY")
32
-
34
+
33
35
  if not key:
34
36
  raise ConfigurationError(
35
37
  "ENCRYPTION_KEY not found. Either set ENCRYPTION_KEY environment variable "
36
38
  "or pass encryption_key parameter to EncryptionService constructor."
37
39
  )
38
-
40
+
39
41
  try:
40
42
  # Fernet.generate_key() returns bytes, but env vars are strings
41
43
  # Convert string to bytes if needed
@@ -45,49 +47,48 @@ class EncryptionService:
45
47
  raise ConfigurationError(
46
48
  f"Failed to initialize encryption service with provided key: {str(e)}"
47
49
  ) from e
48
-
50
+
49
51
  def encrypt(self, plaintext: str) -> str:
50
52
  """
51
53
  Encrypt sensitive data.
52
-
54
+
53
55
  Args:
54
56
  plaintext: Plain text string to encrypt
55
-
57
+
56
58
  Returns:
57
59
  Base64-encoded encrypted string
58
-
60
+
59
61
  Raises:
60
62
  ValueError: If encryption fails
61
63
  """
62
64
  if not plaintext:
63
65
  return ""
64
-
66
+
65
67
  try:
66
68
  encrypted = self.fernet.encrypt(plaintext.encode())
67
69
  return base64.b64encode(encrypted).decode()
68
70
  except Exception as e:
69
71
  raise ValueError(f"Failed to encrypt data: {str(e)}") from e
70
-
72
+
71
73
  def decrypt(self, encrypted_text: str) -> str:
72
74
  """
73
75
  Decrypt sensitive data.
74
-
76
+
75
77
  Args:
76
78
  encrypted_text: Base64-encoded encrypted string
77
-
79
+
78
80
  Returns:
79
81
  Decrypted plain text string
80
-
82
+
81
83
  Raises:
82
84
  ValueError: If decryption fails or data is invalid
83
85
  """
84
86
  if not encrypted_text:
85
87
  return ""
86
-
88
+
87
89
  try:
88
90
  decoded = base64.b64decode(encrypted_text.encode())
89
91
  decrypted = self.fernet.decrypt(decoded)
90
92
  return decrypted.decode()
91
93
  except Exception as e:
92
94
  raise ValueError(f"Failed to decrypt data: {str(e)}") from e
93
-
@@ -9,24 +9,22 @@ import os
9
9
  import random
10
10
  import sys
11
11
  from datetime import datetime
12
- from typing import Optional, Dict, Any, Literal
13
- from ..models.config import (
14
- LogEntry,
15
- ClientLoggingOptions
16
- )
12
+ from typing import Any, Dict, Literal, Optional
13
+
14
+ from ..models.config import ClientLoggingOptions, LogEntry
17
15
  from ..services.redis import RedisService
18
- from ..utils.http_client import HttpClient
19
16
  from ..utils.data_masker import DataMasker
17
+ from ..utils.http_client import HttpClient
20
18
  from ..utils.jwt_tools import decode_token
21
19
 
22
20
 
23
21
  class LoggerService:
24
22
  """Logger service for application logging and audit events."""
25
-
23
+
26
24
  def __init__(self, http_client: HttpClient, redis: RedisService):
27
25
  """
28
26
  Initialize logger service.
29
-
27
+
30
28
  Args:
31
29
  http_client: HTTP client instance
32
30
  redis: Redis service instance
@@ -37,63 +35,67 @@ class LoggerService:
37
35
  self.mask_sensitive_data = True # Default: mask sensitive data
38
36
  self.correlation_counter = 0
39
37
  self.performance_metrics: Dict[str, Dict[str, Any]] = {}
40
-
38
+
41
39
  def set_masking(self, enabled: bool) -> None:
42
40
  """
43
41
  Enable or disable sensitive data masking.
44
-
42
+
45
43
  Args:
46
44
  enabled: Whether to enable data masking
47
45
  """
48
46
  self.mask_sensitive_data = enabled
49
-
47
+
50
48
  def _generate_correlation_id(self) -> str:
51
49
  """
52
50
  Generate unique correlation ID for request tracking.
53
-
51
+
54
52
  Format: {clientId[0:10]}-{timestamp}-{counter}-{random}
55
-
53
+
56
54
  Returns:
57
55
  Correlation ID string
58
56
  """
59
57
  self.correlation_counter = (self.correlation_counter + 1) % 10000
60
58
  timestamp = int(datetime.now().timestamp() * 1000)
61
- random_part = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=6))
62
- client_prefix = self.config.client_id[:10] if len(self.config.client_id) > 10 else self.config.client_id
59
+ random_part = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=6))
60
+ client_prefix = (
61
+ self.config.client_id[:10] if len(self.config.client_id) > 10 else self.config.client_id
62
+ )
63
63
  return f"{client_prefix}-{timestamp}-{self.correlation_counter}-{random_part}"
64
-
64
+
65
65
  def _extract_jwt_context(self, token: Optional[str]) -> Dict[str, Any]:
66
66
  """
67
67
  Extract JWT token information.
68
-
68
+
69
69
  Args:
70
70
  token: JWT token string
71
-
71
+
72
72
  Returns:
73
73
  Dictionary with userId, applicationId, sessionId, roles, permissions
74
74
  """
75
75
  if not token:
76
76
  return {}
77
-
77
+
78
78
  try:
79
79
  decoded = decode_token(token)
80
80
  if not decoded:
81
81
  return {}
82
-
82
+
83
83
  # Extract roles - handle different formats
84
84
  roles = []
85
85
  if "roles" in decoded:
86
86
  roles = decoded["roles"] if isinstance(decoded["roles"], list) else []
87
87
  elif "realm_access" in decoded and isinstance(decoded["realm_access"], dict):
88
88
  roles = decoded["realm_access"].get("roles", [])
89
-
89
+
90
90
  # Extract permissions - handle different formats
91
91
  permissions = []
92
92
  if "permissions" in decoded:
93
- permissions = decoded["permissions"] if isinstance(decoded["permissions"], list) else []
93
+ permissions = (
94
+ decoded["permissions"] if isinstance(decoded["permissions"], list) else []
95
+ )
94
96
  elif "scope" in decoded and isinstance(decoded["scope"], str):
95
97
  permissions = decoded["scope"].split()
96
-
98
+
97
99
  return {
98
100
  "userId": decoded.get("sub") or decoded.get("userId") or decoded.get("user_id"),
99
101
  "applicationId": decoded.get("applicationId") or decoded.get("app_id"),
@@ -104,98 +106,104 @@ class LoggerService:
104
106
  except Exception:
105
107
  # JWT parsing failed, return empty context
106
108
  return {}
107
-
109
+
108
110
  def _extract_metadata(self) -> Dict[str, Any]:
109
111
  """
110
112
  Extract metadata from environment (browser or Node.js).
111
-
113
+
112
114
  Returns:
113
115
  Dictionary with hostname, userAgent, etc.
114
116
  """
115
117
  metadata: Dict[str, Any] = {}
116
-
118
+
117
119
  # Try to extract Node.js/Python metadata
118
120
  if hasattr(os, "environ"):
119
121
  metadata["hostname"] = os.environ.get("HOSTNAME", "unknown")
120
-
122
+
121
123
  # In Python, we don't have browser metadata like in TypeScript
122
124
  # But we can capture some environment info
123
125
  metadata["platform"] = sys.platform
124
126
  metadata["python_version"] = sys.version
125
-
127
+
126
128
  return metadata
127
-
129
+
128
130
  def start_performance_tracking(self, operation_id: str) -> None:
129
131
  """
130
132
  Start performance tracking.
131
-
133
+
132
134
  Args:
133
135
  operation_id: Unique identifier for this operation
134
136
  """
135
137
  try:
136
138
  import psutil
139
+
137
140
  process = psutil.Process()
138
141
  memory_info = process.memory_info()
139
142
  memory_usage = {
140
143
  "rss": memory_info.rss,
141
144
  "heapTotal": memory_info.rss, # Approximation
142
- "heapUsed": memory_info.rss - memory_info.available if hasattr(memory_info, "available") else memory_info.rss,
145
+ "heapUsed": memory_info.rss - memory_info.available
146
+ if hasattr(memory_info, "available")
147
+ else memory_info.rss,
143
148
  "external": 0,
144
149
  "arrayBuffers": 0,
145
150
  }
146
151
  except ImportError:
147
152
  # psutil not available
148
153
  memory_usage = None
149
-
154
+
150
155
  self.performance_metrics[operation_id] = {
151
156
  "startTime": int(datetime.now().timestamp() * 1000),
152
157
  "memoryUsage": memory_usage,
153
158
  }
154
-
159
+
155
160
  def end_performance_tracking(self, operation_id: str) -> Optional[Dict[str, Any]]:
156
161
  """
157
162
  End performance tracking and get metrics.
158
-
163
+
159
164
  Args:
160
165
  operation_id: Unique identifier for this operation
161
-
166
+
162
167
  Returns:
163
168
  Performance metrics dictionary or None if not found
164
169
  """
165
170
  if operation_id not in self.performance_metrics:
166
171
  return None
167
-
172
+
168
173
  metrics = self.performance_metrics[operation_id]
169
174
  metrics["endTime"] = int(datetime.now().timestamp() * 1000)
170
175
  metrics["duration"] = metrics["endTime"] - metrics["startTime"]
171
-
176
+
172
177
  try:
173
178
  import psutil
179
+
174
180
  process = psutil.Process()
175
181
  memory_info = process.memory_info()
176
182
  metrics["memoryUsage"] = {
177
183
  "rss": memory_info.rss,
178
184
  "heapTotal": memory_info.rss,
179
- "heapUsed": memory_info.rss - memory_info.available if hasattr(memory_info, "available") else memory_info.rss,
185
+ "heapUsed": memory_info.rss - memory_info.available
186
+ if hasattr(memory_info, "available")
187
+ else memory_info.rss,
180
188
  "external": 0,
181
189
  "arrayBuffers": 0,
182
190
  }
183
191
  except (ImportError, Exception):
184
192
  pass # psutil not available or error getting memory info
185
-
193
+
186
194
  del self.performance_metrics[operation_id]
187
195
  return metrics
188
-
196
+
189
197
  async def error(
190
198
  self,
191
199
  message: str,
192
200
  context: Optional[Dict[str, Any]] = None,
193
201
  stack_trace: Optional[str] = None,
194
- options: Optional[ClientLoggingOptions] = None
202
+ options: Optional[ClientLoggingOptions] = None,
195
203
  ) -> None:
196
204
  """
197
205
  Log error message with optional stack trace and enhanced options.
198
-
206
+
199
207
  Args:
200
208
  message: Error message
201
209
  context: Additional context data
@@ -203,55 +211,51 @@ class LoggerService:
203
211
  options: Logging options
204
212
  """
205
213
  await self._log("error", message, context, stack_trace, options)
206
-
214
+
207
215
  async def audit(
208
216
  self,
209
217
  action: str,
210
218
  resource: str,
211
219
  context: Optional[Dict[str, Any]] = None,
212
- options: Optional[ClientLoggingOptions] = None
220
+ options: Optional[ClientLoggingOptions] = None,
213
221
  ) -> None:
214
222
  """
215
223
  Log audit event with enhanced options.
216
-
224
+
217
225
  Args:
218
226
  action: Action performed
219
227
  resource: Resource affected
220
228
  context: Additional context data
221
229
  options: Logging options
222
230
  """
223
- audit_context = {
224
- "action": action,
225
- "resource": resource,
226
- **(context or {})
227
- }
231
+ audit_context = {"action": action, "resource": resource, **(context or {})}
228
232
  await self._log("audit", f"Audit: {action} on {resource}", audit_context, None, options)
229
-
233
+
230
234
  async def info(
231
235
  self,
232
236
  message: str,
233
237
  context: Optional[Dict[str, Any]] = None,
234
- options: Optional[ClientLoggingOptions] = None
238
+ options: Optional[ClientLoggingOptions] = None,
235
239
  ) -> None:
236
240
  """
237
241
  Log info message with enhanced options.
238
-
242
+
239
243
  Args:
240
244
  message: Info message
241
245
  context: Additional context data
242
246
  options: Logging options
243
247
  """
244
248
  await self._log("info", message, context, None, options)
245
-
249
+
246
250
  async def debug(
247
251
  self,
248
252
  message: str,
249
253
  context: Optional[Dict[str, Any]] = None,
250
- options: Optional[ClientLoggingOptions] = None
254
+ options: Optional[ClientLoggingOptions] = None,
251
255
  ) -> None:
252
256
  """
253
257
  Log debug message with enhanced options.
254
-
258
+
255
259
  Args:
256
260
  message: Debug message
257
261
  context: Additional context data
@@ -259,18 +263,18 @@ class LoggerService:
259
263
  """
260
264
  if self.config.log_level == "debug":
261
265
  await self._log("debug", message, context, None, options)
262
-
266
+
263
267
  async def _log(
264
268
  self,
265
269
  level: Literal["error", "audit", "info", "debug"],
266
270
  message: str,
267
271
  context: Optional[Dict[str, Any]] = None,
268
272
  stack_trace: Optional[str] = None,
269
- options: Optional[ClientLoggingOptions] = None
273
+ options: Optional[ClientLoggingOptions] = None,
270
274
  ) -> None:
271
275
  """
272
276
  Internal log method with enhanced features.
273
-
277
+
274
278
  Args:
275
279
  level: Log level
276
280
  message: Log message
@@ -279,26 +283,32 @@ class LoggerService:
279
283
  options: Logging options
280
284
  """
281
285
  # Extract JWT context if token provided
282
- jwt_context = self._extract_jwt_context(options.token if options else None) if options else {}
283
-
286
+ jwt_context = (
287
+ self._extract_jwt_context(options.token if options else None) if options else {}
288
+ )
289
+
284
290
  # Extract environment metadata
285
291
  metadata = self._extract_metadata()
286
-
292
+
287
293
  # Generate correlation ID if not provided
288
- correlation_id = (options.correlationId if options else None) or self._generate_correlation_id()
289
-
294
+ correlation_id = (
295
+ options.correlationId if options else None
296
+ ) or self._generate_correlation_id()
297
+
290
298
  # Mask sensitive data in context if enabled
291
- mask_sensitive = (options.maskSensitiveData if options else None) is not False and self.mask_sensitive_data
299
+ mask_sensitive = (
300
+ options.maskSensitiveData if options else None
301
+ ) is not False and self.mask_sensitive_data
292
302
  masked_context = (
293
- DataMasker.mask_sensitive_data(context) if mask_sensitive and context
294
- else context
303
+ DataMasker.mask_sensitive_data(context) if mask_sensitive and context else context
295
304
  )
296
-
305
+
297
306
  # Add performance metrics if requested
298
307
  enhanced_context = masked_context
299
308
  if options and options.performanceMetrics:
300
309
  try:
301
310
  import psutil
311
+
302
312
  process = psutil.Process()
303
313
  memory_info = process.memory_info()
304
314
  enhanced_context = {
@@ -307,14 +317,16 @@ class LoggerService:
307
317
  "memoryUsage": {
308
318
  "rss": memory_info.rss,
309
319
  "heapTotal": memory_info.rss,
310
- "heapUsed": memory_info.rss - memory_info.available if hasattr(memory_info, "available") else memory_info.rss,
320
+ "heapUsed": memory_info.rss - memory_info.available
321
+ if hasattr(memory_info, "available")
322
+ else memory_info.rss,
311
323
  },
312
324
  "uptime": psutil.boot_time() if hasattr(psutil, "boot_time") else 0,
313
- }
325
+ },
314
326
  }
315
327
  except (ImportError, Exception):
316
328
  pass # psutil not available or error getting memory info
317
-
329
+
318
330
  log_entry_data = {
319
331
  "timestamp": datetime.utcnow().isoformat(),
320
332
  "level": level,
@@ -328,47 +340,49 @@ class LoggerService:
328
340
  "userId": (options.userId if options else None) or jwt_context.get("userId"),
329
341
  "sessionId": (options.sessionId if options else None) or jwt_context.get("sessionId"),
330
342
  "requestId": options.requestId if options else None,
331
- **metadata
343
+ **metadata,
332
344
  }
333
-
345
+
334
346
  # Remove None values
335
347
  log_entry_data = {k: v for k, v in log_entry_data.items() if v is not None}
336
-
348
+
337
349
  log_entry = LogEntry(**log_entry_data)
338
-
350
+
339
351
  # Try Redis first (if available)
340
352
  if self.redis.is_connected():
341
353
  queue_name = f"logs:{self.config.client_id}"
342
354
  success = await self.redis.rpush(queue_name, log_entry.model_dump_json())
343
-
355
+
344
356
  if success:
345
357
  return # Successfully queued in Redis
346
-
358
+
347
359
  # Fallback to unified logging endpoint with client credentials
348
360
  try:
349
361
  # Backend extracts environment and application from client credentials
350
- log_payload = log_entry.model_dump(exclude={"environment", "application"}, exclude_none=True)
362
+ log_payload = log_entry.model_dump(
363
+ exclude={"environment", "application"}, exclude_none=True
364
+ )
351
365
  await self.http_client.request("POST", "/api/logs", log_payload)
352
366
  except Exception:
353
367
  # Failed to send log to controller
354
368
  # Silently fail to avoid infinite logging loops
355
369
  # Application should implement retry or buffer strategy if needed
356
370
  pass
357
-
371
+
358
372
  def with_context(self, context: Dict[str, Any]) -> "LoggerChain":
359
373
  """Create logger chain with context."""
360
374
  return LoggerChain(self, context, ClientLoggingOptions())
361
-
375
+
362
376
  def with_token(self, token: str) -> "LoggerChain":
363
377
  """Create logger chain with token."""
364
378
  return LoggerChain(self, {}, ClientLoggingOptions(token=token))
365
-
379
+
366
380
  def with_performance(self) -> "LoggerChain":
367
381
  """Create logger chain with performance metrics."""
368
382
  opts = ClientLoggingOptions()
369
383
  opts.performanceMetrics = True
370
384
  return LoggerChain(self, {}, opts)
371
-
385
+
372
386
  def without_masking(self) -> "LoggerChain":
373
387
  """Create logger chain without data masking."""
374
388
  opts = ClientLoggingOptions()
@@ -378,16 +392,16 @@ class LoggerService:
378
392
 
379
393
  class LoggerChain:
380
394
  """Method chaining class for fluent logging API."""
381
-
395
+
382
396
  def __init__(
383
397
  self,
384
398
  logger: LoggerService,
385
399
  context: Optional[Dict[str, Any]] = None,
386
- options: Optional[ClientLoggingOptions] = None
400
+ options: Optional[ClientLoggingOptions] = None,
387
401
  ):
388
402
  """
389
403
  Initialize logger chain.
390
-
404
+
391
405
  Args:
392
406
  logger: Logger service instance
393
407
  context: Initial context
@@ -396,62 +410,62 @@ class LoggerChain:
396
410
  self.logger = logger
397
411
  self.context = context or {}
398
412
  self.options = options or ClientLoggingOptions()
399
-
413
+
400
414
  def add_context(self, key: str, value: Any) -> "LoggerChain":
401
415
  """Add context key-value pair."""
402
416
  self.context[key] = value
403
417
  return self
404
-
418
+
405
419
  def add_user(self, user_id: str) -> "LoggerChain":
406
420
  """Add user ID."""
407
421
  if self.options is None:
408
422
  self.options = ClientLoggingOptions()
409
423
  self.options.userId = user_id
410
424
  return self
411
-
425
+
412
426
  def add_application(self, application_id: str) -> "LoggerChain":
413
427
  """Add application ID."""
414
428
  if self.options is None:
415
429
  self.options = ClientLoggingOptions()
416
430
  self.options.applicationId = application_id
417
431
  return self
418
-
432
+
419
433
  def add_correlation(self, correlation_id: str) -> "LoggerChain":
420
434
  """Add correlation ID."""
421
435
  if self.options is None:
422
436
  self.options = ClientLoggingOptions()
423
437
  self.options.correlationId = correlation_id
424
438
  return self
425
-
439
+
426
440
  def with_token(self, token: str) -> "LoggerChain":
427
441
  """Add token for context extraction."""
428
442
  if self.options is None:
429
443
  self.options = ClientLoggingOptions()
430
444
  self.options.token = token
431
445
  return self
432
-
446
+
433
447
  def with_performance(self) -> "LoggerChain":
434
448
  """Enable performance metrics."""
435
449
  if self.options is None:
436
450
  self.options = ClientLoggingOptions()
437
451
  self.options.performanceMetrics = True
438
452
  return self
439
-
453
+
440
454
  def without_masking(self) -> "LoggerChain":
441
455
  """Disable data masking."""
442
456
  if self.options is None:
443
457
  self.options = ClientLoggingOptions()
444
458
  self.options.maskSensitiveData = False
445
459
  return self
446
-
460
+
447
461
  async def error(self, message: str, stack_trace: Optional[str] = None) -> None:
448
462
  """Log error."""
449
463
  await self.logger.error(message, self.context, stack_trace, self.options)
450
-
464
+
451
465
  async def info(self, message: str) -> None:
452
466
  """Log info."""
453
467
  await self.logger.info(message, self.context, self.options)
454
-
468
+
455
469
  async def audit(self, action: str, resource: str) -> None:
456
470
  """Log audit."""
457
471
  await self.logger.audit(action, resource, self.context, self.options)