lam-cli 0.0.6__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 CHANGED
@@ -6,152 +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
- posthog = Posthog(project_api_key='phc_wfeHFG0p5yZIdBpjVYy00o5x1HbEpggdMzIuFYgNPSK', host='https://app.posthog.com')
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, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
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
- jq_path = 'jq'
24
-
25
- def truncate_long_strings(data, max_length=1000, truncation_msg="... (truncated)"):
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
- try:
47
- distinct_id = generate_distinct_id(workspace_id, flow_id)
48
- posthog.capture(distinct_id=distinct_id, event=event_name, properties=properties)
49
- except Exception as e:
50
- logger.error(f"Error logging event: {e}")
51
-
52
- def parse_program_file(program_file):
53
- logger.info(f"Parsing program file: {program_file}")
54
- with open(program_file, 'r') as file:
55
- return ''.join(line for line in file if not line.strip().startswith('#'))
56
-
57
- def run_jq(jq_script, input_data):
58
- logger.info(f"Running jq script {jq_script} with input data {truncate_long_strings(input_data)}")
59
- process = subprocess.Popen([jq_path, '-c', jq_script], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
60
- output, error = process.communicate(input=input_data)
61
- if error:
62
- logger.error(f"Error running jq: {error}")
63
- return output, error
64
-
65
- def process_input(input, workspace_id, flow_id):
66
- logger.info(f"Processing input: {truncate_long_strings(input)}")
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")
433
+ @click.option('--execution_id', default="local", help="Execution ID")
107
434
  @click.option('--as-json', is_flag=True, default=True, help="Output as JSON")
108
- def run(program_file, input, workspace_id, flow_id, as_json):
109
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
110
- log_file = f"lam_run_{workspace_id}_{flow_id}_{timestamp}.log"
111
- result_file = f"lam_result_{workspace_id}_{flow_id}_{timestamp}.json"
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
112
446
 
447
+ # Setup logging
448
+ log_file = engine.get_log_file()
449
+ result_file = engine.get_result_file()
450
+
113
451
  file_handler = logging.FileHandler(log_file, 'w')
114
452
  file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
115
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
+ })
116
460
 
117
- logger.info(f"Logging to {log_file}")
118
- logger.info(f"Running command with program file: {program_file}, input: {truncate_long_strings(input)}, workspace_id: {workspace_id}, flow_id: {flow_id}, as_json: {as_json}")
119
- if not shutil.which("jq"):
120
- logger.error("Unable to find jq, killing process")
121
- click.echo({"lam.error": "jq is not installed"}, err=True)
122
- track_event('lam.run.error', {'error': 'jq is not installed', 'workspace_id': workspace_id, 'flow_id': flow_id}, workspace_id, flow_id)
123
- write_to_result_file({"lam.error": "jq is not installed"}, result_file)
124
- return
125
-
126
- input_data, error = process_input(input, workspace_id, flow_id)
127
- if error:
128
- click.echo({"lam.error": f"Invalid input: {error}"}, err=True)
129
- track_event('lam.run.error', {'error': f"Invalid input: {error}", 'workspace_id': workspace_id, 'flow_id': flow_id}, workspace_id, flow_id)
130
- write_to_result_file({"lam.error": f"Invalid input: {error}"}, result_file)
131
- return
132
-
133
- jq_script = parse_program_file(program_file)
134
- 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)
135
- output, jq_error = run_jq(jq_script, input_data)
136
-
137
- if jq_error:
138
- click.echo({"lam.error": jq_error}, err=True)
139
- track_event('lam.run.run_jq_error', {'error': jq_error, 'workspace_id': workspace_id, 'flow_id': flow_id}, workspace_id, flow_id)
140
- write_to_result_file({"lam.error": jq_error}, result_file)
141
- return
461
+ try:
462
+ # Process input
463
+ input_data, error = process_input(input)
464
+ if error:
465
+ raise ProcessingError(f"Invalid input: {error}")
142
466
 
143
- result, error = handle_jq_output(output, as_json, workspace_id, flow_id)
144
- if error:
145
- click.echo({"lam.error": error}, err=True)
146
- track_event('lam.run.handle_jq_output_error', {'error': error, 'workspace_id': workspace_id, 'flow_id': flow_id}, workspace_id, flow_id)
147
- write_to_result_file({"lam.error": error}, result_file)
148
- else:
149
- click.echo(json.dumps(result, indent=4) if as_json else result)
150
- track_event('lam.run.success', {'workspace_id': workspace_id, 'flow_id': flow_id}, workspace_id, flow_id)
151
- write_to_result_file(result, result_file)
152
-
153
- logger.info("Run complete, waiting for event logger to finish")
154
- logger.removeHandler(file_handler)
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)
155
503
 
156
504
  if __name__ == '__main__':
157
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (75.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,16 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: lam-cli
3
- Version: 0.0.6
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
-
@@ -1,8 +0,0 @@
1
- lam/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- lam/lam.py,sha256=a2O622WWikM3OZq_KhTQm-3kl4JHygkYCK_NwQCeTP8,7269
3
- lam_cli-0.0.6.dist-info/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
4
- lam_cli-0.0.6.dist-info/METADATA,sha256=vl5pMwIXxzOzmq1AfrYu0eBpvcUu7eDdivn81ofKrlo,404
5
- lam_cli-0.0.6.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
6
- lam_cli-0.0.6.dist-info/entry_points.txt,sha256=ph7QV6H2VWqf9fU5rtoAgEabDgZ4f85ZImdLXeBmdfA,36
7
- lam_cli-0.0.6.dist-info/top_level.txt,sha256=WyM7-Ig60qQH9meqS293pEd83jrMtbvGJM8ALZOQCtA,4
8
- lam_cli-0.0.6.dist-info/RECORD,,