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.
- miso_client/__init__.py +523 -130
- miso_client/api/__init__.py +35 -0
- miso_client/api/auth_api.py +367 -0
- miso_client/api/logs_api.py +91 -0
- miso_client/api/permissions_api.py +88 -0
- miso_client/api/roles_api.py +88 -0
- miso_client/api/types/__init__.py +75 -0
- miso_client/api/types/auth_types.py +183 -0
- miso_client/api/types/logs_types.py +71 -0
- miso_client/api/types/permissions_types.py +31 -0
- miso_client/api/types/roles_types.py +31 -0
- miso_client/errors.py +30 -4
- miso_client/models/__init__.py +4 -0
- miso_client/models/config.py +275 -72
- miso_client/models/error_response.py +39 -0
- miso_client/models/filter.py +255 -0
- miso_client/models/pagination.py +44 -0
- miso_client/models/sort.py +25 -0
- miso_client/services/__init__.py +6 -5
- miso_client/services/auth.py +496 -87
- miso_client/services/cache.py +42 -41
- miso_client/services/encryption.py +18 -17
- miso_client/services/logger.py +467 -328
- miso_client/services/logger_chain.py +288 -0
- miso_client/services/permission.py +130 -67
- miso_client/services/redis.py +28 -23
- miso_client/services/role.py +145 -62
- miso_client/utils/__init__.py +3 -3
- miso_client/utils/audit_log_queue.py +222 -0
- miso_client/utils/auth_strategy.py +88 -0
- miso_client/utils/auth_utils.py +65 -0
- miso_client/utils/circuit_breaker.py +125 -0
- miso_client/utils/client_token_manager.py +244 -0
- miso_client/utils/config_loader.py +88 -17
- miso_client/utils/controller_url_resolver.py +80 -0
- miso_client/utils/data_masker.py +104 -33
- miso_client/utils/environment_token.py +126 -0
- miso_client/utils/error_utils.py +216 -0
- miso_client/utils/fastapi_endpoints.py +166 -0
- miso_client/utils/filter.py +364 -0
- miso_client/utils/filter_applier.py +143 -0
- miso_client/utils/filter_parser.py +110 -0
- miso_client/utils/flask_endpoints.py +169 -0
- miso_client/utils/http_client.py +494 -262
- miso_client/utils/http_client_logging.py +352 -0
- miso_client/utils/http_client_logging_helpers.py +197 -0
- miso_client/utils/http_client_query_helpers.py +138 -0
- miso_client/utils/http_error_handler.py +92 -0
- miso_client/utils/http_log_formatter.py +115 -0
- miso_client/utils/http_log_masker.py +203 -0
- miso_client/utils/internal_http_client.py +435 -0
- miso_client/utils/jwt_tools.py +125 -16
- miso_client/utils/logger_helpers.py +206 -0
- miso_client/utils/logging_helpers.py +70 -0
- miso_client/utils/origin_validator.py +128 -0
- miso_client/utils/pagination.py +275 -0
- miso_client/utils/request_context.py +285 -0
- miso_client/utils/sensitive_fields_loader.py +116 -0
- miso_client/utils/sort.py +116 -0
- miso_client/utils/token_utils.py +114 -0
- miso_client/utils/url_validator.py +66 -0
- miso_client/utils/user_token_refresh.py +245 -0
- miso_client-3.7.2.dist-info/METADATA +1021 -0
- miso_client-3.7.2.dist-info/RECORD +68 -0
- miso_client-0.1.0.dist-info/METADATA +0 -551
- miso_client-0.1.0.dist-info/RECORD +0 -23
- {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/WHEEL +0 -0
- {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/licenses/LICENSE +0 -0
- {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Client token manager for InternalHttpClient.
|
|
3
|
+
|
|
4
|
+
This module provides client token management functionality including token fetching,
|
|
5
|
+
caching, and correlation ID extraction.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from ..errors import AuthenticationError, ConnectionError
|
|
15
|
+
from ..models.config import ClientTokenResponse, MisoClientConfig
|
|
16
|
+
from .controller_url_resolver import resolve_controller_url
|
|
17
|
+
from .jwt_tools import decode_token
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ClientTokenManager:
|
|
21
|
+
"""
|
|
22
|
+
Manages client token lifecycle including fetching, caching, and expiration.
|
|
23
|
+
|
|
24
|
+
This class handles all client token operations for InternalHttpClient.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, config: MisoClientConfig):
|
|
28
|
+
"""
|
|
29
|
+
Initialize client token manager.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
config: MisoClient configuration
|
|
33
|
+
"""
|
|
34
|
+
self.config = config
|
|
35
|
+
self.client_token: Optional[str] = None
|
|
36
|
+
self.token_expires_at: Optional[datetime] = None
|
|
37
|
+
self.token_refresh_lock = asyncio.Lock()
|
|
38
|
+
|
|
39
|
+
def extract_correlation_id(self, response: Optional[httpx.Response] = None) -> Optional[str]:
|
|
40
|
+
"""
|
|
41
|
+
Extract correlation ID from response headers.
|
|
42
|
+
|
|
43
|
+
Checks common correlation ID header names.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
response: HTTP response object (optional)
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Correlation ID string if found, None otherwise
|
|
50
|
+
"""
|
|
51
|
+
if not response:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
# Check common correlation ID header names (case-insensitive)
|
|
55
|
+
correlation_headers = [
|
|
56
|
+
"x-correlation-id",
|
|
57
|
+
"x-request-id",
|
|
58
|
+
"correlation-id",
|
|
59
|
+
"correlationId",
|
|
60
|
+
"x-correlationid",
|
|
61
|
+
"request-id",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
for header_name in correlation_headers:
|
|
65
|
+
correlation_id = response.headers.get(header_name) or response.headers.get(
|
|
66
|
+
header_name.lower()
|
|
67
|
+
)
|
|
68
|
+
if correlation_id:
|
|
69
|
+
return str(correlation_id)
|
|
70
|
+
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
async def get_client_token(self) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Get client token, fetching if needed.
|
|
76
|
+
|
|
77
|
+
Proactively refreshes if token will expire within 60 seconds.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Client token string
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
AuthenticationError: If token fetch fails
|
|
84
|
+
"""
|
|
85
|
+
now = datetime.now()
|
|
86
|
+
|
|
87
|
+
# If token exists and not expired (with 60s buffer for proactive refresh), return it
|
|
88
|
+
if (
|
|
89
|
+
self.client_token
|
|
90
|
+
and self.token_expires_at
|
|
91
|
+
and self.token_expires_at > now + timedelta(seconds=60)
|
|
92
|
+
):
|
|
93
|
+
assert self.client_token is not None
|
|
94
|
+
return self.client_token
|
|
95
|
+
|
|
96
|
+
# Acquire lock to prevent concurrent token fetches
|
|
97
|
+
async with self.token_refresh_lock:
|
|
98
|
+
# Double-check after acquiring lock
|
|
99
|
+
if (
|
|
100
|
+
self.client_token
|
|
101
|
+
and self.token_expires_at
|
|
102
|
+
and self.token_expires_at > now + timedelta(seconds=60)
|
|
103
|
+
):
|
|
104
|
+
assert self.client_token is not None
|
|
105
|
+
return self.client_token
|
|
106
|
+
|
|
107
|
+
# Fetch new token
|
|
108
|
+
await self.fetch_client_token()
|
|
109
|
+
assert self.client_token is not None
|
|
110
|
+
return self.client_token
|
|
111
|
+
|
|
112
|
+
async def fetch_client_token(self) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Fetch client token from controller.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
AuthenticationError: If token fetch fails
|
|
118
|
+
"""
|
|
119
|
+
client_id = self.config.client_id
|
|
120
|
+
response: Optional[httpx.Response] = None
|
|
121
|
+
correlation_id: Optional[str] = None
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
# Use resolved URL for temporary client
|
|
125
|
+
resolved_url = resolve_controller_url(self.config)
|
|
126
|
+
# Use a temporary client to avoid interceptor recursion
|
|
127
|
+
temp_client = httpx.AsyncClient(
|
|
128
|
+
base_url=resolved_url,
|
|
129
|
+
timeout=30.0,
|
|
130
|
+
headers={
|
|
131
|
+
"Content-Type": "application/json",
|
|
132
|
+
"x-client-id": client_id,
|
|
133
|
+
"x-client-secret": self.config.client_secret,
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Use configurable client token URI or default
|
|
138
|
+
token_uri = self.config.clientTokenUri or "/api/v1/auth/token"
|
|
139
|
+
response = await temp_client.post(token_uri)
|
|
140
|
+
await temp_client.aclose()
|
|
141
|
+
|
|
142
|
+
# Extract correlation ID from response
|
|
143
|
+
correlation_id = self.extract_correlation_id(response)
|
|
144
|
+
|
|
145
|
+
# OpenAPI spec returns 201 (Created) on success, but accept both 200 and 201 for compatibility
|
|
146
|
+
if response.status_code not in [200, 201]:
|
|
147
|
+
error_msg = f"Failed to get client token: HTTP {response.status_code}"
|
|
148
|
+
if client_id:
|
|
149
|
+
error_msg += f" (clientId: {client_id})"
|
|
150
|
+
if correlation_id:
|
|
151
|
+
error_msg += f" (correlationId: {correlation_id})"
|
|
152
|
+
raise AuthenticationError(error_msg, status_code=response.status_code)
|
|
153
|
+
|
|
154
|
+
data = response.json()
|
|
155
|
+
|
|
156
|
+
# Handle nested response structure (data field)
|
|
157
|
+
# If response has {'success': True, 'data': {...}}, extract data and preserve success
|
|
158
|
+
if isinstance(data, dict) and "data" in data and isinstance(data["data"], dict):
|
|
159
|
+
nested_data = data["data"]
|
|
160
|
+
# Merge success from top level if present
|
|
161
|
+
if "success" in data:
|
|
162
|
+
nested_data["success"] = data["success"]
|
|
163
|
+
data = nested_data
|
|
164
|
+
|
|
165
|
+
# Handle controller response format that may not include all fields
|
|
166
|
+
# Controller may return {'token': '...', 'expiresAt': '...'} without success/expiresIn
|
|
167
|
+
if "token" in data:
|
|
168
|
+
# Default success to True if token is present
|
|
169
|
+
if "success" not in data:
|
|
170
|
+
data["success"] = True
|
|
171
|
+
# Calculate expiresIn from expiresAt if missing
|
|
172
|
+
if "expiresIn" not in data and "expiresAt" in data:
|
|
173
|
+
try:
|
|
174
|
+
expires_at = datetime.fromisoformat(
|
|
175
|
+
data["expiresAt"].replace("Z", "+00:00")
|
|
176
|
+
)
|
|
177
|
+
now = (
|
|
178
|
+
datetime.now(expires_at.tzinfo) if expires_at.tzinfo else datetime.now()
|
|
179
|
+
)
|
|
180
|
+
expires_in = max(0, int((expires_at - now).total_seconds()))
|
|
181
|
+
data["expiresIn"] = expires_in
|
|
182
|
+
except Exception:
|
|
183
|
+
# If parsing fails, default to 1800 seconds (30 minutes)
|
|
184
|
+
data["expiresIn"] = 1800
|
|
185
|
+
|
|
186
|
+
token_response = ClientTokenResponse(**data)
|
|
187
|
+
|
|
188
|
+
if not token_response.success or not token_response.token:
|
|
189
|
+
error_msg = "Failed to get client token: Invalid response"
|
|
190
|
+
if client_id:
|
|
191
|
+
error_msg += f" (clientId: {client_id})"
|
|
192
|
+
if correlation_id:
|
|
193
|
+
error_msg += f" (correlationId: {correlation_id})"
|
|
194
|
+
raise AuthenticationError(error_msg)
|
|
195
|
+
|
|
196
|
+
self.client_token = token_response.token
|
|
197
|
+
|
|
198
|
+
# Calculate expiration: use expiresIn if available, otherwise decode JWT to get exp claim
|
|
199
|
+
expires_in = token_response.expiresIn
|
|
200
|
+
if not expires_in or expires_in <= 0:
|
|
201
|
+
# Try to extract expiration from JWT token
|
|
202
|
+
try:
|
|
203
|
+
decoded = decode_token(token_response.token)
|
|
204
|
+
if decoded and "exp" in decoded and isinstance(decoded["exp"], (int, float)):
|
|
205
|
+
# Calculate expires_in from JWT exp claim
|
|
206
|
+
token_exp = datetime.fromtimestamp(decoded["exp"])
|
|
207
|
+
now = datetime.now()
|
|
208
|
+
expires_in = max(0, int((token_exp - now).total_seconds()))
|
|
209
|
+
else:
|
|
210
|
+
# No expiration found, use default (30 minutes)
|
|
211
|
+
expires_in = 1800
|
|
212
|
+
except Exception:
|
|
213
|
+
# JWT decode failed, use default (30 minutes)
|
|
214
|
+
expires_in = 1800
|
|
215
|
+
|
|
216
|
+
# Set expiration with 30 second buffer before actual expiration
|
|
217
|
+
expires_in = max(0, expires_in - 30)
|
|
218
|
+
self.token_expires_at = datetime.now() + timedelta(seconds=expires_in)
|
|
219
|
+
|
|
220
|
+
except httpx.HTTPError as e:
|
|
221
|
+
error_msg = f"Failed to get client token: {str(e)}"
|
|
222
|
+
if client_id:
|
|
223
|
+
error_msg += f" (clientId: {client_id})"
|
|
224
|
+
if correlation_id:
|
|
225
|
+
error_msg += f" (correlationId: {correlation_id})"
|
|
226
|
+
raise ConnectionError(error_msg)
|
|
227
|
+
except Exception as e:
|
|
228
|
+
if isinstance(e, (AuthenticationError, ConnectionError)):
|
|
229
|
+
raise
|
|
230
|
+
error_msg = f"Failed to get client token: {str(e)}"
|
|
231
|
+
if client_id:
|
|
232
|
+
error_msg += f" (clientId: {client_id})"
|
|
233
|
+
if correlation_id:
|
|
234
|
+
error_msg += f" (correlationId: {correlation_id})"
|
|
235
|
+
raise AuthenticationError(error_msg)
|
|
236
|
+
|
|
237
|
+
def clear_token(self) -> None:
|
|
238
|
+
"""
|
|
239
|
+
Clear cached client token.
|
|
240
|
+
|
|
241
|
+
Forces token refresh on next request.
|
|
242
|
+
"""
|
|
243
|
+
self.client_token = None
|
|
244
|
+
self.token_expires_at = None
|
|
@@ -5,51 +5,68 @@ Automatically loads environment variables with sensible defaults.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
|
-
from typing import Literal, cast
|
|
9
|
-
|
|
8
|
+
from typing import List, Literal, cast
|
|
9
|
+
|
|
10
10
|
from ..errors import ConfigurationError
|
|
11
|
+
from ..models.config import AuthMethod, AuthStrategy, MisoClientConfig, RedisConfig
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def load_config() -> MisoClientConfig:
|
|
14
15
|
"""
|
|
15
16
|
Load configuration from environment variables with defaults.
|
|
16
|
-
|
|
17
|
+
|
|
17
18
|
Required environment variables:
|
|
18
19
|
- MISO_CONTROLLER_URL (or default to https://controller.aifabrix.ai)
|
|
19
20
|
- MISO_CLIENTID or MISO_CLIENT_ID
|
|
20
21
|
- MISO_CLIENTSECRET or MISO_CLIENT_SECRET
|
|
21
|
-
|
|
22
|
+
|
|
22
23
|
Optional environment variables:
|
|
23
24
|
- MISO_LOG_LEVEL (debug, info, warn, error)
|
|
25
|
+
- API_KEY (for testing - bypasses OAuth2 authentication)
|
|
26
|
+
- MISO_API_KEY (alternative to API_KEY)
|
|
27
|
+
- MISO_AUTH_STRATEGY (comma-separated list: bearer,client-token,api-key)
|
|
28
|
+
- MISO_CLIENT_TOKEN_URI (custom client token endpoint URI)
|
|
29
|
+
- MISO_ALLOWED_ORIGINS (comma-separated list of allowed origins, supports wildcard ports)
|
|
30
|
+
- MISO_CONTROLLER_URL (maps to controllerPrivateUrl and controller_url for backward compatibility)
|
|
31
|
+
- MISO_WEB_SERVER_URL (maps to controllerPublicUrl for browser/public access)
|
|
24
32
|
- REDIS_HOST (if Redis is used)
|
|
25
33
|
- REDIS_PORT (default: 6379)
|
|
26
34
|
- REDIS_PASSWORD
|
|
27
35
|
- REDIS_DB (default: 0)
|
|
28
36
|
- REDIS_KEY_PREFIX (default: miso:)
|
|
29
|
-
|
|
37
|
+
|
|
30
38
|
Returns:
|
|
31
39
|
MisoClientConfig instance
|
|
32
|
-
|
|
40
|
+
|
|
33
41
|
Raises:
|
|
34
42
|
ConfigurationError: If required environment variables are missing
|
|
35
43
|
"""
|
|
36
44
|
# Load dotenv if available (similar to TypeScript dotenv/config)
|
|
37
45
|
try:
|
|
38
46
|
from dotenv import load_dotenv
|
|
47
|
+
|
|
39
48
|
load_dotenv()
|
|
40
49
|
except ImportError:
|
|
41
50
|
pass # dotenv not installed, continue without it
|
|
42
|
-
|
|
51
|
+
|
|
52
|
+
# MISO_CONTROLLER_URL maps to controllerPrivateUrl (server/internal) and controller_url (backward compatibility)
|
|
43
53
|
controller_url = os.environ.get("MISO_CONTROLLER_URL") or "https://controller.aifabrix.ai"
|
|
44
|
-
|
|
54
|
+
controller_private_url = os.environ.get(
|
|
55
|
+
"MISO_CONTROLLER_URL"
|
|
56
|
+
) # Same as controller_url for server
|
|
57
|
+
# MISO_WEB_SERVER_URL maps to controllerPublicUrl (browser/public)
|
|
58
|
+
controller_public_url = os.environ.get("MISO_WEB_SERVER_URL")
|
|
59
|
+
|
|
45
60
|
client_id = os.environ.get("MISO_CLIENTID") or os.environ.get("MISO_CLIENT_ID") or ""
|
|
46
61
|
if not client_id:
|
|
47
62
|
raise ConfigurationError("MISO_CLIENTID environment variable is required")
|
|
48
|
-
|
|
49
|
-
client_secret =
|
|
63
|
+
|
|
64
|
+
client_secret = (
|
|
65
|
+
os.environ.get("MISO_CLIENTSECRET") or os.environ.get("MISO_CLIENT_SECRET") or ""
|
|
66
|
+
)
|
|
50
67
|
if not client_secret:
|
|
51
68
|
raise ConfigurationError("MISO_CLIENTSECRET environment variable is required")
|
|
52
|
-
|
|
69
|
+
|
|
53
70
|
log_level_str = os.environ.get("MISO_LOG_LEVEL", "info")
|
|
54
71
|
if log_level_str not in ["debug", "info", "warn", "error"]:
|
|
55
72
|
log_level_str = "info"
|
|
@@ -57,22 +74,77 @@ def load_config() -> MisoClientConfig:
|
|
|
57
74
|
log_level: Literal["debug", "info", "warn", "error"] = cast(
|
|
58
75
|
Literal["debug", "info", "warn", "error"], log_level_str
|
|
59
76
|
)
|
|
60
|
-
|
|
77
|
+
|
|
78
|
+
# Optional API_KEY for testing (support both API_KEY and MISO_API_KEY)
|
|
79
|
+
api_key = os.environ.get("API_KEY") or os.environ.get("MISO_API_KEY")
|
|
80
|
+
|
|
81
|
+
# Optional auth strategy
|
|
82
|
+
auth_strategy = None
|
|
83
|
+
auth_strategy_str = os.environ.get("MISO_AUTH_STRATEGY")
|
|
84
|
+
if auth_strategy_str:
|
|
85
|
+
try:
|
|
86
|
+
methods_str = [m.strip() for m in auth_strategy_str.split(",")]
|
|
87
|
+
# Validate methods
|
|
88
|
+
valid_methods: List[AuthMethod] = [
|
|
89
|
+
"bearer",
|
|
90
|
+
"client-token",
|
|
91
|
+
"client-credentials",
|
|
92
|
+
"api-key",
|
|
93
|
+
]
|
|
94
|
+
methods: List[AuthMethod] = []
|
|
95
|
+
for method in methods_str:
|
|
96
|
+
if method in valid_methods:
|
|
97
|
+
methods.append(method) # type: ignore
|
|
98
|
+
else:
|
|
99
|
+
raise ConfigurationError(
|
|
100
|
+
f"Invalid auth method '{method}' in MISO_AUTH_STRATEGY. "
|
|
101
|
+
f"Valid methods: {', '.join(valid_methods)}"
|
|
102
|
+
)
|
|
103
|
+
if methods:
|
|
104
|
+
auth_strategy = AuthStrategy(methods=methods, apiKey=api_key)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
if isinstance(e, ConfigurationError):
|
|
107
|
+
raise
|
|
108
|
+
raise ConfigurationError(f"Failed to parse MISO_AUTH_STRATEGY: {str(e)}")
|
|
109
|
+
|
|
110
|
+
# Optional client token URI
|
|
111
|
+
client_token_uri = os.environ.get("MISO_CLIENT_TOKEN_URI")
|
|
112
|
+
|
|
113
|
+
# Optional allowed origins
|
|
114
|
+
allowed_origins = None
|
|
115
|
+
allowed_origins_str = os.environ.get("MISO_ALLOWED_ORIGINS")
|
|
116
|
+
if allowed_origins_str:
|
|
117
|
+
# Split comma-separated list and trim whitespace
|
|
118
|
+
origins_list = [
|
|
119
|
+
origin.strip() for origin in allowed_origins_str.split(",") if origin.strip()
|
|
120
|
+
]
|
|
121
|
+
if origins_list:
|
|
122
|
+
allowed_origins = origins_list
|
|
123
|
+
|
|
61
124
|
config: MisoClientConfig = MisoClientConfig(
|
|
62
125
|
controller_url=controller_url,
|
|
63
126
|
client_id=client_id,
|
|
64
127
|
client_secret=client_secret,
|
|
65
128
|
log_level=log_level,
|
|
129
|
+
api_key=api_key,
|
|
130
|
+
authStrategy=auth_strategy,
|
|
131
|
+
clientTokenUri=client_token_uri,
|
|
132
|
+
allowedOrigins=allowed_origins,
|
|
133
|
+
controllerPrivateUrl=controller_private_url,
|
|
134
|
+
controllerPublicUrl=controller_public_url,
|
|
66
135
|
)
|
|
67
|
-
|
|
136
|
+
|
|
68
137
|
# Optional Redis configuration
|
|
69
138
|
redis_host = os.environ.get("REDIS_HOST")
|
|
70
139
|
if redis_host:
|
|
71
140
|
redis_port = int(os.environ.get("REDIS_PORT", "6379"))
|
|
72
141
|
redis_password = os.environ.get("REDIS_PASSWORD")
|
|
142
|
+
# Convert empty string to None
|
|
143
|
+
if redis_password == "":
|
|
144
|
+
redis_password = None
|
|
73
145
|
redis_db = int(os.environ.get("REDIS_DB", "0")) if os.environ.get("REDIS_DB") else 0
|
|
74
146
|
redis_key_prefix = os.environ.get("REDIS_KEY_PREFIX", "miso:")
|
|
75
|
-
|
|
147
|
+
|
|
76
148
|
redis_config = RedisConfig(
|
|
77
149
|
host=redis_host,
|
|
78
150
|
port=redis_port,
|
|
@@ -80,8 +152,7 @@ def load_config() -> MisoClientConfig:
|
|
|
80
152
|
db=redis_db,
|
|
81
153
|
key_prefix=redis_key_prefix,
|
|
82
154
|
)
|
|
83
|
-
|
|
155
|
+
|
|
84
156
|
config.redis = redis_config
|
|
85
|
-
|
|
86
|
-
return config
|
|
87
157
|
|
|
158
|
+
return config
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Controller URL resolver with environment detection.
|
|
3
|
+
|
|
4
|
+
Automatically selects appropriate controller URL based on environment
|
|
5
|
+
(public for browser, private for server) with fallback support.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from ..errors import ConfigurationError
|
|
11
|
+
from ..models.config import MisoClientConfig
|
|
12
|
+
from .url_validator import validate_url
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_browser() -> bool:
|
|
16
|
+
"""
|
|
17
|
+
Check if running in browser environment.
|
|
18
|
+
|
|
19
|
+
For Python SDK (server-side only), always returns False.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
False (Python SDK is server-side only)
|
|
23
|
+
"""
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def resolve_controller_url(config: MisoClientConfig) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Resolve controller URL based on environment and configuration.
|
|
30
|
+
|
|
31
|
+
For server environment:
|
|
32
|
+
- Uses controllerPrivateUrl if set
|
|
33
|
+
- Falls back to controller_url if controllerPrivateUrl not set
|
|
34
|
+
- Validates resolved URL
|
|
35
|
+
- Raises ConfigurationError if no valid URL found
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
config: MisoClient configuration
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Resolved controller URL string
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ConfigurationError: If no valid URL is configured
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
>>> config = MisoClientConfig(
|
|
48
|
+
... controller_url="https://controller.example.com",
|
|
49
|
+
... controllerPrivateUrl="https://controller-private.example.com",
|
|
50
|
+
... client_id="test",
|
|
51
|
+
... client_secret="secret"
|
|
52
|
+
... )
|
|
53
|
+
>>> url = resolve_controller_url(config)
|
|
54
|
+
>>> url
|
|
55
|
+
'https://controller-private.example.com'
|
|
56
|
+
"""
|
|
57
|
+
# Server environment: prefer controllerPrivateUrl, fallback to controller_url
|
|
58
|
+
resolved_url: Optional[str] = None
|
|
59
|
+
|
|
60
|
+
if is_browser():
|
|
61
|
+
# Browser environment (not applicable for Python SDK, but included for completeness)
|
|
62
|
+
resolved_url = config.controllerPublicUrl or config.controller_url
|
|
63
|
+
else:
|
|
64
|
+
# Server environment
|
|
65
|
+
resolved_url = config.controllerPrivateUrl or config.controller_url
|
|
66
|
+
|
|
67
|
+
if not resolved_url:
|
|
68
|
+
raise ConfigurationError(
|
|
69
|
+
"No controller URL configured. Set controller_url, controllerPrivateUrl, "
|
|
70
|
+
"or controllerPublicUrl in MisoClientConfig."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Validate URL
|
|
74
|
+
if not validate_url(resolved_url):
|
|
75
|
+
raise ConfigurationError(
|
|
76
|
+
f"Invalid controller URL format: {resolved_url}. "
|
|
77
|
+
"URL must start with http:// or https:// and have a valid hostname."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return resolved_url
|