lam-cli 0.1.7__py3-none-any.whl → 1.0.1__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.
- lam/__init__.py +1 -0
- lam/core.py +95 -0
- lam/engines/__init__.py +23 -0
- lam/engines/base.py +56 -0
- lam/engines/javascript.py +283 -0
- lam/engines/jq.py +65 -0
- lam/engines/python.py +839 -0
- lam/lam.py +18 -863
- lam/utils.py +22 -0
- lam_cli-1.0.1.dist-info/METADATA +229 -0
- lam_cli-1.0.1.dist-info/RECORD +15 -0
- {lam_cli-0.1.7.dist-info → lam_cli-1.0.1.dist-info}/WHEEL +1 -1
- lam_cli-0.1.7.dist-info/METADATA +0 -53
- lam_cli-0.1.7.dist-info/RECORD +0 -8
- {lam_cli-0.1.7.dist-info → lam_cli-1.0.1.dist-info}/entry_points.txt +0 -0
- {lam_cli-0.1.7.dist-info → lam_cli-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {lam_cli-0.1.7.dist-info → lam_cli-1.0.1.dist-info}/top_level.txt +0 -0
lam/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
lam/core.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import tempfile
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import psutil
|
|
12
|
+
from logtail import LogtailHandler
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def setup_logging():
|
|
16
|
+
"""Configure logging with UTC timezone and handlers"""
|
|
17
|
+
# Configure logging with UTC timezone
|
|
18
|
+
logging.Formatter.converter = lambda *args: datetime.now(timezone.utc).timetuple()
|
|
19
|
+
|
|
20
|
+
# Set up root logger configuration
|
|
21
|
+
logging.basicConfig(
|
|
22
|
+
level=logging.DEBUG,
|
|
23
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
24
|
+
force=True # Override any existing configuration
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Suppress noisy loggers
|
|
28
|
+
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
|
29
|
+
|
|
30
|
+
# Add LogTail handler to root logger
|
|
31
|
+
root_logger = logging.getLogger()
|
|
32
|
+
handler = LogtailHandler(source_token="TYz3WrrvC8ehYjXdAEGGyiDp")
|
|
33
|
+
root_logger.addHandler(handler)
|
|
34
|
+
|
|
35
|
+
return logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class LAMError(Exception):
|
|
39
|
+
"""Base exception for LAM errors"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class UserError(LAMError):
|
|
44
|
+
"""Errors caused by user input"""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SystemError(LAMError):
|
|
49
|
+
"""Errors caused by system issues"""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ResourceLimitError(LAMError):
|
|
54
|
+
"""Errors caused by resource limits"""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ProcessingError(Exception):
|
|
59
|
+
"""Custom exception for processing errors"""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def check_resource_limits(modules_dir: Optional[Path] = None) -> None:
|
|
64
|
+
"""Check system resource availability"""
|
|
65
|
+
disk = shutil.disk_usage(tempfile.gettempdir())
|
|
66
|
+
if disk.free < 100 * 1024 * 1024: # 100MB minimum
|
|
67
|
+
raise ResourceLimitError("Insufficient disk space")
|
|
68
|
+
|
|
69
|
+
if modules_dir and modules_dir.exists():
|
|
70
|
+
modules_size = sum(
|
|
71
|
+
os.path.getsize(os.path.join(dirpath, filename))
|
|
72
|
+
for dirpath, _, filenames in os.walk(modules_dir)
|
|
73
|
+
for filename in filenames
|
|
74
|
+
)
|
|
75
|
+
if modules_size > 500 * 1024 * 1024:
|
|
76
|
+
shutil.rmtree(modules_dir)
|
|
77
|
+
modules_dir.mkdir(exist_ok=True)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Stats:
|
|
81
|
+
"""Track execution statistics"""
|
|
82
|
+
def __init__(self):
|
|
83
|
+
self.start_time = datetime.now()
|
|
84
|
+
self.memory_start = self.get_memory_usage()
|
|
85
|
+
|
|
86
|
+
def get_memory_usage(self):
|
|
87
|
+
process = psutil.Process()
|
|
88
|
+
return process.memory_info().rss
|
|
89
|
+
|
|
90
|
+
def finalize(self):
|
|
91
|
+
return {
|
|
92
|
+
'duration_ms': (datetime.now() - self.start_time).total_seconds() * 1000,
|
|
93
|
+
'memory_used_mb': (self.get_memory_usage() - self.memory_start) / (1024 * 1024),
|
|
94
|
+
'timestamp': datetime.now().isoformat()
|
|
95
|
+
}
|
lam/engines/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .base import Engine, EngineType
|
|
2
|
+
from .javascript import BunEngine
|
|
3
|
+
from .jq import JQEngine
|
|
4
|
+
from .python import PythonEngine
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_engine(engine_type: str, workspace_id: str, flow_id: str, execution_id: str) -> Engine:
|
|
8
|
+
"""Factory function to get the appropriate execution engine"""
|
|
9
|
+
engines = {
|
|
10
|
+
EngineType.JQ.value: JQEngine,
|
|
11
|
+
EngineType.JAVASCRIPT.value: BunEngine,
|
|
12
|
+
EngineType.PYTHON.value: PythonEngine
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
engine_class = engines.get(engine_type)
|
|
16
|
+
if not engine_class:
|
|
17
|
+
raise ValueError(f"Unsupported engine type: {engine_type}")
|
|
18
|
+
|
|
19
|
+
engine = engine_class(workspace_id, flow_id, execution_id)
|
|
20
|
+
if not engine.validate_environment():
|
|
21
|
+
raise EnvironmentError(f"Required dependencies not found for {engine_type}")
|
|
22
|
+
|
|
23
|
+
return engine
|
lam/engines/base.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import socket
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Dict, Optional, Tuple, Union
|
|
7
|
+
|
|
8
|
+
from posthog import Posthog
|
|
9
|
+
|
|
10
|
+
# Initialize analytics
|
|
11
|
+
posthog = Posthog(project_api_key='phc_wfeHFG0p5yZIdBpjVYy00o5x1HbEpggdMzIuFYgNPSK',
|
|
12
|
+
host='https://app.posthog.com')
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EngineType(Enum):
|
|
18
|
+
JQ = "jq"
|
|
19
|
+
JAVASCRIPT = "js"
|
|
20
|
+
PYTHON = "py"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Engine:
|
|
24
|
+
"""Base class for execution engines"""
|
|
25
|
+
def __init__(self, workspace_id: str, flow_id: str, execution_id: str):
|
|
26
|
+
self.workspace_id = workspace_id
|
|
27
|
+
self.flow_id = flow_id
|
|
28
|
+
self.execution_id = execution_id
|
|
29
|
+
self.timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
30
|
+
|
|
31
|
+
def get_log_file(self) -> str:
|
|
32
|
+
return f"lam_run_{self.workspace_id}_{self.flow_id}_{self.execution_id}_{self.timestamp}.log"
|
|
33
|
+
|
|
34
|
+
def get_result_file(self) -> str:
|
|
35
|
+
return f"lam_result_{self.workspace_id}_{self.flow_id}_{self.execution_id}_{self.timestamp}.json"
|
|
36
|
+
|
|
37
|
+
def track_event(self, event_name: str, properties: Dict[str, Any]) -> None:
|
|
38
|
+
"""Track events with PostHog"""
|
|
39
|
+
try:
|
|
40
|
+
distinct_id = f"{os.getuid()}_{socket.gethostname()}_{self.workspace_id}_{self.flow_id}"
|
|
41
|
+
properties |= {
|
|
42
|
+
'workspace_id': self.workspace_id,
|
|
43
|
+
'flow_id': self.flow_id,
|
|
44
|
+
'engine': self.__class__.__name__,
|
|
45
|
+
}
|
|
46
|
+
posthog.capture(distinct_id=distinct_id, event=event_name, properties=properties)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.error(f"Error tracking event: {e}")
|
|
49
|
+
|
|
50
|
+
def validate_environment(self) -> bool:
|
|
51
|
+
"""Validate that the engine can run in this environment"""
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
def execute(self, program_file: str, input_data: str) -> Tuple[Union[Dict, str], Optional[str]]:
|
|
55
|
+
"""Execute the program with input data"""
|
|
56
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Optional, Tuple, Union
|
|
10
|
+
|
|
11
|
+
from ..core import ProcessingError, Stats, check_resource_limits
|
|
12
|
+
from .base import Engine
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BunEngine(Engine):
|
|
18
|
+
"""Bun JavaScript execution engine with enhanced logging"""
|
|
19
|
+
def __init__(self, *args, **kwargs):
|
|
20
|
+
super().__init__(*args, **kwargs)
|
|
21
|
+
self.modules_dir = Path(tempfile.gettempdir()) / "lam_modules"
|
|
22
|
+
self.modules_dir.mkdir(exist_ok=True)
|
|
23
|
+
self._setup_shared_modules()
|
|
24
|
+
|
|
25
|
+
self.runtime_template = '''
|
|
26
|
+
const logs = [];
|
|
27
|
+
const originalLog = console.log;
|
|
28
|
+
const originalError = console.error;
|
|
29
|
+
const originalWarn = console.warn;
|
|
30
|
+
|
|
31
|
+
console.log = (...args) => logs.push({ type: 'log', message: args.map(String).join(' ') });
|
|
32
|
+
console.error = (...args) => {
|
|
33
|
+
originalError(...args); // Keep error output for debugging
|
|
34
|
+
logs.push({ type: 'error', message: args.map(String).join(' ') });
|
|
35
|
+
};
|
|
36
|
+
console.warn = (...args) => logs.push({ type: 'warn', message: args.map(String).join(' ') });
|
|
37
|
+
|
|
38
|
+
// Keep original stdout for result output
|
|
39
|
+
const writeResult = (obj) => {
|
|
40
|
+
console.log("Writing result:", JSON.stringify(obj, null, 2));
|
|
41
|
+
originalLog(JSON.stringify(obj));
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const _ = require('lodash');
|
|
45
|
+
const { format, parseISO } = require('date-fns');
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
_,
|
|
49
|
+
format,
|
|
50
|
+
parseISO,
|
|
51
|
+
logs,
|
|
52
|
+
writeResult
|
|
53
|
+
};
|
|
54
|
+
'''
|
|
55
|
+
|
|
56
|
+
def _setup_shared_modules(self):
|
|
57
|
+
"""Setup shared node_modules once"""
|
|
58
|
+
if not (self.modules_dir / "node_modules").exists():
|
|
59
|
+
logger.info("Initializing shared modules directory")
|
|
60
|
+
package_json = {
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"lodash": "^4.17.21",
|
|
63
|
+
"date-fns": "^2.30.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
with open(self.modules_dir / "package.json", "w") as f:
|
|
67
|
+
json.dump(package_json, f, indent=2)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
logger.debug("Installing shared dependencies")
|
|
71
|
+
result = subprocess.run(
|
|
72
|
+
[self.get_bun_path(), "install"],
|
|
73
|
+
cwd=self.modules_dir,
|
|
74
|
+
check=True,
|
|
75
|
+
capture_output=True,
|
|
76
|
+
text=True,
|
|
77
|
+
timeout=30
|
|
78
|
+
)
|
|
79
|
+
logger.debug("Dependency install output: %s", result.stdout)
|
|
80
|
+
except subprocess.CalledProcessError as e:
|
|
81
|
+
logger.error("Dependency install failed: %s", e.stderr)
|
|
82
|
+
raise ProcessingError(f"Environment setup failed: {e.stderr}") from e
|
|
83
|
+
|
|
84
|
+
def create_wrapper(self, input_data: str, user_script: str) -> str:
|
|
85
|
+
"""Create the wrapper script with proper escaping"""
|
|
86
|
+
return f'''
|
|
87
|
+
const {{ _, format, parseISO, logs, writeResult }} = require('./runtime.js');
|
|
88
|
+
|
|
89
|
+
// Utility function to handle circular references in JSON.stringify
|
|
90
|
+
function safeStringify(obj) {{
|
|
91
|
+
const seen = new WeakSet();
|
|
92
|
+
return JSON.stringify(obj, (key, value) => {{
|
|
93
|
+
if (typeof value === 'object' && value !== null) {{
|
|
94
|
+
if (seen.has(value)) {{
|
|
95
|
+
return '[Circular Reference]';
|
|
96
|
+
}}
|
|
97
|
+
seen.add(value);
|
|
98
|
+
}}
|
|
99
|
+
return value;
|
|
100
|
+
}}, 2);
|
|
101
|
+
}}
|
|
102
|
+
|
|
103
|
+
// Validate transform function
|
|
104
|
+
function validateTransform(fn) {{
|
|
105
|
+
if (typeof fn !== 'function') {{
|
|
106
|
+
throw new Error('Transform must be a function');
|
|
107
|
+
}}
|
|
108
|
+
if (fn.length !== 1) {{
|
|
109
|
+
throw new Error('Transform function must accept exactly one argument (input)');
|
|
110
|
+
}}
|
|
111
|
+
}}
|
|
112
|
+
|
|
113
|
+
// Execute transform immediately
|
|
114
|
+
try {{
|
|
115
|
+
// Parse input safely
|
|
116
|
+
let input;
|
|
117
|
+
try {{
|
|
118
|
+
input = JSON.parse({json.dumps(input_data)});
|
|
119
|
+
}} catch (e) {{
|
|
120
|
+
throw new Error(`Failed to parse input data: ${{e.message}}`);
|
|
121
|
+
}}
|
|
122
|
+
|
|
123
|
+
// Get transform function
|
|
124
|
+
let transform;
|
|
125
|
+
try {{
|
|
126
|
+
transform = {user_script};
|
|
127
|
+
}} catch (e) {{
|
|
128
|
+
throw new Error(`Failed to parse transform function: ${{e.message}}`);
|
|
129
|
+
}}
|
|
130
|
+
|
|
131
|
+
// Validate transform
|
|
132
|
+
validateTransform(transform);
|
|
133
|
+
|
|
134
|
+
// Execute transform
|
|
135
|
+
const result = transform(input);
|
|
136
|
+
|
|
137
|
+
// Output result after transform
|
|
138
|
+
writeResult({{
|
|
139
|
+
result,
|
|
140
|
+
logs
|
|
141
|
+
}});
|
|
142
|
+
}} catch (error) {{
|
|
143
|
+
console.error(JSON.stringify({{
|
|
144
|
+
error: error.message,
|
|
145
|
+
stack: error.stack?.split('\\n') || [],
|
|
146
|
+
type: error.constructor.name
|
|
147
|
+
}}));
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}}
|
|
150
|
+
'''
|
|
151
|
+
|
|
152
|
+
def setup_environment(self, temp_dir: Path) -> None:
|
|
153
|
+
"""Set up the JavaScript environment with runtime"""
|
|
154
|
+
# Write runtime file only
|
|
155
|
+
runtime_path = temp_dir / "runtime.js"
|
|
156
|
+
with open(runtime_path, "w") as f:
|
|
157
|
+
f.write(self.runtime_template)
|
|
158
|
+
logger.debug("Runtime file written to: %s", runtime_path)
|
|
159
|
+
|
|
160
|
+
# Symlink node_modules from shared directory
|
|
161
|
+
os.symlink(self.modules_dir / "node_modules", temp_dir / "node_modules")
|
|
162
|
+
logger.debug("node_modules symlinked from: %s", self.modules_dir / "node_modules")
|
|
163
|
+
|
|
164
|
+
def validate_environment(self) -> bool:
|
|
165
|
+
# Check multiple locations for bun
|
|
166
|
+
possible_locations = [
|
|
167
|
+
"bun", # System PATH
|
|
168
|
+
os.path.join(os.path.dirname(sys.executable), "bun"), # venv/bin
|
|
169
|
+
os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "bin", "bun") # venv/bin (alternative)
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
return any(shutil.which(loc) is not None for loc in possible_locations)
|
|
173
|
+
|
|
174
|
+
def get_bun_path(self) -> str:
|
|
175
|
+
"""Get the appropriate bun executable path"""
|
|
176
|
+
possible_locations = [
|
|
177
|
+
"bun",
|
|
178
|
+
os.path.join(os.path.dirname(sys.executable), "bun"),
|
|
179
|
+
os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "bin", "bun")
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
for loc in possible_locations:
|
|
183
|
+
if shutil.which(loc):
|
|
184
|
+
return shutil.which(loc)
|
|
185
|
+
|
|
186
|
+
raise EnvironmentError("Bun not found in environment")
|
|
187
|
+
|
|
188
|
+
def execute(self, program_file: str, input_data: str) -> Tuple[Union[Dict, str], Optional[str]]:
|
|
189
|
+
logger.info(f"Executing Bun script: {program_file}")
|
|
190
|
+
stats = Stats()
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
check_resource_limits(self.modules_dir)
|
|
194
|
+
|
|
195
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
196
|
+
temp_dir = Path(temp_dir)
|
|
197
|
+
self.setup_environment(temp_dir)
|
|
198
|
+
|
|
199
|
+
# Read user script
|
|
200
|
+
with open(program_file, 'r') as f:
|
|
201
|
+
user_script = f.read()
|
|
202
|
+
logger.debug("Loaded user script: %d characters", len(user_script))
|
|
203
|
+
|
|
204
|
+
# Create wrapper script
|
|
205
|
+
wrapper = self.create_wrapper(input_data, user_script)
|
|
206
|
+
script_path = temp_dir / "script.js"
|
|
207
|
+
with open(script_path, 'w') as f:
|
|
208
|
+
f.write(wrapper)
|
|
209
|
+
logger.debug("Generated wrapper script: %s", script_path)
|
|
210
|
+
|
|
211
|
+
# Execute with Bun
|
|
212
|
+
process = subprocess.Popen(
|
|
213
|
+
[
|
|
214
|
+
self.get_bun_path(),
|
|
215
|
+
"run",
|
|
216
|
+
"--no-fetch",
|
|
217
|
+
"--smol",
|
|
218
|
+
"--silent",
|
|
219
|
+
str(script_path)
|
|
220
|
+
],
|
|
221
|
+
stdout=subprocess.PIPE,
|
|
222
|
+
stderr=subprocess.PIPE,
|
|
223
|
+
text=True,
|
|
224
|
+
cwd=temp_dir
|
|
225
|
+
)
|
|
226
|
+
logger.info("Started Bun process PID %d", process.pid)
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
output, error = process.communicate(timeout=30)
|
|
230
|
+
logger.debug("Process completed with code %d", process.returncode)
|
|
231
|
+
except subprocess.TimeoutExpired as e:
|
|
232
|
+
logger.warning("Process timeout after 30 seconds")
|
|
233
|
+
process.kill()
|
|
234
|
+
return {"lam.error": "Script execution timed out"}, "Execution timed out after 30 seconds"
|
|
235
|
+
|
|
236
|
+
# Handle process errors
|
|
237
|
+
if process.returncode != 0:
|
|
238
|
+
try:
|
|
239
|
+
# Try to parse structured error from stderr
|
|
240
|
+
error_data = json.loads(error.strip())
|
|
241
|
+
error_msg = error_data.get('error', 'Unknown error')
|
|
242
|
+
stack = error_data.get('stack', [])
|
|
243
|
+
|
|
244
|
+
# Format error message
|
|
245
|
+
error_details = {
|
|
246
|
+
"lam.error": error_msg,
|
|
247
|
+
"stack_trace": stack
|
|
248
|
+
}
|
|
249
|
+
return error_details, error_msg
|
|
250
|
+
|
|
251
|
+
except json.JSONDecodeError:
|
|
252
|
+
# Fallback to raw error output
|
|
253
|
+
error_msg = error.strip() or "Unknown error"
|
|
254
|
+
return {"lam.error": error_msg}, error_msg
|
|
255
|
+
|
|
256
|
+
# Handle successful output
|
|
257
|
+
try:
|
|
258
|
+
output_data = json.loads(output)
|
|
259
|
+
|
|
260
|
+
# Process JavaScript logs (if any)
|
|
261
|
+
if 'logs' in output_data:
|
|
262
|
+
for log_entry in output_data.get('logs', []):
|
|
263
|
+
if log_entry['type'] == 'error':
|
|
264
|
+
logger.error("[JS] %s", log_entry['message'])
|
|
265
|
+
else:
|
|
266
|
+
logger.debug("[JS] %s", log_entry['message'])
|
|
267
|
+
|
|
268
|
+
result = output_data.get('result', {})
|
|
269
|
+
return result, None
|
|
270
|
+
|
|
271
|
+
except json.JSONDecodeError as e:
|
|
272
|
+
logger.error("Failed to parse output: %s", str(e))
|
|
273
|
+
return {
|
|
274
|
+
"lam.error": "Invalid JSON output",
|
|
275
|
+
"raw_output": output.strip()
|
|
276
|
+
}, "Output format error"
|
|
277
|
+
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.exception("Execution failed")
|
|
280
|
+
return {
|
|
281
|
+
"lam.error": str(e),
|
|
282
|
+
"type": e.__class__.__name__
|
|
283
|
+
}, str(e)
|
lam/engines/jq.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from typing import Dict, Optional, Tuple, Union
|
|
6
|
+
|
|
7
|
+
from ..core import ProcessingError
|
|
8
|
+
from .base import Engine
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class JQEngine(Engine):
|
|
14
|
+
"""JQ execution engine"""
|
|
15
|
+
def validate_environment(self) -> bool:
|
|
16
|
+
logger.debug("Validating JQ environment")
|
|
17
|
+
return shutil.which("jq") is not None
|
|
18
|
+
|
|
19
|
+
def execute(self, program_file: str, input_data: str) -> Tuple[Union[Dict, str], Optional[str]]:
|
|
20
|
+
logger.info(f"Executing JQ script: {program_file}")
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
with open(program_file, 'r') as file:
|
|
24
|
+
jq_script = ''.join(line for line in file if not line.strip().startswith('#'))
|
|
25
|
+
logger.debug("Loaded JQ script: %d characters", len(jq_script))
|
|
26
|
+
|
|
27
|
+
process = subprocess.Popen(
|
|
28
|
+
["jq", "-c", jq_script],
|
|
29
|
+
stdin=subprocess.PIPE,
|
|
30
|
+
stdout=subprocess.PIPE,
|
|
31
|
+
stderr=subprocess.PIPE,
|
|
32
|
+
text=True
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
logger.debug("Starting JQ process PID %d", process.pid)
|
|
36
|
+
output, error = process.communicate(input=input_data)
|
|
37
|
+
|
|
38
|
+
if error:
|
|
39
|
+
logger.error("JQ error output: %s", error.strip())
|
|
40
|
+
raise ProcessingError(error)
|
|
41
|
+
|
|
42
|
+
# Handle output
|
|
43
|
+
try:
|
|
44
|
+
output_lines = [line.strip() for line in output.splitlines() if line.strip()]
|
|
45
|
+
logger.debug(f"Found {len(output_lines)} JSON objects in output")
|
|
46
|
+
|
|
47
|
+
if len(output_lines) > 1:
|
|
48
|
+
parsed = [json.loads(line) for line in output_lines]
|
|
49
|
+
logger.info(f"Processed {len(parsed)} JSON objects")
|
|
50
|
+
return {"lam.result": parsed}, None
|
|
51
|
+
elif len(output_lines) == 1:
|
|
52
|
+
result = json.loads(output_lines[0])
|
|
53
|
+
logger.info("Processed single JSON object")
|
|
54
|
+
return result, None
|
|
55
|
+
else:
|
|
56
|
+
logger.info("No JSON objects in output")
|
|
57
|
+
return {"lam.error": "No JSON objects in output"}, "No JSON objects in output"
|
|
58
|
+
|
|
59
|
+
except json.JSONDecodeError as e:
|
|
60
|
+
return {"lam.result": output}, None
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.exception("JQ execution failed")
|
|
64
|
+
self.track_event('lam.jq.error', {'error': str(e)})
|
|
65
|
+
return {"lam.error": str(e)}, str(e)
|