miso-client 0.1.0__py3-none-any.whl → 0.4.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
@@ -0,0 +1,41 @@
1
+ """
2
+ Structured error response model following RFC 7807-style format.
3
+
4
+ This module provides a generic error response interface that can be used
5
+ across different applications for consistent error handling.
6
+ """
7
+
8
+ from typing import List, Optional
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+
13
+ class ErrorResponse(BaseModel):
14
+ """
15
+ Structured error response following RFC 7807-style format.
16
+
17
+ This model represents a standardized error response structure that includes:
18
+ - Multiple error messages
19
+ - Error type identifier
20
+ - Human-readable title
21
+ - HTTP status code
22
+ - Request instance URI (optional)
23
+
24
+ Example:
25
+ {
26
+ "errors": ["Error message 1", "Error message 2"],
27
+ "type": "/Errors/Bad Input",
28
+ "title": "Bad Request",
29
+ "statusCode": 400,
30
+ "instance": "/OpenApi/rest/Xzy"
31
+ }
32
+ """
33
+
34
+ errors: List[str] = Field(..., description="List of error messages")
35
+ type: str = Field(..., description="Error type URI (e.g., '/Errors/Bad Input')")
36
+ title: str = Field(..., description="Human-readable error title")
37
+ statusCode: int = Field(..., alias="status_code", description="HTTP status code")
38
+ instance: Optional[str] = Field(default=None, description="Request instance URI")
39
+
40
+ class Config:
41
+ populate_by_name = True # Allow both camelCase and snake_case
@@ -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
  """