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.

@@ -5,13 +5,14 @@ This module contains Pydantic models that define the configuration structure
5
5
  and data types used throughout the MisoClient SDK.
6
6
  """
7
7
 
8
- from typing import Optional, Dict, Any, List, Literal
8
+ from typing import Any, Dict, List, Literal, Optional
9
+
9
10
  from pydantic import BaseModel, Field
10
11
 
11
12
 
12
13
  class RedisConfig(BaseModel):
13
14
  """Redis connection configuration."""
14
-
15
+
15
16
  host: str = Field(..., description="Redis host")
16
17
  port: int = Field(default=6379, description="Redis port")
17
18
  password: Optional[str] = Field(default=None, description="Redis password")
@@ -21,38 +22,42 @@ class RedisConfig(BaseModel):
21
22
 
22
23
  class MisoClientConfig(BaseModel):
23
24
  """Main MisoClient configuration.
24
-
25
+
25
26
  Required fields:
26
27
  - controller_url: Miso Controller base URL
27
28
  - client_id: Client identifier for authentication
28
29
  - client_secret: Client secret for authentication
29
-
30
+
30
31
  Optional fields:
31
32
  - redis: Redis configuration for caching
32
33
  - log_level: Logging level (debug, info, warn, error)
33
34
  - cache: Cache TTL settings for roles and permissions
