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
@@ -0,0 +1,222 @@
1
+ """
2
+ Audit log queue for batching multiple logs into single requests.
3
+
4
+ Reduces network overhead by batching audit logs.
5
+ """
6
+
7
+ import asyncio
8
+ import signal
9
+ from typing import TYPE_CHECKING, List, Optional
10
+
11
+ from ..models.config import AuditConfig, LogEntry, MisoClientConfig
12
+ from ..services.redis import RedisService
13
+ from ..utils.circuit_breaker import CircuitBreaker
14
+
15
+ if TYPE_CHECKING:
16
+ from ..utils.http_client import HttpClient
17
+
18
+
19
+ class QueuedLogEntry:
20
+ """Internal class for queued log entries."""
21
+
22
+ def __init__(self, entry: LogEntry, timestamp: int):
23
+ """
24
+ Initialize queued log entry.
25
+
26
+ Args:
27
+ entry: LogEntry object
28
+ timestamp: Timestamp in milliseconds
29
+ """
30
+ self.entry = entry
31
+ self.timestamp = timestamp
32
+
33
+
34
+ class AuditLogQueue:
35
+ """
36
+ Audit log queue for batching multiple logs into single requests.
37
+
38
+ Automatically batches audit logs based on size and time thresholds.
39
+ Supports Redis LIST for efficient queuing with HTTP fallback.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ http_client: "HttpClient",
45
+ redis: RedisService,
46
+ config: MisoClientConfig,
47
+ ):
48
+ """
49
+ Initialize audit log queue.
50
+
51
+ Args:
52
+ http_client: HttpClient instance for sending logs
53
+ redis: RedisService instance for queuing
54
+ config: MisoClientConfig with audit configuration
55
+ """
56
+ self.http_client = http_client
57
+ self.redis = redis
58
+ self.config = config
59
+ self.queue: List[QueuedLogEntry] = []
60
+ self.flush_timer: Optional[asyncio.Task] = None
61
+ self.is_flushing = False
62
+
63
+ audit_config: Optional[AuditConfig] = config.audit
64
+ self.batch_size: int = (
65
+ audit_config.batchSize if audit_config and audit_config.batchSize is not None else 10
66
+ )
67
+ self.batch_interval: int = (
68
+ audit_config.batchInterval
69
+ if audit_config and audit_config.batchInterval is not None
70
+ else 100
71
+ )
72
+
73
+ # Initialize circuit breaker for HTTP logging
74
+ circuit_breaker_config = audit_config.circuitBreaker if audit_config else None
75
+ self.circuit_breaker = CircuitBreaker(circuit_breaker_config)
76
+
77
+ # Setup graceful shutdown handlers (if available)
78
+ try:
79
+ signal.signal(signal.SIGINT, self._signal_handler)
80
+ signal.signal(signal.SIGTERM, self._signal_handler)
81
+ except (ValueError, OSError):
82
+ # Signal handlers may not be available in all environments
83
+ pass
84
+
85
+ def _signal_handler(self, signum, frame):
86
+ """Handle shutdown signals."""
87
+ # Schedule flush on next event loop iteration
88
+ if asyncio.get_event_loop().is_running():
89
+ asyncio.create_task(self.flush(True))
90
+
91
+ async def add(self, entry: LogEntry) -> None:
92
+ """
93
+ Add log entry to queue.
94
+
95
+ Automatically flushes if batch size is reached.
96
+
97
+ Args:
98
+ entry: LogEntry to add to queue
99
+ """
100
+ self.queue.append(QueuedLogEntry(entry, self._current_timestamp()))
101
+
102
+ # Flush if batch size reached
103
+ if len(self.queue) >= self.batch_size:
104
+ await self.flush(False)
105
+ return
106
+
107
+ # Setup flush timer if not already set
108
+ if self.flush_timer is None and len(self.queue) > 0:
109
+ self.flush_timer = asyncio.create_task(self._schedule_flush())
110
+
111
+ async def _schedule_flush(self) -> None:
112
+ """Schedule automatic flush after batch interval."""
113
+ try:
114
+ await asyncio.sleep(self.batch_interval / 1000.0) # Convert ms to seconds
115
+ await self.flush(False)
116
+ except asyncio.CancelledError:
117
+ # Timer was cancelled, ignore
118
+ pass
119
+ finally:
120
+ self.flush_timer = None
121
+
122
+ def _current_timestamp(self) -> int:
123
+ """Get current timestamp in milliseconds."""
124
+ import time
125
+
126
+ return int(time.time() * 1000)
127
+
128
+ async def flush(self, sync: bool = False) -> None:
129
+ """
130
+ Flush queued logs.
131
+
132
+ Args:
133
+ sync: If True, wait for flush to complete (for shutdown)
134
+ """
135
+ if self.is_flushing:
136
+ return
137
+
138
+ # Cancel flush timer
139
+ if self.flush_timer:
140
+ self.flush_timer.cancel()
141
+ try:
142
+ await self.flush_timer
143
+ except asyncio.CancelledError:
144
+ pass
145
+ self.flush_timer = None
146
+
147
+ if len(self.queue) == 0:
148
+ return
149
+
150
+ self.is_flushing = True
151
+
152
+ try:
153
+ entries = self.queue[:] # Copy queue
154
+ self.queue.clear() # Clear queue
155
+
156
+ if len(entries) == 0:
157
+ self.is_flushing = False
158
+ return
159
+
160
+ log_entries = [e.entry for e in entries]
161
+
162
+ # Try Redis first (if available)
163
+ if self.redis.is_connected():
164
+ queue_name = f"audit-logs:{self.config.client_id}"
165
+ # Serialize all entries as JSON array
166
+ import json
167
+
168
+ entries_json = json.dumps([entry.model_dump() for entry in log_entries])
169
+ success = await self.redis.rpush(queue_name, entries_json)
170
+
171
+ if success:
172
+ self.is_flushing = False
173
+ return # Successfully queued in Redis
174
+
175
+ # Check circuit breaker before attempting HTTP logging
176
+ if self.circuit_breaker.is_open():
177
+ # Circuit is open, skip HTTP logging to prevent infinite retry loops
178
+ self.is_flushing = False
179
+ return
180
+
181
+ # Fallback to HTTP batch endpoint
182
+ try:
183
+ await self.http_client.request(
184
+ "POST",
185
+ "/api/v1/logs/batch",
186
+ {
187
+ "logs": [
188
+ entry.model_dump(
189
+ exclude={"environment", "application"}, exclude_none=True
190
+ )
191
+ for entry in log_entries
192
+ ]
193
+ },
194
+ )
195
+ # Record success in circuit breaker
196
+ self.circuit_breaker.record_success()
197
+ except Exception:
198
+ # Failed to send logs - record failure in circuit breaker
199
+ self.circuit_breaker.record_failure()
200
+ # Silently fail to avoid infinite loops
201
+ pass
202
+ except Exception:
203
+ # Silently swallow errors - never break logging
204
+ pass
205
+ finally:
206
+ self.is_flushing = False
207
+
208
+ def get_queue_size(self) -> int:
209
+ """
210
+ Get current queue size.
211
+
212
+ Returns:
213
+ Number of entries in queue
214
+ """
215
+ return len(self.queue)
216
+
217
+ def clear(self) -> None:
218
+ """Clear queue (for testing/cleanup)."""
219
+ if self.flush_timer:
220
+ self.flush_timer.cancel()
221
+ self.flush_timer = None
222
+ self.queue.clear()
@@ -0,0 +1,88 @@
1
+ """
2
+ Authentication strategy handler utility.
3
+
4
+ This module provides utilities for managing authentication strategies with
5
+ priority-based fallback support.
6
+ """
7
+
8
+ from typing import Dict, Optional
9
+
10
+ from ..models.config import AuthMethod, AuthStrategy
11
+
12
+
13
+ class AuthStrategyHandler:
14
+ """Handler for authentication strategies with priority-based fallback."""
15
+
16
+ @staticmethod
17
+ def build_auth_headers(
18
+ method: AuthMethod,
19
+ strategy: AuthStrategy,
20
+ client_token: Optional[str] = None,
21
+ ) -> Dict[str, str]:
22
+ """
23
+ Build authentication headers for a specific auth method.
24
+
25
+ Args:
26
+ method: Authentication method to use
27
+ strategy: Auth strategy configuration
28
+ client_token: Optional client token (for client-token and client-credentials methods)
29
+
30
+ Returns:
31
+ Dictionary of headers to add to the request
32
+
33
+ Raises:
34
+ ValueError: If required credentials are missing for the method
35
+ """
36
+ headers: Dict[str, str] = {}
37
+
38
+ if method == "bearer":
39
+ if not strategy.bearerToken:
40
+ raise ValueError("bearerToken is required for bearer authentication method")
41
+ headers["Authorization"] = f"Bearer {strategy.bearerToken}"
42
+
43
+ elif method == "client-token":
44
+ if not client_token:
45
+ raise ValueError("client_token is required for client-token authentication method")
46
+ headers["x-client-token"] = client_token
47
+
48
+ elif method == "client-credentials":
49
+ # Client credentials uses the same client token mechanism
50
+ # The client token is already automatically sent via _ensure_client_token
51
+ # This method is mainly for strategy ordering
52
+ if not client_token:
53
+ raise ValueError(
54
+ "client_token is required for client-credentials authentication method"
55
+ )
56
+ headers["x-client-token"] = client_token
57
+
58
+ elif method == "api-key":
59
+ if not strategy.apiKey:
60
+ raise ValueError("apiKey is required for api-key authentication method")
61
+ # API key is sent as Bearer token (same format as bearer tokens)
62
+ headers["Authorization"] = f"Bearer {strategy.apiKey}"
63
+
64
+ return headers
65
+
66
+ @staticmethod
67
+ def should_try_method(method: AuthMethod, strategy: AuthStrategy) -> bool:
68
+ """
69
+ Check if a method should be tried based on the strategy.
70
+
71
+ Args:
72
+ method: Authentication method to check
73
+ strategy: Auth strategy configuration
74
+
75
+ Returns:
76
+ True if method should be tried, False otherwise
77
+ """
78
+ return method in strategy.methods
79
+
80
+ @staticmethod
81
+ def get_default_strategy() -> AuthStrategy:
82
+ """
83
+ Get default authentication strategy.
84
+
85
+ Returns:
86
+ Default AuthStrategy with ['bearer', 'client-token'] methods
87
+ """
88
+ return AuthStrategy(methods=["bearer", "client-token"])
@@ -0,0 +1,65 @@
1
+ """
2
+ Authentication utilities for shared use across services.
3
+
4
+ This module provides shared authentication utilities to avoid code duplication
5
+ across service classes.
6
+ """
7
+
8
+ from typing import TYPE_CHECKING, Any, Dict, Optional
9
+
10
+ from ..models.config import AuthStrategy
11
+ from ..utils.http_client import HttpClient
12
+
13
+ if TYPE_CHECKING:
14
+ from ..api import ApiClient
15
+
16
+
17
+ async def validate_token_request(
18
+ token: str,
19
+ http_client: HttpClient,
20
+ api_client: Optional["ApiClient"] = None,
21
+ auth_strategy: Optional[AuthStrategy] = None,
22
+ ) -> Dict[str, Any]:
23
+ """
24
+ Helper function to call /api/v1/auth/validate endpoint with proper request body.
25
+
26
+ Shared utility for RoleService and PermissionService to avoid code duplication.
27
+
28
+ Args:
29
+ token: JWT token to validate
30
+ http_client: HTTP client instance (for backward compatibility)
31
+ api_client: Optional API client instance (for typed API calls)
32
+ auth_strategy: Optional authentication strategy
33
+
34
+ Returns:
35
+ Validation result dictionary
36
+ """
37
+ if api_client:
38
+ # Use ApiClient for typed API calls
39
+ response = await api_client.auth.validate_token(token, auth_strategy=auth_strategy)
40
+ # Extract data from typed response
41
+ return {
42
+ "success": response.success,
43
+ "data": {
44
+ "authenticated": response.data.authenticated,
45
+ "user": response.data.user.model_dump() if response.data.user else None,
46
+ "expiresAt": response.data.expiresAt,
47
+ },
48
+ "timestamp": response.timestamp,
49
+ }
50
+ else:
51
+ # Fallback to HttpClient for backward compatibility
52
+ if auth_strategy is not None:
53
+ result = await http_client.authenticated_request(
54
+ "POST",
55
+ "/api/v1/auth/validate",
56
+ token,
57
+ {"token": token},
58
+ auth_strategy=auth_strategy,
59
+ )
60
+ return result # type: ignore[no-any-return]
61
+ else:
62
+ result = await http_client.authenticated_request(
63
+ "POST", "/api/v1/auth/validate", token, {"token": token}
64
+ )
65
+ return result # type: ignore[no-any-return]
@@ -0,0 +1,125 @@
1
+ """
2
+ Circuit breaker implementation for HTTP logging.
3
+
4
+ Prevents infinite retry loops when logging service is unavailable by opening
5
+ the circuit after consecutive failures and resetting after a timeout period.
6
+ """
7
+
8
+ import time
9
+ from enum import Enum
10
+ from typing import Optional
11
+
12
+ from ..models.config import CircuitBreakerConfig
13
+
14
+
15
+ class CircuitState(Enum):
16
+ """Circuit breaker state."""
17
+
18
+ CLOSED = "CLOSED" # Normal operation, requests allowed
19
+ OPEN = "OPEN" # Circuit open, requests blocked
20
+ HALF_OPEN = "HALF_OPEN" # Testing if service recovered
21
+
22
+
23
+ class CircuitBreaker:
24
+ """
25
+ Circuit breaker for HTTP logging.
26
+
27
+ Prevents infinite retry loops when logging service is unavailable.
28
+ Opens circuit after consecutive failures and resets after timeout period.
29
+
30
+ Attributes:
31
+ failure_threshold: Number of consecutive failures before opening circuit
32
+ reset_timeout: Seconds to wait before resetting circuit
33
+ state: Current circuit state
34
+ failure_count: Current consecutive failure count
35
+ last_failure_time: Timestamp of last failure
36
+ opened_at: Timestamp when circuit was opened
37
+ """
38
+
39
+ def __init__(self, config: Optional[CircuitBreakerConfig] = None):
40
+ """
41
+ Initialize circuit breaker with configuration.
42
+
43
+ Args:
44
+ config: Circuit breaker configuration (optional)
45
+ """
46
+ if config:
47
+ self.failure_threshold = config.failureThreshold or 3
48
+ self.reset_timeout = config.resetTimeout or 60
49
+ else:
50
+ self.failure_threshold = 3
51
+ self.reset_timeout = 60
52
+
53
+ self.state = CircuitState.CLOSED
54
+ self.failure_count = 0
55
+ self.last_failure_time: Optional[float] = None
56
+ self.opened_at: Optional[float] = None
57
+
58
+ def is_open(self) -> bool:
59
+ """
60
+ Check if circuit is open (requests should be blocked).
61
+
62
+ Automatically transitions from OPEN to HALF_OPEN after reset timeout.
63
+
64
+ Returns:
65
+ True if circuit is open, False otherwise
66
+ """
67
+ if self.state == CircuitState.CLOSED:
68
+ return False
69
+
70
+ if self.state == CircuitState.OPEN:
71
+ # Check if reset timeout has passed
72
+ if self.opened_at and (time.time() - self.opened_at) >= self.reset_timeout:
73
+ # Transition to HALF_OPEN to test if service recovered
74
+ self.state = CircuitState.HALF_OPEN
75
+ self.failure_count = 0
76
+ return False
77
+ return True
78
+
79
+ # HALF_OPEN state - allow requests to test recovery
80
+ return False
81
+
82
+ def record_success(self) -> None:
83
+ """
84
+ Record successful request.
85
+
86
+ Resets failure count and closes circuit if it was open.
87
+ """
88
+ if self.state == CircuitState.HALF_OPEN:
89
+ # Service recovered, close circuit
90
+ self.state = CircuitState.CLOSED
91
+ self.failure_count = 0
92
+ self.opened_at = None
93
+ elif self.state == CircuitState.CLOSED:
94
+ # Reset failure count on success
95
+ self.failure_count = 0
96
+
97
+ def record_failure(self) -> None:
98
+ """
99
+ Record failed request.
100
+
101
+ Increments failure count and opens circuit if threshold reached.
102
+ """
103
+ self.failure_count += 1
104
+ self.last_failure_time = time.time()
105
+
106
+ if self.failure_count >= self.failure_threshold:
107
+ # Open circuit
108
+ self.state = CircuitState.OPEN
109
+ self.opened_at = time.time()
110
+
111
+ def reset(self) -> None:
112
+ """Reset circuit breaker to initial state."""
113
+ self.state = CircuitState.CLOSED
114
+ self.failure_count = 0
115
+ self.last_failure_time = None
116
+ self.opened_at = None
117
+
118
+ def get_state(self) -> CircuitState:
119
+ """
120
+ Get current circuit state.
121
+
122
+ Returns:
123
+ Current circuit state
124
+ """
125
+ return self.state