signalwire-agents 0.1.23__py3-none-any.whl → 0.1.24__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.
- signalwire_agents/__init__.py +1 -1
- signalwire_agents/agent_server.py +2 -1
- signalwire_agents/cli/config.py +61 -0
- signalwire_agents/cli/core/__init__.py +1 -0
- signalwire_agents/cli/core/agent_loader.py +254 -0
- signalwire_agents/cli/core/argparse_helpers.py +164 -0
- signalwire_agents/cli/core/dynamic_config.py +62 -0
- signalwire_agents/cli/execution/__init__.py +1 -0
- signalwire_agents/cli/execution/datamap_exec.py +437 -0
- signalwire_agents/cli/execution/webhook_exec.py +125 -0
- signalwire_agents/cli/output/__init__.py +1 -0
- signalwire_agents/cli/output/output_formatter.py +132 -0
- signalwire_agents/cli/output/swml_dump.py +177 -0
- signalwire_agents/cli/simulation/__init__.py +1 -0
- signalwire_agents/cli/simulation/data_generation.py +365 -0
- signalwire_agents/cli/simulation/data_overrides.py +187 -0
- signalwire_agents/cli/simulation/mock_env.py +271 -0
- signalwire_agents/cli/test_swaig.py +522 -2539
- signalwire_agents/cli/types.py +72 -0
- signalwire_agents/core/agent/__init__.py +1 -3
- signalwire_agents/core/agent/config/__init__.py +1 -3
- signalwire_agents/core/agent/prompt/manager.py +25 -7
- signalwire_agents/core/agent/tools/decorator.py +2 -0
- signalwire_agents/core/agent/tools/registry.py +8 -0
- signalwire_agents/core/agent_base.py +492 -3053
- signalwire_agents/core/function_result.py +31 -42
- signalwire_agents/core/mixins/__init__.py +28 -0
- signalwire_agents/core/mixins/ai_config_mixin.py +373 -0
- signalwire_agents/core/mixins/auth_mixin.py +287 -0
- signalwire_agents/core/mixins/prompt_mixin.py +345 -0
- signalwire_agents/core/mixins/serverless_mixin.py +368 -0
- signalwire_agents/core/mixins/skill_mixin.py +55 -0
- signalwire_agents/core/mixins/state_mixin.py +219 -0
- signalwire_agents/core/mixins/tool_mixin.py +295 -0
- signalwire_agents/core/mixins/web_mixin.py +1130 -0
- signalwire_agents/core/skill_manager.py +3 -1
- signalwire_agents/core/swaig_function.py +10 -1
- signalwire_agents/core/swml_service.py +140 -58
- signalwire_agents/skills/README.md +452 -0
- signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
- signalwire_agents/skills/datasphere/README.md +210 -0
- signalwire_agents/skills/datasphere_serverless/README.md +258 -0
- signalwire_agents/skills/datetime/README.md +132 -0
- signalwire_agents/skills/joke/README.md +149 -0
- signalwire_agents/skills/math/README.md +161 -0
- signalwire_agents/skills/native_vector_search/skill.py +33 -13
- signalwire_agents/skills/play_background_file/README.md +218 -0
- signalwire_agents/skills/spider/README.md +236 -0
- signalwire_agents/skills/spider/__init__.py +4 -0
- signalwire_agents/skills/spider/skill.py +479 -0
- signalwire_agents/skills/swml_transfer/README.md +395 -0
- signalwire_agents/skills/swml_transfer/__init__.py +1 -0
- signalwire_agents/skills/swml_transfer/skill.py +257 -0
- signalwire_agents/skills/weather_api/README.md +178 -0
- signalwire_agents/skills/web_search/README.md +163 -0
- signalwire_agents/skills/wikipedia_search/README.md +228 -0
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.24.dist-info}/METADATA +47 -2
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.24.dist-info}/RECORD +62 -22
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.24.dist-info}/entry_points.txt +1 -1
- signalwire_agents/core/agent/config/ephemeral.py +0 -176
- signalwire_agents-0.1.23.data/data/schema.json +0 -5611
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.24.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.24.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.24.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,187 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Handle CLI overrides and mapping to nested data
|
4
|
+
"""
|
5
|
+
|
6
|
+
import json
|
7
|
+
import uuid
|
8
|
+
import argparse
|
9
|
+
from typing import Dict, Any, List
|
10
|
+
|
11
|
+
|
12
|
+
def set_nested_value(data: Dict[str, Any], path: str, value: Any) -> None:
|
13
|
+
"""
|
14
|
+
Set a nested value using dot notation path
|
15
|
+
|
16
|
+
Args:
|
17
|
+
data: Dictionary to modify
|
18
|
+
path: Dot-notation path (e.g., "call.call_id" or "vars.userVariables.custom")
|
19
|
+
value: Value to set
|
20
|
+
"""
|
21
|
+
keys = path.split('.')
|
22
|
+
current = data
|
23
|
+
|
24
|
+
# Navigate to the parent of the target key
|
25
|
+
for key in keys[:-1]:
|
26
|
+
if key not in current:
|
27
|
+
current[key] = {}
|
28
|
+
current = current[key]
|
29
|
+
|
30
|
+
# Set the final value
|
31
|
+
current[keys[-1]] = value
|
32
|
+
|
33
|
+
|
34
|
+
def parse_value(value_str: str) -> Any:
|
35
|
+
"""
|
36
|
+
Parse a string value into appropriate Python type
|
37
|
+
|
38
|
+
Args:
|
39
|
+
value_str: String representation of value
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
Parsed value (str, int, float, bool, None, or JSON object)
|
43
|
+
"""
|
44
|
+
# Handle special values
|
45
|
+
if value_str.lower() == 'null':
|
46
|
+
return None
|
47
|
+
elif value_str.lower() == 'true':
|
48
|
+
return True
|
49
|
+
elif value_str.lower() == 'false':
|
50
|
+
return False
|
51
|
+
|
52
|
+
# Try parsing as number
|
53
|
+
try:
|
54
|
+
if '.' in value_str:
|
55
|
+
return float(value_str)
|
56
|
+
else:
|
57
|
+
return int(value_str)
|
58
|
+
except ValueError:
|
59
|
+
pass
|
60
|
+
|
61
|
+
# Try parsing as JSON (for objects/arrays)
|
62
|
+
try:
|
63
|
+
return json.loads(value_str)
|
64
|
+
except json.JSONDecodeError:
|
65
|
+
pass
|
66
|
+
|
67
|
+
# Return as string
|
68
|
+
return value_str
|
69
|
+
|
70
|
+
|
71
|
+
def apply_overrides(data: Dict[str, Any], overrides: List[str],
|
72
|
+
json_overrides: List[str]) -> Dict[str, Any]:
|
73
|
+
"""
|
74
|
+
Apply override values to data using dot notation paths
|
75
|
+
|
76
|
+
Args:
|
77
|
+
data: Data dictionary to modify
|
78
|
+
overrides: List of "path=value" strings
|
79
|
+
json_overrides: List of "path=json_value" strings
|
80
|
+
|
81
|
+
Returns:
|
82
|
+
Modified data dictionary
|
83
|
+
"""
|
84
|
+
data = data.copy()
|
85
|
+
|
86
|
+
# Apply simple overrides
|
87
|
+
for override in overrides:
|
88
|
+
if '=' not in override:
|
89
|
+
continue
|
90
|
+
path, value_str = override.split('=', 1)
|
91
|
+
value = parse_value(value_str)
|
92
|
+
set_nested_value(data, path, value)
|
93
|
+
|
94
|
+
# Apply JSON overrides
|
95
|
+
for json_override in json_overrides:
|
96
|
+
if '=' not in json_override:
|
97
|
+
continue
|
98
|
+
path, json_str = json_override.split('=', 1)
|
99
|
+
try:
|
100
|
+
value = json.loads(json_str)
|
101
|
+
set_nested_value(data, path, value)
|
102
|
+
except json.JSONDecodeError as e:
|
103
|
+
print(f"Warning: Invalid JSON in override '{json_override}': {e}")
|
104
|
+
|
105
|
+
return data
|
106
|
+
|
107
|
+
|
108
|
+
def apply_convenience_mappings(data: Dict[str, Any], args: argparse.Namespace) -> Dict[str, Any]:
|
109
|
+
"""
|
110
|
+
Apply convenience CLI arguments to data structure
|
111
|
+
|
112
|
+
Args:
|
113
|
+
data: Data dictionary to modify
|
114
|
+
args: Parsed CLI arguments
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
Modified data dictionary
|
118
|
+
"""
|
119
|
+
data = data.copy()
|
120
|
+
|
121
|
+
# Map high-level arguments to specific paths
|
122
|
+
if hasattr(args, 'call_id') and args.call_id:
|
123
|
+
set_nested_value(data, "call.call_id", args.call_id)
|
124
|
+
set_nested_value(data, "call.tag", args.call_id) # tag often matches call_id
|
125
|
+
|
126
|
+
if hasattr(args, 'project_id') and args.project_id:
|
127
|
+
set_nested_value(data, "call.project_id", args.project_id)
|
128
|
+
|
129
|
+
if hasattr(args, 'space_id') and args.space_id:
|
130
|
+
set_nested_value(data, "call.space_id", args.space_id)
|
131
|
+
|
132
|
+
if hasattr(args, 'call_state') and args.call_state:
|
133
|
+
set_nested_value(data, "call.state", args.call_state)
|
134
|
+
|
135
|
+
if hasattr(args, 'call_direction') and args.call_direction:
|
136
|
+
set_nested_value(data, "call.direction", args.call_direction)
|
137
|
+
|
138
|
+
# Handle from/to addresses with fake generation if needed
|
139
|
+
if hasattr(args, 'from_number') and args.from_number:
|
140
|
+
# If looks like phone number, use as-is, otherwise generate fake
|
141
|
+
if args.from_number.startswith('+') or args.from_number.isdigit():
|
142
|
+
set_nested_value(data, "call.from", args.from_number)
|
143
|
+
else:
|
144
|
+
# Generate fake phone number or SIP address
|
145
|
+
call_type = getattr(args, 'call_type', 'webrtc')
|
146
|
+
if call_type == 'sip':
|
147
|
+
set_nested_value(data, "call.from", f"+1555{uuid.uuid4().hex[:7]}")
|
148
|
+
else:
|
149
|
+
set_nested_value(data, "call.from", f"{args.from_number}@test.domain")
|
150
|
+
|
151
|
+
if hasattr(args, 'to_extension') and args.to_extension:
|
152
|
+
# Similar logic for 'to' address
|
153
|
+
if args.to_extension.startswith('+') or args.to_extension.isdigit():
|
154
|
+
set_nested_value(data, "call.to", args.to_extension)
|
155
|
+
else:
|
156
|
+
call_type = getattr(args, 'call_type', 'webrtc')
|
157
|
+
if call_type == 'sip':
|
158
|
+
set_nested_value(data, "call.to", f"+1444{uuid.uuid4().hex[:7]}")
|
159
|
+
else:
|
160
|
+
set_nested_value(data, "call.to", f"{args.to_extension}@test.domain")
|
161
|
+
|
162
|
+
# Merge user variables
|
163
|
+
user_vars = {}
|
164
|
+
|
165
|
+
# Add user_vars if provided
|
166
|
+
if hasattr(args, 'user_vars') and args.user_vars:
|
167
|
+
try:
|
168
|
+
user_vars.update(json.loads(args.user_vars))
|
169
|
+
except json.JSONDecodeError as e:
|
170
|
+
print(f"Warning: Invalid JSON in --user-vars: {e}")
|
171
|
+
|
172
|
+
# Add query_params if provided (merged into userVariables)
|
173
|
+
if hasattr(args, 'query_params') and args.query_params:
|
174
|
+
try:
|
175
|
+
user_vars.update(json.loads(args.query_params))
|
176
|
+
except json.JSONDecodeError as e:
|
177
|
+
print(f"Warning: Invalid JSON in --query-params: {e}")
|
178
|
+
|
179
|
+
# Apply user variables
|
180
|
+
if user_vars:
|
181
|
+
if "vars" not in data:
|
182
|
+
data["vars"] = {}
|
183
|
+
if "userVariables" not in data["vars"]:
|
184
|
+
data["vars"]["userVariables"] = {}
|
185
|
+
data["vars"]["userVariables"].update(user_vars)
|
186
|
+
|
187
|
+
return data
|
@@ -0,0 +1,271 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Mock environment and serverless simulation functionality
|
4
|
+
"""
|
5
|
+
|
6
|
+
import os
|
7
|
+
import json
|
8
|
+
from typing import Optional, Dict, Any
|
9
|
+
from ..types import PostData
|
10
|
+
|
11
|
+
|
12
|
+
class MockQueryParams:
|
13
|
+
"""Mock FastAPI QueryParams (simple dict-like)"""
|
14
|
+
def __init__(self, params: Optional[Dict[str, str]] = None):
|
15
|
+
self._params = params or {}
|
16
|
+
|
17
|
+
def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
|
18
|
+
return self._params.get(key, default)
|
19
|
+
|
20
|
+
def __getitem__(self, key: str) -> str:
|
21
|
+
return self._params[key]
|
22
|
+
|
23
|
+
def __contains__(self, key: str) -> bool:
|
24
|
+
return key in self._params
|
25
|
+
|
26
|
+
def items(self):
|
27
|
+
return self._params.items()
|
28
|
+
|
29
|
+
def keys(self):
|
30
|
+
return self._params.keys()
|
31
|
+
|
32
|
+
def values(self):
|
33
|
+
return self._params.values()
|
34
|
+
|
35
|
+
|
36
|
+
class MockHeaders:
|
37
|
+
"""Mock FastAPI Headers (case-insensitive dict-like)"""
|
38
|
+
def __init__(self, headers: Optional[Dict[str, str]] = None):
|
39
|
+
# Store headers with lowercase keys for case-insensitive lookup
|
40
|
+
self._headers = {}
|
41
|
+
if headers:
|
42
|
+
for k, v in headers.items():
|
43
|
+
self._headers[k.lower()] = v
|
44
|
+
|
45
|
+
def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
|
46
|
+
return self._headers.get(key.lower(), default)
|
47
|
+
|
48
|
+
def __getitem__(self, key: str) -> str:
|
49
|
+
return self._headers[key.lower()]
|
50
|
+
|
51
|
+
def __contains__(self, key: str) -> bool:
|
52
|
+
return key.lower() in self._headers
|
53
|
+
|
54
|
+
def items(self):
|
55
|
+
return self._headers.items()
|
56
|
+
|
57
|
+
def keys(self):
|
58
|
+
return self._headers.keys()
|
59
|
+
|
60
|
+
def values(self):
|
61
|
+
return self._headers.values()
|
62
|
+
|
63
|
+
|
64
|
+
class MockURL:
|
65
|
+
"""Mock FastAPI URL object"""
|
66
|
+
def __init__(self, url: str = "http://localhost:8080/swml"):
|
67
|
+
self._url = url
|
68
|
+
# Parse basic components
|
69
|
+
if "?" in url:
|
70
|
+
self.path, query_string = url.split("?", 1)
|
71
|
+
self.query = query_string
|
72
|
+
else:
|
73
|
+
self.path = url
|
74
|
+
self.query = ""
|
75
|
+
|
76
|
+
# Extract scheme and netloc
|
77
|
+
if "://" in url:
|
78
|
+
self.scheme, rest = url.split("://", 1)
|
79
|
+
if "/" in rest:
|
80
|
+
self.netloc = rest.split("/", 1)[0]
|
81
|
+
else:
|
82
|
+
self.netloc = rest
|
83
|
+
else:
|
84
|
+
self.scheme = "http"
|
85
|
+
self.netloc = "localhost:8080"
|
86
|
+
|
87
|
+
def __str__(self):
|
88
|
+
return self._url
|
89
|
+
|
90
|
+
|
91
|
+
class MockRequest:
|
92
|
+
"""Mock FastAPI Request object for dynamic agent testing"""
|
93
|
+
def __init__(self, method: str = "POST", url: str = "http://localhost:8080/swml",
|
94
|
+
headers: Optional[Dict[str, str]] = None,
|
95
|
+
query_params: Optional[Dict[str, str]] = None,
|
96
|
+
json_body: Optional[Dict[str, Any]] = None):
|
97
|
+
self.method = method
|
98
|
+
self.url = MockURL(url)
|
99
|
+
self.headers = MockHeaders(headers)
|
100
|
+
self.query_params = MockQueryParams(query_params)
|
101
|
+
self._json_body = json_body or {}
|
102
|
+
self._body = json.dumps(self._json_body).encode('utf-8')
|
103
|
+
|
104
|
+
async def json(self) -> Dict[str, Any]:
|
105
|
+
"""Return the JSON body"""
|
106
|
+
return self._json_body
|
107
|
+
|
108
|
+
async def body(self) -> bytes:
|
109
|
+
"""Return the raw body bytes"""
|
110
|
+
return self._body
|
111
|
+
|
112
|
+
def client(self):
|
113
|
+
"""Mock client property"""
|
114
|
+
return type('MockClient', (), {'host': '127.0.0.1', 'port': 0})()
|
115
|
+
|
116
|
+
|
117
|
+
def create_mock_request(method: str = "POST", url: str = "http://localhost:8080/swml",
|
118
|
+
headers: Optional[Dict[str, str]] = None,
|
119
|
+
query_params: Optional[Dict[str, str]] = None,
|
120
|
+
body: Optional[Dict[str, Any]] = None) -> MockRequest:
|
121
|
+
"""
|
122
|
+
Factory function to create a mock FastAPI Request object
|
123
|
+
"""
|
124
|
+
return MockRequest(method=method, url=url, headers=headers,
|
125
|
+
query_params=query_params, json_body=body)
|
126
|
+
|
127
|
+
|
128
|
+
class ServerlessSimulator:
|
129
|
+
"""Manages serverless environment simulation for different platforms"""
|
130
|
+
|
131
|
+
# Default environment presets for each platform
|
132
|
+
PLATFORM_PRESETS = {
|
133
|
+
'lambda': {
|
134
|
+
'AWS_LAMBDA_FUNCTION_NAME': 'test-agent-function',
|
135
|
+
'AWS_LAMBDA_FUNCTION_URL': 'https://abc123.lambda-url.us-east-1.on.aws/',
|
136
|
+
'AWS_REGION': 'us-east-1',
|
137
|
+
'_HANDLER': 'lambda_function.lambda_handler'
|
138
|
+
},
|
139
|
+
'cgi': {
|
140
|
+
'GATEWAY_INTERFACE': 'CGI/1.1',
|
141
|
+
'HTTP_HOST': 'example.com',
|
142
|
+
'SCRIPT_NAME': '/cgi-bin/agent.cgi',
|
143
|
+
'HTTPS': 'on',
|
144
|
+
'SERVER_NAME': 'example.com'
|
145
|
+
},
|
146
|
+
'cloud_function': {
|
147
|
+
'GOOGLE_CLOUD_PROJECT': 'test-project',
|
148
|
+
'FUNCTION_URL': 'https://my-function-abc123.cloudfunctions.net',
|
149
|
+
'GOOGLE_CLOUD_REGION': 'us-central1',
|
150
|
+
'K_SERVICE': 'agent'
|
151
|
+
},
|
152
|
+
'azure_function': {
|
153
|
+
'AZURE_FUNCTIONS_ENVIRONMENT': 'Development',
|
154
|
+
'FUNCTIONS_WORKER_RUNTIME': 'python',
|
155
|
+
'WEBSITE_SITE_NAME': 'my-function-app'
|
156
|
+
}
|
157
|
+
}
|
158
|
+
|
159
|
+
def __init__(self, platform: str, overrides: Optional[Dict[str, str]] = None):
|
160
|
+
self.platform = platform
|
161
|
+
self.original_env = dict(os.environ)
|
162
|
+
self.preset_env = self.PLATFORM_PRESETS.get(platform, {}).copy()
|
163
|
+
self.overrides = overrides or {}
|
164
|
+
self.active = False
|
165
|
+
self._cleared_vars = {}
|
166
|
+
|
167
|
+
def activate(self, verbose: bool = False):
|
168
|
+
"""Apply serverless environment simulation"""
|
169
|
+
if self.active:
|
170
|
+
return
|
171
|
+
|
172
|
+
# Clear conflicting environment variables
|
173
|
+
self._clear_conflicting_env()
|
174
|
+
|
175
|
+
# Apply preset environment
|
176
|
+
os.environ.update(self.preset_env)
|
177
|
+
|
178
|
+
# Apply user overrides
|
179
|
+
os.environ.update(self.overrides)
|
180
|
+
|
181
|
+
# Set appropriate logging mode for serverless simulation
|
182
|
+
if self.platform == 'cgi' and 'SIGNALWIRE_LOG_MODE' not in self.overrides:
|
183
|
+
# CGI mode should default to 'off' unless explicitly overridden
|
184
|
+
os.environ['SIGNALWIRE_LOG_MODE'] = 'off'
|
185
|
+
|
186
|
+
self.active = True
|
187
|
+
|
188
|
+
if verbose:
|
189
|
+
print(f"✓ Activated {self.platform} environment simulation")
|
190
|
+
|
191
|
+
# Debug: Show key environment variables
|
192
|
+
if self.platform == 'lambda':
|
193
|
+
print(f" AWS_LAMBDA_FUNCTION_NAME: {os.environ.get('AWS_LAMBDA_FUNCTION_NAME')}")
|
194
|
+
print(f" AWS_LAMBDA_FUNCTION_URL: {os.environ.get('AWS_LAMBDA_FUNCTION_URL')}")
|
195
|
+
print(f" AWS_REGION: {os.environ.get('AWS_REGION')}")
|
196
|
+
elif self.platform == 'cgi':
|
197
|
+
print(f" GATEWAY_INTERFACE: {os.environ.get('GATEWAY_INTERFACE')}")
|
198
|
+
print(f" HTTP_HOST: {os.environ.get('HTTP_HOST')}")
|
199
|
+
print(f" SCRIPT_NAME: {os.environ.get('SCRIPT_NAME')}")
|
200
|
+
print(f" SIGNALWIRE_LOG_MODE: {os.environ.get('SIGNALWIRE_LOG_MODE')}")
|
201
|
+
elif self.platform == 'cloud_function':
|
202
|
+
print(f" GOOGLE_CLOUD_PROJECT: {os.environ.get('GOOGLE_CLOUD_PROJECT')}")
|
203
|
+
print(f" FUNCTION_URL: {os.environ.get('FUNCTION_URL')}")
|
204
|
+
print(f" GOOGLE_CLOUD_REGION: {os.environ.get('GOOGLE_CLOUD_REGION')}")
|
205
|
+
elif self.platform == 'azure_function':
|
206
|
+
print(f" AZURE_FUNCTIONS_ENVIRONMENT: {os.environ.get('AZURE_FUNCTIONS_ENVIRONMENT')}")
|
207
|
+
print(f" WEBSITE_SITE_NAME: {os.environ.get('WEBSITE_SITE_NAME')}")
|
208
|
+
|
209
|
+
# Debug: Confirm SWML_PROXY_URL_BASE is cleared
|
210
|
+
proxy_url = os.environ.get('SWML_PROXY_URL_BASE')
|
211
|
+
if proxy_url:
|
212
|
+
print(f" WARNING: SWML_PROXY_URL_BASE still set: {proxy_url}")
|
213
|
+
else:
|
214
|
+
print(f" ✓ SWML_PROXY_URL_BASE cleared successfully")
|
215
|
+
|
216
|
+
def deactivate(self, verbose: bool = False):
|
217
|
+
"""Restore original environment"""
|
218
|
+
if not self.active:
|
219
|
+
return
|
220
|
+
|
221
|
+
os.environ.clear()
|
222
|
+
os.environ.update(self.original_env)
|
223
|
+
self.active = False
|
224
|
+
|
225
|
+
if verbose:
|
226
|
+
print(f"✓ Deactivated {self.platform} environment simulation")
|
227
|
+
|
228
|
+
def _clear_conflicting_env(self):
|
229
|
+
"""Clear environment variables that might conflict with simulation"""
|
230
|
+
# Remove variables from other platforms
|
231
|
+
conflicting_vars = []
|
232
|
+
for platform, preset in self.PLATFORM_PRESETS.items():
|
233
|
+
if platform != self.platform:
|
234
|
+
conflicting_vars.extend(preset.keys())
|
235
|
+
|
236
|
+
# Always clear SWML_PROXY_URL_BASE during serverless simulation
|
237
|
+
# so that platform-specific URL generation takes precedence
|
238
|
+
conflicting_vars.append('SWML_PROXY_URL_BASE')
|
239
|
+
|
240
|
+
for var in conflicting_vars:
|
241
|
+
if var in os.environ:
|
242
|
+
self._cleared_vars[var] = os.environ[var]
|
243
|
+
os.environ.pop(var)
|
244
|
+
|
245
|
+
def add_override(self, key: str, value: str):
|
246
|
+
"""Add an environment variable override"""
|
247
|
+
self.overrides[key] = value
|
248
|
+
if self.active:
|
249
|
+
os.environ[key] = value
|
250
|
+
|
251
|
+
def get_current_env(self) -> Dict[str, str]:
|
252
|
+
"""Get the current environment that would be applied"""
|
253
|
+
env = self.preset_env.copy()
|
254
|
+
env.update(self.overrides)
|
255
|
+
return env
|
256
|
+
|
257
|
+
|
258
|
+
def load_env_file(env_file_path: str) -> Dict[str, str]:
|
259
|
+
"""Load environment variables from a file"""
|
260
|
+
env_vars = {}
|
261
|
+
if not os.path.exists(env_file_path):
|
262
|
+
raise FileNotFoundError(f"Environment file not found: {env_file_path}")
|
263
|
+
|
264
|
+
with open(env_file_path, 'r') as f:
|
265
|
+
for line in f:
|
266
|
+
line = line.strip()
|
267
|
+
if line and not line.startswith('#') and '=' in line:
|
268
|
+
key, value = line.split('=', 1)
|
269
|
+
env_vars[key.strip()] = value.strip()
|
270
|
+
|
271
|
+
return env_vars
|