35
+ - api_key: API key for testing (bypasses OAuth2 authentication)
34
36
  """
35
-
37
+
36
38
  controller_url: str = Field(..., description="Miso Controller base URL")
37
39
  client_id: str = Field(..., description="Client identifier for authentication")
38
40
  client_secret: str = Field(..., description="Client secret for authentication")
39
41
  redis: Optional[RedisConfig] = Field(default=None, description="Optional Redis configuration")
40
42
  log_level: Literal["debug", "info", "warn", "error"] = Field(
41
- default="info",
42
- description="Log level"
43
+ default="info", description="Log level"
43
44
  )
44
45
  cache: Optional[Dict[str, int]] = Field(
45
46
  default=None,
46
- description="Cache TTL settings: permission_ttl, role_ttl (default: 900 seconds)"
47
+ description="Cache TTL settings: permission_ttl, role_ttl (default: 900 seconds)",
47
48
  )
48
-
49
+ api_key: Optional[str] = Field(
50
+ default=None,
51
+ description="API key for testing - when set, bearer tokens matching this key bypass OAuth2 validation",
52
+ )
53
+
49
54
  @property
50
55
  def role_ttl(self) -> int:
51
56
  """Get role cache TTL in seconds."""
52
57
  if self.cache and "role_ttl" in self.cache:
53
58
  return self.cache["role_ttl"]
54
59
  return self.cache.get("roleTTL", 900) if self.cache else 900 # 15 minutes default
55
-
60
+
56
61
  @property
57
62
  def permission_ttl(self) -> int:
58
63
  """Get permission cache TTL in seconds."""
@@ -63,21 +68,21 @@ class MisoClientConfig(BaseModel):
63
68
 
64
69
  class UserInfo(BaseModel):
65
70
  """User information from token validation."""
66
-
71
+
67
72
  id: str = Field(..., description="User ID")
68
73
  username: str = Field(..., description="Username")
69
74
  email: Optional[str] = Field(default=None, description="User email")
70
75
  firstName: Optional[str] = Field(default=None, alias="first_name", description="First name")
71
76
  lastName: Optional[str] = Field(default=None, alias="last_name", description="Last name")
72
77
  roles: Optional[List[str]] = Field(default=None, description="User roles")
73
-
78
+
74
79
  class Config:
75
80
  populate_by_name = True # Allow both snake_case and camelCase
76
81
 
77
82
 
78
83
  class AuthResult(BaseModel):
79
84
  """Authentication result."""
80
-
85
+
81
86
  authenticated: bool = Field(..., description="Whether authentication was successful")
82
87
  user: Optional[UserInfo] = Field(default=None, description="User information if authenticated")
83
88
  error: Optional[str] = Field(default=None, description="Error message if authentication failed")
@@ -85,90 +90,106 @@ class AuthResult(BaseModel):
85
90
 
86
91
  class LogEntry(BaseModel):
87
92
  """Log entry structure."""
88
-
93
+
89
94
  timestamp: str = Field(..., description="ISO timestamp")
90
95
  level: Literal["error", "audit", "info", "debug"] = Field(..., description="Log level")
91
96
  environment: str = Field(..., description="Environment name (extracted by backend)")
92
97
  application: str = Field(..., description="Application identifier (clientId)")
93
- applicationId: Optional[str] = Field(default=None, alias="application_id", description="Application ID")
98
+ applicationId: Optional[str] = Field(
99
+ default=None, alias="application_id", description="Application ID"
100
+ )
94
101
  userId: Optional[str] = Field(default=None, alias="user_id", description="User ID if available")
95
102
  message: str = Field(..., description="Log message")
96
103
  context: Optional[Dict[str, Any]] = Field(default=None, description="Additional context")
97
- correlationId: Optional[str] = Field(default=None, alias="correlation_id", description="Correlation ID for tracing")
104
+ correlationId: Optional[str] = Field(
105
+ default=None, alias="correlation_id", description="Correlation ID for tracing"
106
+ )
98
107
  requestId: Optional[str] = Field(default=None, alias="request_id", description="Request ID")
99
108
  sessionId: Optional[str] = Field(default=None, alias="session_id", description="Session ID")
100
- stackTrace: Optional[str] = Field(default=None, alias="stack_trace", description="Stack trace for errors")
109
+ stackTrace: Optional[str] = Field(
110
+ default=None, alias="stack_trace", description="Stack trace for errors"
111
+ )
101
112
  ipAddress: Optional[str] = Field(default=None, alias="ip_address", description="IP address")
102
113
  userAgent: Optional[str] = Field(default=None, alias="user_agent", description="User agent")
103
114
  hostname: Optional[str] = Field(default=None, description="Hostname")
104
-
115
+
105
116
  class Config:
106
117
  populate_by_name = True
107
118
 
108
119
 
109
120
  class RoleResult(BaseModel):
110
121
  """Role query result."""
111
-
122
+
112
123
  userId: str = Field(..., alias="user_id", description="User ID")
113
124
  roles: List[str] = Field(..., description="List of user roles")
114
125
  environment: str = Field(..., description="Environment name")
115
126
  application: str = Field(..., description="Application name")
116
-
127
+
117
128
  class Config:
118
129
  populate_by_name = True
119
130
 
120
131
 
121
132
  class PermissionResult(BaseModel):
122
133
  """Permission query result."""
123
-
134
+
124
135
  userId: str = Field(..., alias="user_id", description="User ID")
125
136
  permissions: List[str] = Field(..., description="List of user permissions")
126
137
  environment: str = Field(..., description="Environment name")
127
138
  application: str = Field(..., description="Application name")
128
-
139
+
129
140
  class Config:
130
141
  populate_by_name = True
131
142
 
132
143
 
133
144
  class ClientTokenResponse(BaseModel):
134
145
  """Client token response."""
135
-
146
+
136
147
  success: bool = Field(..., description="Whether token request was successful")
137
148
  token: str = Field(..., description="Client token")
138
149
  expiresIn: int = Field(..., alias="expires_in", description="Token expiration in seconds")
139
150
  expiresAt: str = Field(..., alias="expires_at", description="Token expiration ISO timestamp")
140
-
151
+
141
152
  class Config:
142
153
  populate_by_name = True
143
154
 
144
155
 
145
156
  class PerformanceMetrics(BaseModel):
146
157
  """Performance metrics for logging."""
147
-
158
+
148
159
  startTime: int = Field(..., alias="start_time", description="Start time in milliseconds")
149
- endTime: Optional[int] = Field(default=None, alias="end_time", description="End time in milliseconds")
160
+ endTime: Optional[int] = Field(
161
+ default=None, alias="end_time", description="End time in milliseconds"
162
+ )
150
163
  duration: Optional[int] = Field(default=None, description="Duration in milliseconds")
151
164
  memoryUsage: Optional[Dict[str, int]] = Field(
152
- default=None,
165
+ default=None,
153
166
  alias="memory_usage",
154
- description="Memory usage metrics (rss, heapTotal, heapUsed, external, arrayBuffers)"
167
+ description="Memory usage metrics (rss, heapTotal, heapUsed, external, arrayBuffers)",
155
168
  )
156
-
169
+
157
170
  class Config:
158
171
  populate_by_name = True
159
172
 
160
173
 
161
174
  class ClientLoggingOptions(BaseModel):
162
175
  """Options for client logging."""
163
-
164
- applicationId: Optional[str] = Field(default=None, alias="application_id", description="Application ID")
176
+
177
+ applicationId: Optional[str] = Field(
178
+ default=None, alias="application_id", description="Application ID"
179
+ )
165
180
  userId: Optional[str] = Field(default=None, alias="user_id", description="User ID")
166
- correlationId: Optional[str] = Field(default=None, alias="correlation_id", description="Correlation ID")
181
+ correlationId: Optional[str] = Field(
182
+ default=None, alias="correlation_id", description="Correlation ID"
183
+ )
167
184
  requestId: Optional[str] = Field(default=None, alias="request_id", description="Request ID")
168
185
  sessionId: Optional[str] = Field(default=None, alias="session_id", description="Session ID")
169
186
  token: Optional[str] = Field(default=None, description="JWT token for context extraction")
170
- maskSensitiveData: Optional[bool] = Field(default=None, alias="mask_sensitive_data", description="Enable data masking")
171
- performanceMetrics: Optional[bool] = Field(default=None, alias="performance_metrics", description="Include performance metrics")
172
-
187
+ maskSensitiveData: Optional[bool] = Field(
188
+ default=None, alias="mask_sensitive_data", description="Enable data masking"
189
+ )
190
+ performanceMetrics: Optional[bool] = Field(
191
+ default=None, alias="performance_metrics", description="Include performance metrics"
192
+ )
193
+
173
194
  class Config:
174
195
  populate_by_name = True
@@ -1,12 +1,12 @@
1
1
  """Service implementations for MisoClient SDK."""
2
2
 
3
3
  from .auth import AuthService
4
- from .role import RoleService
4
+ from .cache import CacheService
5
+ from .encryption import EncryptionService
6
+ from .logger import LoggerChain, LoggerService
5
7
  from .permission import PermissionService
6
- from .logger import LoggerService, LoggerChain
7
8
  from .redis import RedisService
8
- from .encryption import EncryptionService
9
- from .cache import CacheService
9
+ from .role import RoleService
10
10
 
11
11
  __all__ = [
12
12
  "AuthService",
@@ -17,4 +17,4 @@ __all__ = [
17
17
  "RedisService",
18
18
  "EncryptionService",
19
19
  "CacheService",
20
- ]
20
+ ]
@@ -6,18 +6,19 @@ token validation, user information retrieval, and logout functionality.
6
6
  """
