Coverage for src/dataknobs_fsm/cli/main.py: 0%
470 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-20 16:46 -0600
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-20 16:46 -0600
1"""FSM CLI tool for managing and executing FSM configurations.
3This module provides a command-line interface for:
4- Creating and validating FSM configurations
5- Running FSM executions with data
6- Managing FSM history and checkpoints
7- Debugging and profiling FSM operations
8"""
10import click
11import json
12from dataknobs_fsm.utils.json_encoder import dumps as json_dumps
13import yaml
14import sys
15from pathlib import Path
16from rich.console import Console
17from rich.table import Table
18from rich.syntax import Syntax
19from rich.tree import Tree
20from rich.progress import Progress, SpinnerColumn, TextColumn
21import asyncio
23from ..api.simple import SimpleFSM
24from ..api.advanced import AdvancedFSM
25from ..config.loader import ConfigLoader
26from ..execution.history import ExecutionHistory
27from ..storage.file import FileStorage
28from ..patterns.etl import create_etl_pipeline, ETLMode
29from ..patterns.file_processing import create_csv_processor, create_json_stream_processor
31console = Console()
34@click.group()
35@click.version_option(version='0.1.0')
36def cli():
37 """FSM CLI - Finite State Machine Management Tool"""
38 pass
41@cli.group()
42def config():
43 """FSM configuration management commands"""
44 pass
47@config.command()
48@click.argument('template', type=click.Choice(['basic', 'etl', 'workflow', 'processing']))
49@click.option('--output', '-o', default='fsm_config.yaml', help='Output file path')
50@click.option('--format', '-f', type=click.Choice(['yaml', 'json']), default='yaml')
51def create(template: str, output: str, format: str):
52 """Create a new FSM configuration from template"""
53 templates = {
54 'basic': {
55 'name': 'Basic_FSM',
56 'data_mode': 'copy',
57 'states': [
58 {'name': 'start', 'is_start': True},
59 {'name': 'process'},
60 {'name': 'end', 'is_end': True}
61 ],
62 'arcs': [
63 {'from': 'start', 'to': 'process', 'name': 'begin'},
64 {'from': 'process', 'to': 'end', 'name': 'complete'}
65 ]
66 },
67 'etl': {
68 'name': 'ETL_Pipeline',
69 'data_mode': 'copy',
70 'resources': [
71 {'name': 'source_db', 'type': 'database', 'provider': 'sqlite', 'path': 'source.db'},
72 {'name': 'target_db', 'type': 'database', 'provider': 'sqlite', 'path': 'target.db'}
73 ],
74 'states': [
75 {'name': 'extract', 'is_start': True, 'resources': ['source_db']},
76 {'name': 'transform'},
77 {'name': 'load', 'resources': ['target_db']},
78 {'name': 'complete', 'is_end': True}
79 ],
80 'arcs': [
81 {'from': 'extract', 'to': 'transform', 'name': 'extracted'},
82 {'from': 'transform', 'to': 'load', 'name': 'transformed'},
83 {'from': 'load', 'to': 'complete', 'name': 'loaded'}
84 ]
85 },
86 'workflow': {
87 'name': 'Workflow_FSM',
88 'data_mode': 'reference',
89 'states': [
90 {'name': 'receive', 'is_start': True},
91 {'name': 'validate'},
92 {'name': 'approve'},
93 {'name': 'reject'},
94 {'name': 'complete', 'is_end': True}
95 ],
96 'arcs': [
97 {'from': 'receive', 'to': 'validate', 'name': 'received'},
98 {'from': 'validate', 'to': 'approve', 'name': 'valid'},
99 {'from': 'validate', 'to': 'reject', 'name': 'invalid'},
100 {'from': 'approve', 'to': 'complete', 'name': 'approved'},
101 {'from': 'reject', 'to': 'complete', 'name': 'rejected'}
102 ]
103 },
104 'processing': {
105 'name': 'File_Processor',
106 'data_mode': 'direct',
107 'states': [
108 {'name': 'read', 'is_start': True},
109 {'name': 'parse'},
110 {'name': 'process'},
111 {'name': 'write'},
112 {'name': 'done', 'is_end': True}
113 ],
114 'arcs': [
115 {'from': 'read', 'to': 'parse', 'name': 'file_read'},
116 {'from': 'parse', 'to': 'process', 'name': 'parsed'},
117 {'from': 'process', 'to': 'write', 'name': 'processed'},
118 {'from': 'write', 'to': 'done', 'name': 'written'}
119 ]
120 }
121 }
123 config_data = templates[template]
125 output_path = Path(output)
126 if format == 'yaml':
127 with open(output_path, 'w') as f:
128 yaml.dump(config_data, f, default_flow_style=False)
129 else:
130 with open(output_path, 'w') as f:
131 json.dump(config_data, f, indent=2)
133 console.print(f"[green][/green] Created {template} configuration in {output}")
134 console.print("\nConfiguration overview:")
135 console.print(f" Name: {config_data['name']}")
136 console.print(f" States: {len(config_data['states'])}")
137 console.print(f" Arcs: {len(config_data['arcs'])}")
138 console.print(f" Data Mode: {config_data['data_mode']}")
141@config.command()
142@click.argument('config_file', type=click.Path(exists=True))
143@click.option('--verbose', '-v', is_flag=True, help='Show detailed validation output')
144def validate(config_file: str, verbose: bool):
145 """Validate an FSM configuration file"""
146 loader = ConfigLoader()
148 try:
149 with Progress(
150 SpinnerColumn(),
151 TextColumn("[progress.description]{task.description}"),
152 console=console
153 ) as progress:
154 task = progress.add_task("Loading configuration...", total=None)
156 config = loader.load_from_file(config_file)
157 progress.update(task, description="Validating configuration...")
159 # Configuration is already validated by loading it
160 is_valid = True
161 errors = []
163 progress.stop()
165 if is_valid:
166 console.print("[green][/green] Configuration is valid!")
168 if verbose:
169 console.print("\n[bold]Configuration Details:[/bold]")
170 console.print(f" Name: {config.name}")
171 console.print(f" Data Mode: {config.data_mode}")
173 # Count states and arcs across all networks
174 total_states = sum(len(net.states) for net in config.networks)
175 total_arcs = 0
176 for net in config.networks:
177 for state in net.states:
178 if hasattr(state, 'arcs') and state.arcs:
179 total_arcs += len(state.arcs)
181 console.print(f" States: {total_states}")
182 console.print(f" Arcs: {total_arcs}")
184 if config.resources:
185 console.print(f" Resources: {len(config.resources)}")
186 else:
187 console.print("[red][/red] Configuration validation failed!")
188 console.print("\n[bold red]Errors:[/bold red]")
189 for error in errors:
190 console.print(f" {error}")
191 sys.exit(1)
193 except Exception as e:
194 console.print(f"[red]Error loading configuration: {e}[/red]")
195 sys.exit(1)
198@config.command()
199@click.argument('config_file', type=click.Path(exists=True))
200@click.option('--format', '-f', type=click.Choice(['tree', 'graph', 'table']), default='tree')
201def show(config_file: str, format: str):
202 """Display FSM configuration structure"""
203 loader = ConfigLoader()
205 try:
206 config = loader.load_from_file(config_file)
208 if format == 'tree':
209 tree = Tree(f"[bold]{config.name}[/bold]")
211 # Show networks
212 for network in config.networks:
213 network_branch = tree.add(f"Network: {network.name}")
215 states_branch = network_branch.add("States")
216 for state in network.states:
217 state_label = state.name
218 if state.is_start:
219 state_label += " [green](start)[/green]"
220 if state.is_end:
221 state_label += " [red](end)[/red]"
222 states_branch.add(state_label)
224 arcs_branch = network_branch.add("Arcs")
225 for state in network.states:
226 if hasattr(state, 'arcs') and state.arcs:
227 for arc in state.arcs:
228 arc_label = f"{state.name} → {arc.target}"
229 if hasattr(arc, 'name') and arc.name:
230 arc_label += f" [{arc.name}]"
231 arcs_branch.add(arc_label)
233 if config.resources:
234 resources_branch = tree.add("Resources")
235 for resource in config.resources:
236 resources_branch.add(f"{resource.name}: {resource.type}")
238 console.print(tree)
240 elif format == 'table':
241 # States table
242 states_table = Table(title=f"{config.name} - States")
243 states_table.add_column("Network", style="blue")
244 states_table.add_column("Name", style="cyan")
245 states_table.add_column("Type", style="green")
247 for network in config.networks:
248 for state in network.states:
249 state_type = []
250 if state.is_start:
251 state_type.append("Start")
252 if state.is_end:
253 state_type.append("End")
254 if not state_type:
255 state_type.append("Normal")
257 states_table.add_row(
258 network.name,
259 state.name,
260 ' '.join(state_type)
261 )
263 console.print(states_table)
265 # Arcs table
266 arcs_table = Table(title=f"{config.name} - Arcs")
267 arcs_table.add_column("Network", style="blue")
268 arcs_table.add_column("From", style="cyan")
269 arcs_table.add_column("To", style="cyan")
270 arcs_table.add_column("Name", style="yellow")
272 for network in config.networks:
273 for state in network.states:
274 if hasattr(state, 'arcs') and state.arcs:
275 for arc in state.arcs:
276 arc_name = arc.name if hasattr(arc, 'name') and arc.name else '-'
277 arcs_table.add_row(
278 network.name,
279 state.name,
280 arc.target,
281 arc_name
282 )
284 console.print(arcs_table)
286 elif format == 'graph':
287 console.print("[yellow]Graph visualization (Mermaid format)[/yellow]")
288 console.print("\n```mermaid")
289 console.print("graph TD")
291 # Process all networks
292 for network in config.networks:
293 if len(config.networks) > 1:
294 console.print(f" subgraph {network.name}")
296 for state in network.states:
297 shape = "([{}])" if state.is_start else \
298 "(({}))" if state.is_end else \
299 "[{}]"
300 console.print(f" {state.name}{shape.format(state.name)}")
302 # Collect arcs from states
303 for state in network.states:
304 if hasattr(state, 'arcs') and state.arcs:
305 for arc in state.arcs:
306 arc_label = arc.name if hasattr(arc, 'name') and arc.name else "transition"
307 console.print(f" {state.name} -->|{arc_label}| {arc.target}")
309 if len(config.networks) > 1:
310 console.print(" end")
312 console.print("```")
314 except Exception as e:
315 console.print(f"[red]Error loading configuration: {e}[/red]")
316 sys.exit(1)
319@cli.group()
320def run():
321 """Execute FSM operations"""
322 pass
325@run.command()
326@click.argument('config_file', type=click.Path(exists=True))
327@click.option('--data', '-d', help='Input data (JSON string or file path)')
328@click.option('--initial-state', '-s', help='Initial state name')
329@click.option('--timeout', '-t', type=float, help='Execution timeout in seconds')
330@click.option('--output', '-o', help='Output file for results')
331@click.option('--verbose', '-v', is_flag=True, help='Show execution details')
332def execute(config_file: str, data: str | None, initial_state: str | None,
333 timeout: float | None, output: str | None, verbose: bool):
334 """Execute FSM with data"""
335 # Parse input data
336 input_data = {}
337 if data:
338 if Path(data).exists():
339 with open(data) as f:
340 input_data = json.load(f)
341 else:
342 try:
343 input_data = json.loads(data)
344 except json.JSONDecodeError:
345 console.print("[red]Invalid JSON data[/red]")
346 sys.exit(1)
348 # Create and run FSM
349 fsm = SimpleFSM(config_file)
351 with Progress(
352 SpinnerColumn(),
353 TextColumn("[progress.description]{task.description}"),
354 console=console
355 ) as progress:
356 progress.add_task("Executing FSM...", total=None)
358 try:
359 result = fsm.process(
360 data=input_data,
361 initial_state=initial_state,
362 timeout=timeout
363 )
364 progress.stop()
366 if result.get('success', False):
367 console.print("[green][/green] Execution completed successfully!")
368 console.print(f" Final state: {result.get('final_state', 'unknown')}")
369 path = result.get('path', [])
370 console.print(f" Transitions: {len(path) - 1 if path else 0}")
372 if verbose:
373 console.print("\n[bold]Execution Path:[/bold]")
374 for i, state in enumerate(result.get('path', [])):
375 console.print(f" {i+1}. {state}")
377 if 'data' in result:
378 console.print("\n[bold]Final Data:[/bold]")
379 console.print(Syntax(
380 json_dumps(result['data'], indent=2),
381 "json",
382 theme="monokai"
383 ))
385 if output:
386 with open(output, 'w') as f:
387 json.dump(result, f, indent=2)
388 console.print(f"\n[green]Results saved to {output}[/green]")
390 else:
391 console.print("[red][/red] Execution failed!")
392 console.print(f" Error: {result.get('error', 'Unknown error')}")
393 sys.exit(1)
395 except Exception as e:
396 progress.stop()
397 console.print(f"[red]Execution error: {e}[/red]")
398 sys.exit(1)
401@run.command()
402@click.argument('config_file', type=click.Path(exists=True))
403@click.argument('data_file', type=click.Path(exists=True))
404@click.option('--batch-size', '-b', type=int, default=10, help='Batch size')
405@click.option('--workers', '-w', type=int, default=4, help='Number of parallel workers')
406@click.option('--output', '-o', help='Output file for results')
407@click.option('--progress', '-p', is_flag=True, help='Show progress bar')
408def batch(config_file: str, data_file: str, batch_size: int, workers: int,
409 output: str | None, progress: bool):
410 """Execute FSM on batch data"""
411 # Load batch data
412 with open(data_file) as f:
413 if data_file.endswith('.jsonl'):
414 batch_data = [json.loads(line) for line in f]
415 else:
416 batch_data = json.load(f)
417 if not isinstance(batch_data, list):
418 console.print("[red]Data file must contain a JSON array or JSONL[/red]")
419 sys.exit(1)
421 # Create FSM
422 fsm = SimpleFSM(config_file)
424 console.print(f"Processing {len(batch_data)} items...")
425 console.print(f" Batch size: {batch_size}")
426 console.print(f" Workers: {workers}")
428 try:
429 if progress:
430 with Progress(console=console) as prog:
431 task = prog.add_task("Processing...", total=len(batch_data))
433 results = []
434 for i in range(0, len(batch_data), batch_size):
435 batch = batch_data[i:i+batch_size]
436 batch_results = fsm.process_batch(
437 data=batch,
438 batch_size=batch_size,
439 max_workers=workers
440 )
441 results.extend(batch_results)
442 prog.update(task, advance=len(batch))
443 else:
444 results = fsm.process_batch(
445 data=batch_data,
446 batch_size=batch_size,
447 max_workers=workers
448 )
450 # Calculate statistics
451 successful = sum(1 for r in results if r['success'])
452 failed = len(results) - successful
454 console.print("\n[bold]Results:[/bold]")
455 console.print(f" Total: {len(results)}")
456 console.print(f" [green]Successful: {successful}[/green]")
457 if failed > 0:
458 console.print(f" [red]Failed: {failed}[/red]")
460 if output:
461 with open(output, 'w') as f:
462 json.dump(results, f, indent=2)
463 console.print(f"\n[green]Results saved to {output}[/green]")
465 except Exception as e:
466 console.print(f"[red]Batch processing error: {e}[/red]")
467 sys.exit(1)
470@run.command()
471@click.argument('config_file', type=click.Path(exists=True))
472@click.argument('source')
473@click.option('--sink', '-s', help='Output sink (file path or URL)')
474@click.option('--chunk-size', '-c', type=int, default=100, help='Stream chunk size')
475@click.option('--format', '-f', type=click.Choice(['json', 'csv']), default='json')
476def stream(config_file: str, source: str, sink: str | None,
477 chunk_size: int, format: str):
478 """Process streaming data through FSM"""
479 # Create FSM
480 fsm = SimpleFSM(config_file)
482 console.print("Starting stream processing...")
483 console.print(f" Source: {source}")
484 if sink:
485 console.print(f" Sink: {sink}")
486 console.print(f" Chunk size: {chunk_size}")
488 async def run_stream():
489 with Progress(
490 SpinnerColumn(),
491 TextColumn("[progress.description]{task.description}"),
492 console=console
493 ) as progress:
494 progress.add_task("Processing stream...", total=None)
496 try:
497 result = await fsm.process_stream(
498 source=source,
499 sink=sink,
500 chunk_size=chunk_size
501 )
502 progress.stop()
504 console.print("\n[green][/green] Stream processing completed!")
505 console.print(f" Records processed: {result.get('total_processed', 0)}")
506 console.print(f" Chunks: {result.get('chunks_processed', 0)}")
507 if 'errors' in result and result['errors'] > 0:
508 console.print(f" [yellow]Errors: {result['errors']}[/yellow]")
510 except Exception as e:
511 progress.stop()
512 console.print(f"[red]Stream processing error: {e}[/red]")
513 sys.exit(1)
515 asyncio.run(run_stream())
518@cli.group()
519def debug():
520 """Debug and profile FSM operations"""
521 pass
524@debug.command() # type: ignore
525@click.argument('config_file', type=click.Path(exists=True))
526@click.option('--data', '-d', help='Input data (JSON string or file path)')
527@click.option('--breakpoint', '-b', multiple=True, help='Set breakpoint at state')
528@click.option('--trace', '-t', is_flag=True, help='Enable execution tracing')
529@click.option('--profile', '-p', is_flag=True, help='Enable performance profiling')
530def run(config_file: str, data: str | None, breakpoint: tuple,
531 trace: bool, profile: bool):
532 """Debug FSM execution with breakpoints and tracing"""
533 # Load configuration
534 loader = ConfigLoader()
535 config = loader.load_from_file(config_file)
537 # Parse input data
538 input_data = {}
539 if data:
540 if Path(data).exists():
541 with open(data) as f:
542 input_data = json.load(f)
543 else:
544 try:
545 input_data = json.loads(data)
546 except json.JSONDecodeError:
547 console.print("[red]Invalid JSON data[/red]")
548 sys.exit(1)
550 # Create advanced FSM
551 fsm = AdvancedFSM(config)
553 # Set breakpoints
554 for bp in breakpoint:
555 fsm.set_breakpoint(bp)
556 console.print(f"[yellow]Breakpoint set at state: {bp}[/yellow]")
558 async def run_debug():
559 try:
560 if trace:
561 console.print("[cyan]Tracing enabled[/cyan]\n")
562 trace_log = await fsm.trace_execution(input_data)
564 # Display trace
565 table = Table(title="Execution Trace")
566 table.add_column("Time", style="cyan")
567 table.add_column("State", style="green")
568 table.add_column("Arc", style="yellow")
569 table.add_column("Event")
571 for entry in trace_log:
572 table.add_row(
573 entry['timestamp'],
574 entry.get('state', '-'),
575 entry.get('arc', '-'),
576 entry['event']
577 )
579 console.print(table)
581 elif profile:
582 console.print("[cyan]Profiling enabled[/cyan]\n")
583 profile_data = await fsm.profile_execution(input_data)
585 # Display profile
586 console.print("[bold]Performance Profile:[/bold]")
587 console.print(f" Total time: {profile_data['total_time']:.3f}s")
588 console.print(f" Transitions: {profile_data['transition_count']}")
590 if 'state_times' in profile_data:
591 console.print("\n[bold]State Execution Times:[/bold]")
592 for state, time in profile_data['state_times'].items():
593 console.print(f" {state}: {time:.3f}s")
595 if 'arc_times' in profile_data:
596 console.print("\n[bold]Arc Transition Times:[/bold]")
597 for arc, time in profile_data['arc_times'].items():
598 console.print(f" {arc}: {time:.3f}s")
600 else:
601 # Interactive debugging
602 from ..api.advanced import FSMDebugger
604 debugger = FSMDebugger(fsm, config)
605 await debugger.start_session(input_data)
607 except Exception as e:
608 console.print(f"[red]Debug error: {e}[/red]")
609 sys.exit(1)
611 asyncio.run(run_debug())
614@cli.group()
615def history():
616 """Manage FSM execution history"""
617 pass
620@history.command(name='list') # Explicitly set command name
621@click.option('--fsm-name', '-n', help='Filter by FSM name')
622@click.option('--limit', '-l', type=int, default=10, help='Number of entries to show')
623@click.option('--format', '-f', type=click.Choice(['table', 'json']), default='table')
624def list_history(fsm_name: str | None, limit: int, format: str):
625 """List execution history"""
626 # Create history manager with file backend
627 storage = FileStorage(Path.home() / '.fsm' / 'history')
628 manager = ExecutionHistory(storage) # type: ignore
630 # Query history
631 entries = asyncio.run(manager.query_history(
632 fsm_name=fsm_name,
633 limit=limit
634 ))
636 if not entries:
637 console.print("[yellow]No history entries found[/yellow]")
638 return
640 if format == 'table':
641 table = Table(title="Execution History")
642 table.add_column("Execution ID", style="cyan")
643 table.add_column("FSM Name", style="green")
644 table.add_column("Start Time")
645 table.add_column("End Time")
646 table.add_column("Status", style="yellow")
647 table.add_column("States")
649 for entry in entries:
650 status = "Success" if entry.get('success') else "Failed"
651 states = len(entry.get('states', []))
653 table.add_row(
654 entry['execution_id'][:8],
655 entry['fsm_name'],
656 entry['start_time'],
657 entry.get('end_time', '-'),
658 status,
659 str(states)
660 )
662 console.print(table)
663 else:
664 console.print(json_dumps(entries, indent=2))
667@history.command()
668@click.argument('execution_id')
669@click.option('--verbose', '-v', is_flag=True, help='Show detailed information')
670def show_execution(execution_id: str, verbose: bool):
671 """Show details of a specific execution"""
672 # Create history manager
673 storage = FileStorage(Path.home() / '.fsm' / 'history')
674 manager = ExecutionHistory(storage) # type: ignore
676 # Get execution details
677 entry = asyncio.run(manager.get_execution(execution_id))
679 if not entry:
680 console.print(f"[red]Execution {execution_id} not found[/red]")
681 sys.exit(1)
683 console.print("[bold]Execution Details:[/bold]")
684 console.print(f" ID: {entry['execution_id']}")
685 console.print(f" FSM: {entry['fsm_name']}")
686 console.print(f" Start: {entry['start_time']}")
687 console.print(f" End: {entry.get('end_time', 'In progress')}")
688 console.print(f" Status: {'Success' if entry.get('success') else 'Failed'}")
690 if verbose:
691 console.print("\n[bold]Execution Path:[/bold]")
692 for i, state in enumerate(entry.get('states', [])):
693 console.print(f" {i+1}. {state['name']} @ {state['timestamp']}")
695 if 'arcs' in entry:
696 console.print("\n[bold]Transitions:[/bold]")
697 for arc in entry['arcs']:
698 console.print(f" {arc['from']} � {arc['to']} [{arc['name']}]")
700 if 'error' in entry:
701 console.print("\n[bold red]Error:[/bold red]")
702 console.print(f" {entry['error']}")
705@cli.group()
706def pattern():
707 """Run pre-configured FSM patterns"""
708 pass
711@pattern.command()
712@click.option('--source', '-s', required=True, help='Source database connection')
713@click.option('--target', '-t', required=True, help='Target database connection')
714@click.option('--mode', '-m', type=click.Choice(['full', 'incremental', 'upsert']),
715 default='full')
716@click.option('--batch-size', '-b', type=int, default=1000)
717@click.option('--checkpoint', '-c', help='Resume from checkpoint ID')
718def etl(source: str, target: str, mode: str, batch_size: int, checkpoint: str | None):
719 """Run ETL pipeline pattern"""
720 console.print("[bold]Starting ETL Pipeline[/bold]")
721 console.print(f" Source: {source}")
722 console.print(f" Target: {target}")
723 console.print(f" Mode: {mode}")
724 console.print(f" Batch size: {batch_size}")
726 if checkpoint:
727 console.print(f" Resuming from checkpoint: {checkpoint}")
729 # Create ETL pipeline
730 etl_mode = ETLMode[mode.upper()]
731 pipeline = create_etl_pipeline(
732 source=source,
733 target=target,
734 mode=etl_mode,
735 batch_size=batch_size
736 )
738 async def run_etl():
739 with Progress(
740 SpinnerColumn(),
741 TextColumn("[progress.description]{task.description}"),
742 console=console
743 ) as progress:
744 progress.add_task("Running ETL...", total=None)
746 try:
747 metrics = await pipeline.run(checkpoint_id=checkpoint)
748 progress.stop()
750 console.print("\n[green][/green] ETL completed successfully!")
751 console.print(f" Records extracted: {metrics['extracted']}")
752 console.print(f" Records loaded: {metrics['loaded']}")
753 if metrics['errors'] > 0:
754 console.print(f" [yellow]Errors: {metrics['errors']}[/yellow]")
756 except Exception as e:
757 progress.stop()
758 console.print(f"[red]ETL error: {e}[/red]")
759 sys.exit(1)
761 asyncio.run(run_etl())
764@pattern.command()
765@click.argument('input_file', type=click.Path(exists=True))
766@click.option('--output', '-o', help='Output file path')
767@click.option('--format', '-f', type=click.Choice(['csv', 'json']), default='csv')
768@click.option('--transform', '-t', multiple=True, help='Transformation functions')
769@click.option('--filter', '-F', multiple=True, help='Filter expressions')
770def process_file(input_file: str, output: str | None, format: str,
771 transform: tuple, filter: tuple):
772 """Process file using FSM pattern"""
773 console.print(f"[bold]Processing {format.upper()} file[/bold]")
774 console.print(f" Input: {input_file}")
775 if output:
776 console.print(f" Output: {output}")
778 # Create processor based on format
779 if format == 'csv':
780 processor = create_csv_processor(
781 input_file=input_file,
782 output_file=output
783 )
784 else:
785 processor = create_json_stream_processor(
786 input_file=input_file,
787 output_file=output
788 )
790 async def run_processing():
791 with Progress(
792 SpinnerColumn(),
793 TextColumn("[progress.description]{task.description}"),
794 console=console
795 ) as progress:
796 progress.add_task("Processing file...", total=None)
798 try:
799 metrics = await processor.process()
800 progress.stop()
802 console.print("\n[green][/green] File processing completed!")
803 console.print(f" Lines read: {metrics.get('lines_read', 0)}")
804 console.print(f" Records processed: {metrics['records_processed']}")
805 console.print(f" Records written: {metrics.get('records_written', 0)}")
806 if metrics.get('errors', 0) > 0:
807 console.print(f" [yellow]Errors: {metrics['errors']}[/yellow]")
809 except Exception as e:
810 progress.stop()
811 console.print(f"[red]Processing error: {e}[/red]")
812 sys.exit(1)
814 asyncio.run(run_processing())
817def main():
818 """Main entry point for CLI"""
819 cli()
822if __name__ == '__main__':
823 main()