lam-cli 0.0.7__py3-none-any.whl → 0.1.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.
- lam/lam.py +455 -108
- lam_cli-0.1.0.dist-info/METADATA +196 -0
- lam_cli-0.1.0.dist-info/RECORD +8 -0
- {lam_cli-0.0.7.dist-info → lam_cli-0.1.0.dist-info}/WHEEL +1 -1
- lam_cli-0.0.7.dist-info/METADATA +0 -16
- lam_cli-0.0.7.dist-info/RECORD +0 -8
- {lam_cli-0.0.7.dist-info → lam_cli-0.1.0.dist-info}/LICENSE +0 -0
- {lam_cli-0.0.7.dist-info → lam_cli-0.1.0.dist-info}/entry_points.txt +0 -0
- {lam_cli-0.0.7.dist-info → lam_cli-0.1.0.dist-info}/top_level.txt +0 -0
lam/lam.py
CHANGED
|
@@ -6,153 +6,500 @@ import os
|
|
|
6
6
|
import shutil
|
|
7
7
|
import socket
|
|
8
8
|
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
9
11
|
from datetime import datetime
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, Optional, Tuple, Union
|
|
10
15
|
|
|
11
16
|
import click
|
|
17
|
+
import psutil
|
|
12
18
|
from logtail import LogtailHandler
|
|
13
19
|
from posthog import Posthog
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
# Initialize analytics and logging
|
|
22
|
+
posthog = Posthog(project_api_key='phc_wfeHFG0p5yZIdBpjVYy00o5x1HbEpggdMzIuFYgNPSK',
|
|
23
|
+
host='https://app.posthog.com')
|
|
16
24
|
|
|
17
|
-
logging.basicConfig(level=logging.INFO,
|
|
25
|
+
logging.basicConfig(level=logging.INFO,
|
|
26
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
18
27
|
logger = logging.getLogger(__name__)
|
|
19
28
|
|
|
20
29
|
handler = LogtailHandler(source_token="TYz3WrrvC8ehYjXdAEGGyiDp")
|
|
21
30
|
logger.addHandler(handler)
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"""
|
|
27
|
-
Truncate long strings in a JSON object if they exceed max_length.
|
|
28
|
-
Append a message to indicate truncation.
|
|
29
|
-
"""
|
|
30
|
-
if isinstance(data, dict):
|
|
31
|
-
return {key: truncate_long_strings(value, max_length, truncation_msg) for key, value in data.items()}
|
|
32
|
-
elif isinstance(data, list):
|
|
33
|
-
return [truncate_long_strings(item, max_length, truncation_msg) for item in data]
|
|
34
|
-
elif isinstance(data, str):
|
|
35
|
-
return data[:max_length] + truncation_msg if len(data) > max_length else data
|
|
36
|
-
return data
|
|
37
|
-
|
|
38
|
-
def generate_distinct_id(workspace_id, flow_id):
|
|
39
|
-
user_id = os.getuid()
|
|
40
|
-
hostname = socket.gethostname()
|
|
41
|
-
return f"{user_id}_{hostname}_{workspace_id}_{flow_id}"
|
|
42
|
-
|
|
43
|
-
def track_event(event_name, properties, workspace_id="local", flow_id="local"):
|
|
44
|
-
logger.info(f"Event {event_name} triggered, with properties: {properties}")
|
|
32
|
+
class LAMError(Exception):
|
|
33
|
+
"""Base exception for LAM errors"""
|
|
34
|
+
pass
|
|
45
35
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
36
|
+
class UserError(LAMError):
|
|
37
|
+
"""Errors caused by user input"""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
class SystemError(LAMError):
|
|
41
|
+
"""Errors caused by system issues"""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
class ResourceLimitError(LAMError):
|
|
45
|
+
"""Errors caused by resource limits"""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
def check_resource_limits(modules_dir: Optional[Path] = None) -> None:
|
|
49
|
+
"""Check system resource availability"""
|
|
50
|
+
# Check disk space
|
|
51
|
+
disk = shutil.disk_usage(tempfile.gettempdir())
|
|
52
|
+
if disk.free < 100 * 1024 * 1024: # 100MB minimum
|
|
53
|
+
raise ResourceLimitError("Insufficient disk space")
|
|
54
|
+
|
|
55
|
+
# Check shared modules size if provided
|
|
56
|
+
if modules_dir and modules_dir.exists():
|
|
57
|
+
modules_size = sum(
|
|
58
|
+
os.path.getsize(os.path.join(dirpath, filename))
|
|
59
|
+
for dirpath, _, filenames in os.walk(modules_dir)
|
|
60
|
+
for filename in filenames
|
|
61
|
+
)
|
|
62
|
+
if modules_size > 500 * 1024 * 1024: # 500MB limit
|
|
63
|
+
logger.warning("Shared modules exceeding size limit, cleaning up")
|
|
64
|
+
shutil.rmtree(modules_dir)
|
|
65
|
+
modules_dir.mkdir(exist_ok=True)
|
|
66
|
+
|
|
67
|
+
class Stats:
|
|
68
|
+
"""Track execution statistics"""
|
|
69
|
+
def __init__(self):
|
|
70
|
+
self.start_time = datetime.now()
|
|
71
|
+
self.memory_start = self.get_memory_usage()
|
|
72
|
+
|
|
73
|
+
def get_memory_usage(self):
|
|
74
|
+
import psutil
|
|
75
|
+
process = psutil.Process()
|
|
76
|
+
return process.memory_info().rss
|
|
77
|
+
|
|
78
|
+
def finalize(self):
|
|
79
|
+
return {
|
|
80
|
+
'duration_ms': (datetime.now() - self.start_time).total_seconds() * 1000,
|
|
81
|
+
'memory_used_mb': (self.get_memory_usage() - self.memory_start) / (1024 * 1024),
|
|
82
|
+
'timestamp': datetime.now().isoformat()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
class EngineType(Enum):
|
|
86
|
+
JQ = "jq"
|
|
87
|
+
JAVASCRIPT = "js"
|
|
88
|
+
|
|
89
|
+
class ProcessingError(Exception):
|
|
90
|
+
"""Custom exception for processing errors"""
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
class Engine:
|
|
94
|
+
"""Base class for execution engines"""
|
|
95
|
+
def __init__(self, workspace_id: str, flow_id: str, execution_id: str):
|
|
96
|
+
self.workspace_id = workspace_id
|
|
97
|
+
self.flow_id = flow_id
|
|
98
|
+
self.execution_id = execution_id
|
|
99
|
+
self.timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
100
|
+
|
|
101
|
+
def get_log_file(self) -> str:
|
|
102
|
+
return f"lam_run_{self.workspace_id}_{self.flow_id}_{self.execution_id}_{self.timestamp}.log"
|
|
103
|
+
|
|
104
|
+
def get_result_file(self) -> str:
|
|
105
|
+
return f"lam_result_{self.workspace_id}_{self.flow_id}_{self.execution_id}_{self.timestamp}.json"
|
|
106
|
+
|
|
107
|
+
def track_event(self, event_name: str, properties: Dict[str, Any]) -> None:
|
|
108
|
+
"""Track events with PostHog"""
|
|
109
|
+
try:
|
|
110
|
+
distinct_id = f"{os.getuid()}_{socket.gethostname()}_{self.workspace_id}_{self.flow_id}"
|
|
111
|
+
properties |= {
|
|
112
|
+
'workspace_id': self.workspace_id,
|
|
113
|
+
'flow_id': self.flow_id,
|
|
114
|
+
'engine': self.__class__.__name__,
|
|
115
|
+
}
|
|
116
|
+
posthog.capture(distinct_id=distinct_id, event=event_name, properties=properties)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.error(f"Error tracking event: {e}")
|
|
119
|
+
|
|
120
|
+
class JQEngine(Engine):
|
|
121
|
+
"""JQ execution engine"""
|
|
122
|
+
def validate_environment(self) -> bool:
|
|
123
|
+
return shutil.which("jq") is not None
|
|
124
|
+
|
|
125
|
+
def execute(self, program_file: str, input_data: str) -> Tuple[Union[Dict, str], Optional[str]]:
|
|
126
|
+
logger.info(f"Executing JQ script: {program_file}")
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
# Parse JQ program
|
|
130
|
+
with open(program_file, 'r') as file:
|
|
131
|
+
jq_script = ''.join(line for line in file if not line.strip().startswith('#'))
|
|
132
|
+
|
|
133
|
+
# Run JQ
|
|
134
|
+
process = subprocess.Popen(
|
|
135
|
+
["jq", "-c", jq_script],
|
|
136
|
+
stdin=subprocess.PIPE,
|
|
137
|
+
stdout=subprocess.PIPE,
|
|
138
|
+
stderr=subprocess.PIPE,
|
|
139
|
+
text=True
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
output, error = process.communicate(input=input_data)
|
|
143
|
+
|
|
144
|
+
if error:
|
|
145
|
+
raise ProcessingError(error)
|
|
146
|
+
|
|
147
|
+
# Handle output
|
|
148
|
+
try:
|
|
149
|
+
return json.loads(output), None
|
|
150
|
+
except json.JSONDecodeError:
|
|
151
|
+
return {"lam.result": output}, None
|
|
152
|
+
|
|
153
|
+
except Exception as e:
|
|
154
|
+
self.track_event('lam.jq.error', {'error': str(e)})
|
|
155
|
+
return {"lam.error": str(e)}, str(e)
|
|
156
|
+
|
|
157
|
+
class BunEngine(Engine):
|
|
158
|
+
"""Bun JavaScript execution engine"""
|
|
159
|
+
def __init__(self, *args, **kwargs):
|
|
160
|
+
super().__init__(*args, **kwargs)
|
|
161
|
+
# Create a persistent temp directory for node_modules
|
|
162
|
+
self.modules_dir = Path(tempfile.gettempdir()) / "lam_modules"
|
|
163
|
+
self.modules_dir.mkdir(exist_ok=True)
|
|
164
|
+
self._setup_shared_modules()
|
|
165
|
+
|
|
166
|
+
self.runtime_template = '''
|
|
167
|
+
// Secure runtime environment
|
|
168
|
+
globalThis.process = undefined;
|
|
169
|
+
globalThis.Deno = undefined;
|
|
170
|
+
globalThis.fetch = undefined;
|
|
171
|
+
|
|
172
|
+
import _ from 'lodash';
|
|
173
|
+
import { format, parseISO } from 'date-fns';
|
|
174
|
+
|
|
175
|
+
// Safe console methods
|
|
176
|
+
const secureConsole = {
|
|
177
|
+
log: console.log,
|
|
178
|
+
error: console.error,
|
|
179
|
+
warn: console.warn
|
|
180
|
+
};
|
|
181
|
+
globalThis.console = secureConsole;
|
|
182
|
+
|
|
183
|
+
// Expose safe utilities
|
|
184
|
+
globalThis._ = _;
|
|
185
|
+
globalThis.format = format;
|
|
186
|
+
globalThis.parseISO = parseISO;
|
|
187
|
+
'''
|
|
188
|
+
|
|
189
|
+
def _setup_shared_modules(self):
|
|
190
|
+
"""Setup shared node_modules once"""
|
|
191
|
+
if not (self.modules_dir / "node_modules").exists():
|
|
192
|
+
# Create package.json
|
|
193
|
+
package_json = {
|
|
194
|
+
"type": "module",
|
|
195
|
+
"dependencies": {
|
|
196
|
+
"lodash": "^4.17.21",
|
|
197
|
+
"date-fns": "^2.30.0"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
with open(self.modules_dir / "package.json", "w") as f:
|
|
201
|
+
json.dump(package_json, f, indent=2)
|
|
202
|
+
|
|
203
|
+
# Install dependencies once
|
|
204
|
+
try:
|
|
205
|
+
subprocess.run(
|
|
206
|
+
[self.get_bun_path(), "install"],
|
|
207
|
+
cwd=self.modules_dir,
|
|
208
|
+
check=True,
|
|
209
|
+
capture_output=True,
|
|
210
|
+
text=True,
|
|
211
|
+
timeout=30 # Reasonable timeout for installation
|
|
212
|
+
)
|
|
213
|
+
except subprocess.CalledProcessError as e:
|
|
214
|
+
logger.error(f"Failed to install shared dependencies: {e.stderr}")
|
|
215
|
+
raise ProcessingError(
|
|
216
|
+
f"Failed to set up JavaScript environment: {e.stderr}"
|
|
217
|
+
) from e
|
|
218
|
+
|
|
219
|
+
def create_wrapper(self, input_data: str, user_script: str) -> str:
|
|
220
|
+
"""Create the wrapper script with proper escaping"""
|
|
221
|
+
return f'''
|
|
222
|
+
import './runtime.js';
|
|
223
|
+
|
|
224
|
+
// Utility function to handle circular references in JSON.stringify
|
|
225
|
+
function safeStringify(obj) {{
|
|
226
|
+
const seen = new WeakSet();
|
|
227
|
+
return JSON.stringify(obj, (key, value) => {{
|
|
228
|
+
if (typeof value === 'object' && value !== null) {{
|
|
229
|
+
if (seen.has(value)) {{
|
|
230
|
+
return '[Circular Reference]';
|
|
231
|
+
}}
|
|
232
|
+
seen.add(value);
|
|
233
|
+
}}
|
|
234
|
+
return value;
|
|
235
|
+
}}, 2);
|
|
236
|
+
}}
|
|
237
|
+
|
|
238
|
+
// Validate transform function
|
|
239
|
+
function validateTransform(fn) {{
|
|
240
|
+
if (typeof fn !== 'function') {{
|
|
241
|
+
throw new Error('Transform must be a function');
|
|
242
|
+
}}
|
|
243
|
+
if (fn.length !== 1) {{
|
|
244
|
+
throw new Error('Transform function must accept exactly one argument (input)');
|
|
245
|
+
}}
|
|
246
|
+
}}
|
|
247
|
+
|
|
248
|
+
// Execute transform immediately
|
|
249
|
+
try {{
|
|
250
|
+
// Parse input safely
|
|
251
|
+
let input;
|
|
252
|
+
try {{
|
|
253
|
+
input = {input_data};
|
|
254
|
+
}} catch (e) {{
|
|
255
|
+
throw new Error(`Failed to parse input data: ${{e.message}}`);
|
|
256
|
+
}}
|
|
257
|
+
|
|
258
|
+
// Get transform function
|
|
259
|
+
let transform;
|
|
260
|
+
try {{
|
|
261
|
+
transform = {user_script};
|
|
262
|
+
}} catch (e) {{
|
|
263
|
+
throw new Error(`Failed to parse transform function: ${{e.message}}`);
|
|
264
|
+
}}
|
|
265
|
+
|
|
266
|
+
// Validate transform
|
|
267
|
+
validateTransform(transform);
|
|
268
|
+
|
|
269
|
+
// Execute transform
|
|
270
|
+
const result = transform(input);
|
|
271
|
+
|
|
272
|
+
// Validate result is serializable
|
|
273
|
+
try {{
|
|
274
|
+
const serialized = safeStringify(result);
|
|
275
|
+
console.log(serialized);
|
|
276
|
+
}} catch (e) {{
|
|
277
|
+
throw new Error(`Result is not JSON serializable: ${{e.message}}`);
|
|
278
|
+
}}
|
|
279
|
+
}} catch (error) {{
|
|
280
|
+
console.error(JSON.stringify({{
|
|
281
|
+
error: error.message,
|
|
282
|
+
stack: error.stack,
|
|
283
|
+
type: error.constructor.name
|
|
284
|
+
}}));
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}}
|
|
287
|
+
'''
|
|
288
|
+
|
|
289
|
+
def setup_environment(self, temp_dir: Path) -> None:
|
|
290
|
+
"""Set up the JavaScript environment with runtime"""
|
|
291
|
+
# Write runtime file only
|
|
292
|
+
runtime_path = temp_dir / "runtime.js"
|
|
293
|
+
with open(runtime_path, "w") as f:
|
|
294
|
+
f.write(self.runtime_template)
|
|
295
|
+
|
|
296
|
+
# Symlink node_modules from shared directory
|
|
297
|
+
os.symlink(self.modules_dir / "node_modules", temp_dir / "node_modules")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def validate_environment(self) -> bool:
|
|
301
|
+
# Check multiple locations for bun
|
|
302
|
+
possible_locations = [
|
|
303
|
+
"bun", # System PATH
|
|
304
|
+
os.path.join(os.path.dirname(sys.executable), "bun"), # venv/bin
|
|
305
|
+
os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "bin", "bun") # venv/bin (alternative)
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
return any(shutil.which(loc) is not None for loc in possible_locations)
|
|
309
|
+
|
|
310
|
+
def get_bun_path(self) -> str:
|
|
311
|
+
"""Get the appropriate bun executable path"""
|
|
312
|
+
possible_locations = [
|
|
313
|
+
"bun",
|
|
314
|
+
os.path.join(os.path.dirname(sys.executable), "bun"),
|
|
315
|
+
os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "bin", "bun")
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
for loc in possible_locations:
|
|
319
|
+
if shutil.which(loc):
|
|
320
|
+
return shutil.which(loc)
|
|
321
|
+
|
|
322
|
+
raise EnvironmentError("Bun not found in environment")
|
|
323
|
+
|
|
324
|
+
def execute(self, program_file: str, input_data: str) -> Tuple[Union[Dict, str], Optional[str]]:
|
|
325
|
+
logger.info(f"Executing Bun script: {program_file}")
|
|
326
|
+
stats = Stats()
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
check_resource_limits(self.modules_dir)
|
|
330
|
+
|
|
331
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
332
|
+
temp_dir = Path(temp_dir)
|
|
333
|
+
self.setup_environment(temp_dir)
|
|
334
|
+
|
|
335
|
+
# Read user script
|
|
336
|
+
with open(program_file, 'r') as f:
|
|
337
|
+
user_script = f.read()
|
|
338
|
+
|
|
339
|
+
# Create wrapper script
|
|
340
|
+
wrapper = self.create_wrapper(input_data, user_script)
|
|
341
|
+
|
|
342
|
+
script_path = temp_dir / "script.js"
|
|
343
|
+
with open(script_path, "w") as f:
|
|
344
|
+
f.write(wrapper)
|
|
345
|
+
|
|
346
|
+
# Execute with Bun
|
|
347
|
+
process = subprocess.Popen(
|
|
348
|
+
[
|
|
349
|
+
self.get_bun_path(),
|
|
350
|
+
"run",
|
|
351
|
+
"--no-fetch", # Disable network
|
|
352
|
+
"--smol", # Reduced memory
|
|
353
|
+
"--silent", # Reduce Bun's own error noise
|
|
354
|
+
str(script_path)
|
|
355
|
+
],
|
|
356
|
+
stdout=subprocess.PIPE,
|
|
357
|
+
stderr=subprocess.PIPE,
|
|
358
|
+
text=True,
|
|
359
|
+
cwd=temp_dir
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
output, error = process.communicate(timeout=5) # 5 second timeout
|
|
364
|
+
except subprocess.TimeoutExpired as e:
|
|
365
|
+
process.kill()
|
|
366
|
+
raise ProcessingError("Script execution timed out") from e
|
|
367
|
+
|
|
368
|
+
if error:
|
|
369
|
+
try:
|
|
370
|
+
error_data = json.loads(error)
|
|
371
|
+
error_msg = error_data.get('error', 'Unknown error')
|
|
372
|
+
if error_data.get('stack'):
|
|
373
|
+
error_msg = f"{error_msg}\nStack trace:\n{error_data['stack']}"
|
|
374
|
+
except json.JSONDecodeError:
|
|
375
|
+
error_msg = error.split('\n')[0] # Just take the first line if not JSON
|
|
376
|
+
raise ProcessingError(error_msg)
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
return json.loads(output), None
|
|
380
|
+
except json.JSONDecodeError:
|
|
381
|
+
return {"lam.result": output}, None
|
|
382
|
+
|
|
383
|
+
except Exception as e:
|
|
384
|
+
stats_data = stats.finalize()
|
|
385
|
+
self.track_event('lam.bun.error', {
|
|
386
|
+
'error': str(e),
|
|
387
|
+
'error_type': e.__class__.__name__,
|
|
388
|
+
**stats_data
|
|
389
|
+
})
|
|
390
|
+
return {"lam.error": str(e)}, str(e)
|
|
391
|
+
|
|
392
|
+
def get_engine(engine_type: str, workspace_id: str, flow_id: str, execution_id: str) -> Engine:
|
|
393
|
+
"""Factory function to get the appropriate execution engine"""
|
|
394
|
+
engines = {
|
|
395
|
+
EngineType.JQ.value: JQEngine,
|
|
396
|
+
EngineType.JAVASCRIPT.value: BunEngine
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
engine_class = engines.get(engine_type)
|
|
400
|
+
if not engine_class:
|
|
401
|
+
raise ValueError(f"Unsupported engine type: {engine_type}")
|
|
402
|
+
|
|
403
|
+
engine = engine_class(workspace_id, flow_id, execution_id)
|
|
404
|
+
if not engine.validate_environment():
|
|
405
|
+
raise EnvironmentError(f"Required dependencies not found for {engine_type}")
|
|
406
|
+
|
|
407
|
+
return engine
|
|
408
|
+
|
|
409
|
+
def process_input(input: str) -> Tuple[str, Optional[str]]:
|
|
410
|
+
"""Process and validate input data"""
|
|
67
411
|
if os.path.isfile(input):
|
|
68
412
|
with open(input, 'r') as file:
|
|
69
413
|
return file.read(), None
|
|
414
|
+
|
|
70
415
|
try:
|
|
71
416
|
json.loads(input)
|
|
72
417
|
return input, None
|
|
73
418
|
except json.JSONDecodeError as e:
|
|
74
|
-
logger.error(f"Invalid JSON input: {e}")
|
|
75
|
-
track_event('lam.run.error', {'error': f"Invalid JSON input: {e}", 'workspace_id': workspace_id, 'flow_id': flow_id}, workspace_id, flow_id)
|
|
76
419
|
return None, str(e)
|
|
77
420
|
|
|
78
|
-
def handle_jq_output(output, as_json, workspace_id, flow_id):
|
|
79
|
-
logger.info(f"Handling jq output: {truncate_long_strings(output)}")
|
|
80
|
-
try:
|
|
81
|
-
json_output = json.loads(output)
|
|
82
|
-
if not isinstance(json_output, dict):
|
|
83
|
-
track_event('lam.run.warn', {'error': 'Invalid JSON output', 'workspace_id': workspace_id, 'flow_id': flow_id}, workspace_id, flow_id)
|
|
84
|
-
return {"lam.result": json_output} if as_json else output, None
|
|
85
|
-
return json_output if as_json else output, None
|
|
86
|
-
except json.JSONDecodeError as e:
|
|
87
|
-
logger.error("Failed to parse JSON output, may be multiple JSON objects. Attempting to parse as JSON lines.")
|
|
88
|
-
track_event('lam.run.warn', {'error': f"Invalid JSON output: {e}", 'workspace_id': workspace_id, 'flow_id': flow_id}, workspace_id, flow_id)
|
|
89
|
-
if as_json:
|
|
90
|
-
json_objects = [json.loads(line) for line in output.strip().split('\n') if line]
|
|
91
|
-
return {"lam.concatenated": json_objects}, None
|
|
92
|
-
return output, "Failed to parse JSON output."
|
|
93
|
-
|
|
94
|
-
def write_to_result_file(result, result_file):
|
|
95
|
-
with open(result_file, 'w') as file:
|
|
96
|
-
file.write(json.dumps(result, indent=4))
|
|
97
|
-
|
|
98
421
|
@click.group()
|
|
99
422
|
def lam():
|
|
423
|
+
"""LAM - Laminar Data Transformation Tool"""
|
|
100
424
|
pass
|
|
101
425
|
|
|
102
426
|
@lam.command()
|
|
103
427
|
@click.argument('program_file', type=click.Path(exists=True))
|
|
104
428
|
@click.argument('input', type=str)
|
|
429
|
+
@click.option('--language', type=click.Choice(['jq', 'js']), default='jq',
|
|
430
|
+
help='Script language (default: jq)')
|
|
105
431
|
@click.option('--workspace_id', default="local", help="Workspace ID")
|
|
106
432
|
@click.option('--flow_id', default="local", help="Flow ID")
|
|
107
433
|
@click.option('--execution_id', default="local", help="Execution ID")
|
|
108
434
|
@click.option('--as-json', is_flag=True, default=True, help="Output as JSON")
|
|
109
|
-
def run(program_file, input
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
435
|
+
def run(program_file: str, input: str, language: str, workspace_id: str,
|
|
436
|
+
flow_id: str, execution_id: str, as_json: bool):
|
|
437
|
+
"""Execute a LAM transformation script"""
|
|
438
|
+
stats = Stats() # Start tracking stats at the top level
|
|
439
|
+
|
|
440
|
+
# Initialize engine
|
|
441
|
+
try:
|
|
442
|
+
engine = get_engine(language, workspace_id, flow_id, execution_id)
|
|
443
|
+
except (ValueError, EnvironmentError) as e:
|
|
444
|
+
click.echo({"lam.error": str(e)}, err=True)
|
|
445
|
+
return
|
|
113
446
|
|
|
447
|
+
# Setup logging
|
|
448
|
+
log_file = engine.get_log_file()
|
|
449
|
+
result_file = engine.get_result_file()
|
|
450
|
+
|
|
114
451
|
file_handler = logging.FileHandler(log_file, 'w')
|
|
115
452
|
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
|
|
116
453
|
logger.addHandler(file_handler)
|
|
454
|
+
|
|
455
|
+
logger.info(f"Starting LAM execution with {language} engine")
|
|
456
|
+
engine.track_event('lam.run.start', {
|
|
457
|
+
'language': language,
|
|
458
|
+
'program_file': program_file
|
|
459
|
+
})
|
|
117
460
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
track_event('lam.run.error', {'error': 'jq is not installed', 'workspace_id': workspace_id, 'flow_id': flow_id}, workspace_id, flow_id)
|
|
124
|
-
write_to_result_file({"lam.error": "jq is not installed"}, result_file)
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
input_data, error = process_input(input, workspace_id, flow_id)
|
|
128
|
-
if error:
|
|
129
|
-
click.echo({"lam.error": f"Invalid input: {error}"}, err=True)
|
|
130
|
-
track_event('lam.run.error', {'error': f"Invalid input: {error}", 'workspace_id': workspace_id, 'flow_id': flow_id}, workspace_id, flow_id)
|
|
131
|
-
write_to_result_file({"lam.error": f"Invalid input: {error}"}, result_file)
|
|
132
|
-
return
|
|
133
|
-
|
|
134
|
-
jq_script = parse_program_file(program_file)
|
|
135
|
-
track_event('lam.run.start', {'program_file': program_file, 'as_json': as_json, 'workspace_id': workspace_id, 'flow_id': flow_id}, workspace_id, flow_id)
|
|
136
|
-
output, jq_error = run_jq(jq_script, input_data)
|
|
137
|
-
|
|
138
|
-
if jq_error:
|
|
139
|
-
click.echo({"lam.error": jq_error}, err=True)
|
|
140
|
-
track_event('lam.run.run_jq_error', {'error': jq_error, 'workspace_id': workspace_id, 'flow_id': flow_id}, workspace_id, flow_id)
|
|
141
|
-
write_to_result_file({"lam.error": jq_error}, result_file)
|
|
142
|
-
return
|
|
461
|
+
try:
|
|
462
|
+
# Process input
|
|
463
|
+
input_data, error = process_input(input)
|
|
464
|
+
if error:
|
|
465
|
+
raise ProcessingError(f"Invalid input: {error}")
|
|
143
466
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
467
|
+
# Execute transformation
|
|
468
|
+
result, error = engine.execute(program_file, input_data)
|
|
469
|
+
|
|
470
|
+
# Get final stats
|
|
471
|
+
stats_data = stats.finalize()
|
|
472
|
+
logger.info(f"Execution stats: duration={stats_data['duration_ms']:.2f}ms, "
|
|
473
|
+
f"memory_used={stats_data['memory_used_mb']:.2f}MB")
|
|
474
|
+
|
|
475
|
+
if error:
|
|
476
|
+
click.echo({"lam.error": error}, err=True)
|
|
477
|
+
engine.track_event('lam.run.error', {'error': error, **stats_data})
|
|
478
|
+
else:
|
|
479
|
+
output = json.dumps(result, indent=4) if as_json else result
|
|
480
|
+
click.echo(output)
|
|
481
|
+
engine.track_event('lam.run.success', stats_data)
|
|
482
|
+
|
|
483
|
+
# Save result with stats
|
|
484
|
+
result_with_stats = {
|
|
485
|
+
'result': result,
|
|
486
|
+
'stats': stats_data,
|
|
487
|
+
'error': error or None
|
|
488
|
+
}
|
|
489
|
+
with open(result_file, 'w') as f:
|
|
490
|
+
json.dump(result_with_stats, f, indent=4)
|
|
491
|
+
|
|
492
|
+
except Exception as e:
|
|
493
|
+
stats_data = stats.finalize()
|
|
494
|
+
logger.error(f"Execution failed: {e}")
|
|
495
|
+
logger.error(f"Final stats: duration={stats_data['duration_ms']:.2f}ms, "
|
|
496
|
+
f"memory_used={stats_data['memory_used_mb']:.2f}MB")
|
|
497
|
+
click.echo({"lam.error": str(e)}, err=True)
|
|
498
|
+
engine.track_event('lam.run.error', {'error': str(e), **stats_data})
|
|
499
|
+
|
|
500
|
+
finally:
|
|
501
|
+
logger.info("Execution complete")
|
|
502
|
+
logger.removeHandler(file_handler)
|
|
156
503
|
|
|
157
504
|
if __name__ == '__main__':
|
|
158
505
|
lam()
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: lam-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Secure data transformation tool supporting JQ and JavaScript (Bun)
|
|
5
|
+
Home-page: https://github.com/laminar-run/lam
|
|
6
|
+
Author: Laminar Run, Inc.
|
|
7
|
+
Author-email: connect@laminar.run
|
|
8
|
+
License: GPLv3
|
|
9
|
+
Project-URL: Documentation, https://docs.laminar.run
|
|
10
|
+
Project-URL: Source, https://github.com/laminar-run/lam
|
|
11
|
+
Project-URL: Issue Tracker, https://github.com/laminar-run/lam/issues
|
|
12
|
+
Keywords: laminar,api,integration,transformation,json,jq,javascript,bun
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
24
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: backoff>=2.2.1
|
|
29
|
+
Requires-Dist: certifi>=2024.12.14
|
|
30
|
+
Requires-Dist: charset-normalizer>=3.3.2
|
|
31
|
+
Requires-Dist: click>=8.1.7
|
|
32
|
+
Requires-Dist: idna>=3.7
|
|
33
|
+
Requires-Dist: logtail-python>=0.2.2
|
|
34
|
+
Requires-Dist: monotonic>=1.6
|
|
35
|
+
Requires-Dist: msgpack>=1.0.8
|
|
36
|
+
Requires-Dist: posthog>=3.4.0
|
|
37
|
+
Requires-Dist: psutil>=5.9.0
|
|
38
|
+
Requires-Dist: python-dateutil>=2.8.2
|
|
39
|
+
Requires-Dist: requests>=2.32.3
|
|
40
|
+
Requires-Dist: six>=1.16.0
|
|
41
|
+
Requires-Dist: urllib3>=2.2.2
|
|
42
|
+
Dynamic: author
|
|
43
|
+
Dynamic: author-email
|
|
44
|
+
Dynamic: classifier
|
|
45
|
+
Dynamic: description
|
|
46
|
+
Dynamic: description-content-type
|
|
47
|
+
Dynamic: home-page
|
|
48
|
+
Dynamic: keywords
|
|
49
|
+
Dynamic: license
|
|
50
|
+
Dynamic: project-url
|
|
51
|
+
Dynamic: requires-dist
|
|
52
|
+
Dynamic: requires-python
|
|
53
|
+
Dynamic: summary
|
|
54
|
+
|
|
55
|
+
# lam
|
|
56
|
+
Lam is a data transformation tool for Laminar that supports both `jq` and JavaScript transformations using Bun.
|
|
57
|
+
|
|
58
|
+
## Quickstart
|
|
59
|
+
Install the dependencies:
|
|
60
|
+
```bash
|
|
61
|
+
# For JQ support
|
|
62
|
+
brew install jq # or sudo apt-get install jq
|
|
63
|
+
|
|
64
|
+
# For JavaScript support
|
|
65
|
+
curl -fsSL https://bun.sh/install | bash
|
|
66
|
+
|
|
67
|
+
make setup
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Run the CLI tool:
|
|
71
|
+
```bash
|
|
72
|
+
make cli ARGS="run <program> <input> [--language jq|js]"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Features
|
|
76
|
+
- JQ transformations (default)
|
|
77
|
+
- JavaScript transformations with Bun runtime
|
|
78
|
+
- Built-in utilities (lodash, date-fns)
|
|
79
|
+
- Resource monitoring and limits
|
|
80
|
+
- Detailed execution statistics
|
|
81
|
+
- Secure execution environment
|
|
82
|
+
|
|
83
|
+
## Examples
|
|
84
|
+
|
|
85
|
+
### JQ Transform
|
|
86
|
+
```bash
|
|
87
|
+
make cli ARGS="run examples/transform.jq data.json"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### JavaScript Transform
|
|
91
|
+
```bash
|
|
92
|
+
make cli ARGS="run examples/transform.js data.json --language js"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Example JavaScript transform:
|
|
96
|
+
```javascript
|
|
97
|
+
(input) => {
|
|
98
|
+
// Lodash available as _
|
|
99
|
+
return _.map(input.data, item => ({
|
|
100
|
+
value: item.value * 2
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Installation
|
|
106
|
+
|
|
107
|
+
### Docker Installation
|
|
108
|
+
```dockerfile
|
|
109
|
+
# Install lam-cli
|
|
110
|
+
RUN pip3 install git+https://${GITHUB_USER}:${GITHUB_TOKEN}@github.com/user/project.git@{version}
|
|
111
|
+
|
|
112
|
+
# Install dependencies
|
|
113
|
+
RUN apt-get update && apt-get install -y jq
|
|
114
|
+
RUN curl -fsSL https://bun.sh/install | bash
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Manual Setup
|
|
118
|
+
Create a virtual environment and install dependencies:
|
|
119
|
+
```bash
|
|
120
|
+
python3 -m venv ./venv
|
|
121
|
+
source ./venv/bin/activate
|
|
122
|
+
pip install -r requirements.txt
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Usage
|
|
126
|
+
```bash
|
|
127
|
+
# Basic usage
|
|
128
|
+
python3 ./lam/lam.py run <program> <input>
|
|
129
|
+
|
|
130
|
+
# With JavaScript
|
|
131
|
+
python3 ./lam/lam.py run script.js data.json --language js
|
|
132
|
+
|
|
133
|
+
# Full options
|
|
134
|
+
python3 ./lam/lam.py run <program> <input> \
|
|
135
|
+
--language [jq|js] \
|
|
136
|
+
--workspace_id <id> \
|
|
137
|
+
--flow_id <id> \
|
|
138
|
+
--execution_id <id> \
|
|
139
|
+
[--as-json]
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Resource Limits
|
|
143
|
+
- Maximum input size: 10MB
|
|
144
|
+
- Execution timeout: 5 seconds
|
|
145
|
+
- Memory limits enabled
|
|
146
|
+
- Disk space monitoring
|
|
147
|
+
|
|
148
|
+
## Security
|
|
149
|
+
- Sandboxed JavaScript execution
|
|
150
|
+
- Network access disabled
|
|
151
|
+
- Limited global scope
|
|
152
|
+
- Resource monitoring
|
|
153
|
+
- Secure dependency management
|
|
154
|
+
|
|
155
|
+
## Logging and Monitoring
|
|
156
|
+
- Execution statistics (duration, memory usage)
|
|
157
|
+
- Detailed error tracking
|
|
158
|
+
- PostHog analytics integration
|
|
159
|
+
- Log file generation
|
|
160
|
+
|
|
161
|
+
## Development
|
|
162
|
+
```bash
|
|
163
|
+
# Run all tests
|
|
164
|
+
make test
|
|
165
|
+
|
|
166
|
+
# Run specific test suite
|
|
167
|
+
make test-jq
|
|
168
|
+
make test-js
|
|
169
|
+
make test-js-edge-cases
|
|
170
|
+
|
|
171
|
+
# Run single test
|
|
172
|
+
make test-single TEST=test/js/example.js DATA=test/data/input.json
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Releases
|
|
176
|
+
Update version in `setup.py`:
|
|
177
|
+
```python
|
|
178
|
+
setup(
|
|
179
|
+
name="lam-cli",
|
|
180
|
+
version="0.0.<x>",
|
|
181
|
+
...
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Create and push tag:
|
|
186
|
+
```bash
|
|
187
|
+
git tag v<version>-<increment>
|
|
188
|
+
git push origin v<version>-<increment>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Dependencies
|
|
192
|
+
Update dependencies:
|
|
193
|
+
```bash
|
|
194
|
+
pip3 install <package>
|
|
195
|
+
pip3 freeze > requirements.txt
|
|
196
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
lam/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
lam/lam.py,sha256=22zXRw3pz0yQvsORvpWIdUjmvaFhP_6_DMkw02_CG1o,17939
|
|
3
|
+
lam_cli-0.1.0.dist-info/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
|
|
4
|
+
lam_cli-0.1.0.dist-info/METADATA,sha256=z2GeyYTNb3x8gM4Px8XUlldd8UpC86ZyUunGeO6SeQM,4674
|
|
5
|
+
lam_cli-0.1.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
6
|
+
lam_cli-0.1.0.dist-info/entry_points.txt,sha256=ph7QV6H2VWqf9fU5rtoAgEabDgZ4f85ZImdLXeBmdfA,36
|
|
7
|
+
lam_cli-0.1.0.dist-info/top_level.txt,sha256=WyM7-Ig60qQH9meqS293pEd83jrMtbvGJM8ALZOQCtA,4
|
|
8
|
+
lam_cli-0.1.0.dist-info/RECORD,,
|
lam_cli-0.0.7.dist-info/METADATA
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: lam-cli
|
|
3
|
-
Version: 0.0.7
|
|
4
|
-
Summary: Laminar data transformation tool
|
|
5
|
-
Home-page: https://github.com/laminar-run/lam
|
|
6
|
-
Author: Laminar Run, Inc.
|
|
7
|
-
Author-email: connect@laminar.run
|
|
8
|
-
License: GPLv3
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Requires-Dist: click
|
|
11
|
-
Requires-Dist: posthog
|
|
12
|
-
Requires-Dist: logtail-python
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Laminar is a platform that makes building and maintaining API integrations faster.
|
|
16
|
-
|
lam_cli-0.0.7.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
lam/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
lam/lam.py,sha256=tgtblEtVQrhgZmuDkFu6sdOMC41xtIQj05j8pryfZTY,7383
|
|
3
|
-
lam_cli-0.0.7.dist-info/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
|
|
4
|
-
lam_cli-0.0.7.dist-info/METADATA,sha256=2W6uPGH7oSD1Qg7MnouI_XM0xEQqVaKd0YY--_FSco0,404
|
|
5
|
-
lam_cli-0.0.7.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
6
|
-
lam_cli-0.0.7.dist-info/entry_points.txt,sha256=ph7QV6H2VWqf9fU5rtoAgEabDgZ4f85ZImdLXeBmdfA,36
|
|
7
|
-
lam_cli-0.0.7.dist-info/top_level.txt,sha256=WyM7-Ig60qQH9meqS293pEd83jrMtbvGJM8ALZOQCtA,4
|
|
8
|
-
lam_cli-0.0.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|