7
7
 
8
8
  from typing import Optional
9
- from ..models.config import UserInfo, AuthResult
9
+
10
+ from ..models.config import AuthResult, UserInfo
10
11
  from ..services.redis import RedisService
11
12
  from ..utils.http_client import HttpClient
12
13
 
13
14
 
14
15
  class AuthService:
15
16
  """Authentication service for token validation and user management."""
16
-
17
+
17
18
  def __init__(self, http_client: HttpClient, redis: RedisService):
18
19
  """
19
20
  Initialize authentication service.
20
-
21
+
21
22
  Args:
22
23
  http_client: HTTP client instance
23
24
  redis: Redis service instance
@@ -25,117 +26,132 @@ class AuthService:
25
26
  self.config = http_client.config
26
27
  self.http_client = http_client
27
28
  self.redis = redis
28
-
29
+
29
30
  async def get_environment_token(self) -> str:
30
31
  """
31
32
  Get environment token using client credentials.
32
-
33
+
33
34
  This is called automatically by HttpClient, but can be called manually if needed.
34
-
35
+
35
36
  Returns:
36
37
  Client token string
37
-
38
+
38
39
  Raises:
39
40
  AuthenticationError: If token fetch fails
40
41
  """
41
42
  return await self.http_client.get_environment_token()
42
-
43
+
43
44
  def login(self, redirect_uri: str) -> str:
44
45
  """
45
46
  Initiate login flow by redirecting to controller.
46
-
47
+
47
48
  Returns the login URL for browser redirect or manual navigation.
48
49
  Backend will extract environment and application from client token.
49
-
50
+
50
51
  Args:
51
52
  redirect_uri: URI to redirect to after successful login
52
-
53
+
53
54
  Returns:
54
55
  Login URL string
55
56
  """
56
57
  return f"{self.config.controller_url}/api/auth/login?redirect={redirect_uri}"
57
-
58
+
58
59
  async def validate_token(self, token: str) -> bool:
59
60
  """
60
61
  Validate token with controller.
61
-
62
+
63
+ If API_KEY is configured and token matches it, bypasses OAuth2 validation.
64
+
62
65
  Args:
63
- token: JWT token to validate
64
-
66
+ token: JWT token to validate (or API_KEY for testing)
67
+
65
68
  Returns:
66
69
  True if token is valid, False otherwise
67
70
  """
71
+ # Check API_KEY first (for testing)
72
+ if self.config.api_key and token == self.config.api_key:
73
+ return True
74
+
75
+ # Fall back to OAuth2 validation
68
76
  try:
69
77
  result = await self.http_client.authenticated_request(
70
- "POST",
71
- "/api/auth/validate", # Backend knows app/env from client token
72
- token
78
+ "POST", "/api/auth/validate", token # Backend knows app/env from client token
73
79
  )
74
-
80
+
75
81
  auth_result = AuthResult(**result)
76
82
  return auth_result.authenticated
77
-
83
+
78
84
  except Exception:
79
85
  # Token validation failed, return false
80
86
  return False
81
-
87
+
82
88
  async def get_user(self, token: str) -> Optional[UserInfo]:
83
89
  """
84
90
  Get user information from token.
85
-
91
+
92
+ If API_KEY is configured and token matches it, returns None (no user info for API key auth).
93
+
86
94
  Args:
87
- token: JWT token
88
-
95
+ token: JWT token (or API_KEY for testing)
96
+
89
97
  Returns:
90
98
  UserInfo if token is valid, None otherwise
91
99
  """
100
+ # Check API_KEY first (for testing)
101
+ if self.config.api_key and token == self.config.api_key:
102
+ # API key authentication doesn't provide user info
103
+ return None
104
+
105
+ # Fall back to OAuth2 validation
92
106
  try:
93
107
  result = await self.http_client.authenticated_request(
94
- "POST",
95
- "/api/auth/validate",
96
- token
108
+ "POST", "/api/auth/validate", token
97
109
  )
98
-
110
+
99
111
  auth_result = AuthResult(**result)
100
-
112
+
101
113
  if auth_result.authenticated and auth_result.user:
102
114
  return auth_result.user
103
-
115
+
104
116
  return None
105
-
117
+
106
118
  except Exception:
107
119
  # Failed to get user info, return null
108
120
  return None
109
-
121
+
110
122
  async def get_user_info(self, token: str) -> Optional[UserInfo]:
111
123
  """
112
124
  Get user information from GET /api/auth/user endpoint.
113
-
125
+
126
+ If API_KEY is configured and token matches it, returns None (no user info for API key auth).
127
+
114
128
  Args:
115
- token: JWT token
116
-
129
+ token: JWT token (or API_KEY for testing)
130
+
117
131
  Returns:
118
132
  UserInfo if token is valid, None otherwise
119
133
  """
134
+ # Check API_KEY first (for testing)
135
+ if self.config.api_key and token == self.config.api_key:
136
+ # API key authentication doesn't provide user info
137
+ return None
138
+
139
+ # Fall back to OAuth2 validation
120
140
  try:
121
- user_data = await self.http_client.authenticated_request(
122
- "GET",
123
- "/api/auth/user",
124
- token
125
- )
126
-
141
+ user_data = await self.http_client.authenticated_request("GET", "/api/auth/user", token)
142
+
127
143
  return UserInfo(**user_data)
128
-
144
+
129
145
  except Exception:
130
146
  # Failed to get user info, return None
131
147
  return None
132
-
148
+
133
149
  async def logout(self) -> None:
134
150
  """
135
151
  Logout user.
136
-
152
+
137
153
  Backend extracts app/env from client token (no body needed).
138
-
154
+
139
155
  Raises:
140
156
  MisoClientError: If logout fails
141
157
  """
@@ -145,15 +161,16 @@ class AuthService:
145
161
  except Exception as e:
146
162
  # Logout failed, re-raise error for application to handle
147
163
  from ..errors import MisoClientError
164
+
148
165
  raise MisoClientError(f"Logout failed: {str(e)}")
149
-
166
+
150
167
  async def is_authenticated(self, token: str) -> bool:
151
168
  """
152
169
  Check if user is authenticated (has valid token).
153
-
170
+
154
171
  Args:
155
172
  token: JWT token
156
-
173
+
157
174
  Returns:
158
175
  True if user is authenticated, False otherwise
159
176
  """
@@ -8,17 +8,18 @@ in-memory TTL-based caching when Redis is unavailable.
8
8
 
9
9
  import json
10
10
  import time
11
- from typing import Any, Optional, Dict, Tuple
11
+ from typing import Any, Dict, Optional, Tuple
12
+
12
13
  from ..services.redis import RedisService
13
14
 
14
15
 
15
16
  class CacheService:
16
17
  """Cache service with Redis and in-memory TTL fallback."""
17
-
18
+
18
19
  def __init__(self, redis: Optional[RedisService] = None):
19
20
  """
20
21
  Initialize cache service.
21
-
22
+
22
23
  Args:
23
24
  redis: Optional RedisService instance. If provided, Redis will be used
24
25
  as primary cache with in-memory as fallback. If None, only
@@ -29,31 +30,32 @@ class CacheService:
29
30
  self._memory_cache: Dict[str, Tuple[Any, float]] = {}
30
31
  # Cleanup threshold: clean expired entries if cache exceeds this size
31
32
  self._cleanup_threshold = 1000
32
-
33
+
33
34
  def _is_expired(self, expiration: float) -> bool:
34
35
  """Check if entry has expired."""
35
36
  return time.time() > expiration
36
-
37
+
37
38
  def _cleanup_expired(self) -> None:
38
39
  """Remove expired entries from memory cache."""
39
40
  if len(self._memory_cache) <= self._cleanup_threshold:
40
41
  return
41
-
42
+
42
43
  expired_keys = [
43
- key for key, (_, expiration) in self._memory_cache.items()
44
+ key
45
+ for key, (_, expiration) in self._memory_cache.items()
44
46
  if self._is_expired(expiration)
45
47
  ]
46
-
48
+
47
49
  for key in expired_keys:
48
50
  del self._memory_cache[key]
49
-
51
+
50
52
  def _serialize_value(self, value: Any) -> str:
51
53
  """
52
54
  Serialize value to JSON string.
53
-
55
+
54
56
  Args:
55
57
  value: Value to serialize (can be any JSON-serializable type)
56
-
58
+
57
59
  Returns:
58
60
  JSON string representation
59
61
  """
@@ -62,23 +64,23 @@ class CacheService:
62
64
  if isinstance(value, str):
63
65
  return value
64
66
  return json.dumps(value)
65
-
67
+
66
68
  # For complex types, use JSON serialization with a marker
67
69
  return json.dumps({"__cached_value__": value})
68
-
70
+
69
71
  def _deserialize_value(self, value_str: str) -> Any:
70
72
  """
71
73
  Deserialize JSON string back to original value.
72
-
74
+
73
75
  Args:
74
76
  value_str: JSON string to deserialize
75
-
77
+
76
78
  Returns:
77
79
  Deserialized value
78
80
  """
79
81
  if not value_str:
80
82
  return None
81
-
83
+
82
84
  try:
83
85
  # Try to parse as JSON
84
86
  parsed = json.loads(value_str)
@@ -90,16 +92,16 @@ class CacheService:
90
92
  except (json.JSONDecodeError, TypeError):
91
93
  # If JSON parsing fails, assume it's a plain string
92
94
  return value_str
93
-
95
+
94
96
  async def get(self, key: str) -> Optional[Any]:
95
97
  """
96
98
  Get cached value.
97
-
99
+
98
100
  Checks Redis first (if available), then falls back to in-memory cache.
99
-
101
+
100
102
  Args:
101
103
  key: Cache key
102
-
104
+
103
105
  Returns:
104
106
  Cached value if found, None otherwise
105
107
  """
@@ -112,7 +114,7 @@ class CacheService:
112
114
  except Exception:
113
115
  # Redis operation failed, fall through to memory cache
114
116
  pass
115
-
117
+
116
118
  # Fallback to in-memory cache
117
119
  if key in self._memory_cache:
118
120
  value, expiration = self._memory_cache[key]
@@ -121,26 +123,26 @@ class CacheService:
121
123
  else:
122
124
  # Entry expired, remove it
123
125
  del self._memory_cache[key]
124
-
126
+
125
127
  return None
126
-
128
+
127
129
  async def set(self, key: str, value: Any, ttl: int) -> bool:
128
130
  """
129
131
  Set cached value with TTL.
130
-
132
+
131
133
  Stores in both Redis (if available) and in-memory cache.
132
-
134
+
133
135
  Args:
134
136
  key: Cache key
135
137
  value: Value to cache (any JSON-serializable type)
136
138
  ttl: Time to live in seconds
137
-
139
+
138
140
  Returns:
139
141
  True if successful, False otherwise
140
142
  """
141
143
  serialized_value = self._serialize_value(value)
142
144
  success = False
143
-
145
+
144
146
  # Store in Redis if available
145
147
  if self.redis and self.redis.is_connected():
146
148
  try:
@@ -148,57 +150,56 @@ class CacheService:
148
150
  except Exception:
149
151
  # Redis operation failed, continue to memory cache
150
152
  pass
151
-
153
+
152
154
  # Also store in memory cache
153
155
  expiration = time.time() + ttl
154
156
  self._memory_cache[key] = (value, expiration)
155
-
157
+
156
158
  # Cleanup expired entries periodically
157
159
  self._cleanup_expired()
158
-
160
+
159
161
  return success or True # Return True if at least memory cache succeeded
160
-
162
+
161
163
  async def delete(self, key: str) -> bool:
162
164
  """
163
165
  Delete cached value.
164
-
166
+
165
167
  Deletes from both Redis (if available) and in-memory cache.
166
-
168
+
167
169
  Args:
168
170
  key: Cache key
169
-
171
+
170
172
  Returns:
171
173
  True if deleted from at least one cache, False otherwise
172
174
  """
173
175
  deleted = False
174
-
176
+
175
177
  # Delete from Redis if available
176
178
  if self.redis and self.redis.is_connected():
177
179
  try:
178
180
  deleted = await self.redis.delete(key)
179
181
  except Exception:
180
182
  pass
181
-
183
+
182
184
  # Delete from memory cache
183
185
  if key in self._memory_cache:
184
186
  del self._memory_cache[key]
185
187
  deleted = True
186
-
188
+
187
189
  return deleted
188
-
190
+
189
191
  async def clear(self) -> None:
190
192
  """
191
193
  Clear all cached values.
192
-
194
+
193
195
  Clears both Redis (if available) and in-memory cache.
194
196
  Note: Redis clear operation only clears keys with the configured prefix,
195
197
  not the entire Redis database.
196
198
  """
197
199
  # Clear memory cache
198
200
  self._memory_cache.clear()
199
-
201
+
200
202
  # For Redis, we would need to delete all keys with the prefix
201
203
  # This is more complex and potentially dangerous, so we'll skip it
202
204
  # Users should use delete() for specific keys or clear Redis manually
203
205
  # if needed
204
-