misata 0.1.0b0__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.
misata/cli.py ADDED
@@ -0,0 +1,680 @@
1
+ """
2
+ Command-line interface for Misata.
3
+
4
+ Provides easy-to-use commands for generating synthetic data from stories
5
+ or configuration files, now with LLM-powered schema generation.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import time
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ import click
15
+ from rich.console import Console
16
+ from rich.panel import Panel
17
+ from rich.progress import Progress, SpinnerColumn, TextColumn
18
+ from rich.table import Table as RichTable
19
+
20
+ from misata import DataSimulator, SchemaConfig
21
+ from misata.codegen import ScriptGenerator
22
+ from misata.story_parser import StoryParser
23
+
24
+ console = Console()
25
+
26
+
27
+ def print_banner():
28
+ """Print the Misata banner."""
29
+ console.print(Panel.fit(
30
+ "[bold purple]🧠 Misata[/bold purple] [dim]- AI-Powered Synthetic Data Engine[/dim]",
31
+ border_style="purple"
32
+ ))
33
+
34
+
35
+ @click.group()
36
+ @click.version_option(version="2.0.0")
37
+ def main() -> None:
38
+ """
39
+ Misata - AI-Powered Synthetic Data Engine
40
+
41
+ Generate industry-realistic data from natural language stories.
42
+ """
43
+ pass
44
+
45
+
46
+ @main.command()
47
+ @click.option(
48
+ "--story",
49
+ "-s",
50
+ type=str,
51
+ help="Natural language description of the data to generate",
52
+ )
53
+ @click.option(
54
+ "--config",
55
+ "-c",
56
+ type=click.Path(exists=True),
57
+ help="Path to YAML configuration file",
58
+ )
59
+ @click.option(
60
+ "--output-dir",
61
+ "-o",
62
+ type=click.Path(),
63
+ default="./generated_data",
64
+ help="Output directory for CSV files (default: ./generated_data)",
65
+ )
66
+ @click.option(
67
+ "--rows",
68
+ "-n",
69
+ type=int,
70
+ default=10000,
71
+ help="Default number of rows (if not specified in story/config)",
72
+ )
73
+ @click.option(
74
+ "--seed",
75
+ type=int,
76
+ default=None,
77
+ help="Random seed for reproducibility",
78
+ )
79
+ @click.option(
80
+ "--use-llm",
81
+ is_flag=True,
82
+ default=False,
83
+ help="Use LLM for intelligent schema generation",
84
+ )
85
+ @click.option(
86
+ "--provider",
87
+ "-p",
88
+ type=click.Choice(["groq", "openai", "ollama"]),
89
+ default=None,
90
+ help="LLM provider (groq, openai, ollama). Default: MISATA_PROVIDER env or groq",
91
+ )
92
+ @click.option(
93
+ "--model",
94
+ "-m",
95
+ type=str,
96
+ default=None,
97
+ help="LLM model name (e.g., llama3, gpt-4o-mini)",
98
+ )
99
+ @click.option(
100
+ "--export-script",
101
+ type=click.Path(),
102
+ default=None,
103
+ help="Export a standalone Python script instead of generating data",
104
+ )
105
+ def generate(
106
+ story: Optional[str],
107
+ config: Optional[str],
108
+ output_dir: str,
109
+ rows: int,
110
+ seed: Optional[int],
111
+ use_llm: bool,
112
+ provider: Optional[str],
113
+ model: Optional[str],
114
+ export_script: Optional[str],
115
+ ) -> None:
116
+ """
117
+ Generate synthetic data from a story or configuration file.
118
+
119
+ Examples:
120
+
121
+ # From natural language story (rule-based)
122
+ misata generate --story "A SaaS company with 50K users, 20% churn in Q3"
123
+
124
+ # From story with LLM (requires GROQ_API_KEY)
125
+ misata generate --story "SaaS company with churn" --use-llm
126
+
127
+ # From configuration file
128
+ misata generate --config config.yaml --output-dir ./data
129
+ """
130
+ print_banner()
131
+
132
+ # Validate inputs
133
+ if not story and not config:
134
+ console.print("[red]Error: Must provide either --story or --config[/red]")
135
+ sys.exit(1)
136
+
137
+ if story and config:
138
+ console.print("[yellow]Warning: Both story and config provided. Using config.[/yellow]")
139
+
140
+ if config:
141
+ console.print(f"šŸ“„ Loading configuration from: [cyan]{config}[/cyan]")
142
+ import yaml
143
+ with open(config, "r") as f:
144
+ config_dict = yaml.safe_load(f)
145
+ schema_config = SchemaConfig(**config_dict)
146
+ else:
147
+ console.print(f"šŸ“– Parsing story: [italic]{story}[/italic]\n")
148
+
149
+ if use_llm:
150
+ try:
151
+ from misata.llm_parser import LLMSchemaGenerator
152
+
153
+ # Determine provider for display
154
+ display_provider = provider or os.environ.get("MISATA_PROVIDER", "groq")
155
+ display_model = model or LLMSchemaGenerator.PROVIDERS.get(display_provider, {}).get("default_model", "")
156
+
157
+ console.print(f"🧠 [purple]Using {display_provider.title()} ({display_model}) for intelligent parsing...[/purple]")
158
+
159
+ with console.status("[purple]Generating schema with AI...[/purple]"):
160
+ llm = LLMSchemaGenerator(provider=provider, model=model)
161
+ schema_config = llm.generate_from_story(story, default_rows=rows)
162
+
163
+ console.print("āœ… [green]LLM schema generated successfully![/green]")
164
+ except ValueError as e:
165
+ error_msg = str(e)
166
+ if "API key required" in error_msg:
167
+ console.print(f"\n[red]āŒ {error_msg}[/red]")
168
+ console.print("\n Options:")
169
+ console.print(" • [yellow]export GROQ_API_KEY=xxx[/yellow] (free: https://console.groq.com)")
170
+ console.print(" • [yellow]export OPENAI_API_KEY=xxx[/yellow]")
171
+ console.print(" • [yellow]--provider ollama[/yellow] (local, no key needed)")
172
+ sys.exit(1)
173
+ raise
174
+ else:
175
+ # Rule-based parsing (original)
176
+ parser = StoryParser()
177
+ schema_config = parser.parse(story, default_rows=rows)
178
+
179
+ if parser.detected_domain:
180
+ console.print(f"āœ“ Detected domain: [green]{parser.detected_domain}[/green]")
181
+ if parser.scale_params:
182
+ console.print(f"āœ“ Detected scale: [green]{parser.scale_params}[/green]")
183
+ if parser.temporal_events:
184
+ console.print(f"āœ“ Detected events: [green]{len(parser.temporal_events)}[/green]")
185
+
186
+ # Set seed if provided
187
+ if seed is not None:
188
+ schema_config.seed = seed
189
+
190
+ # Display schema info
191
+ console.print(f"\nšŸ“‹ Schema: [bold]{schema_config.name}[/bold]")
192
+ console.print(f" Tables: {len(schema_config.tables)}")
193
+ console.print(f" Relationships: {len(schema_config.relationships)}")
194
+ console.print(f" Events: {len(schema_config.events)}")
195
+
196
+ # Export script or generate data
197
+ if export_script:
198
+ console.print("\nšŸ“ Generating standalone script...")
199
+ generator = ScriptGenerator(schema_config)
200
+ generator.generate(export_script)
201
+ console.print(f"[green]āœ“ Script saved to: {export_script}[/green]")
202
+ return
203
+
204
+ # Generate data
205
+ console.print("\nāš™ļø Initializing simulator...")
206
+ # Default batch size is 10k, good for CLI
207
+ simulator = DataSimulator(schema_config)
208
+
209
+ console.print(f"\nšŸ”§ Generating {len(schema_config.tables)} table(s)...\n")
210
+
211
+ start_time = time.time()
212
+ total_rows = 0
213
+
214
+ # Prepare export
215
+ import os
216
+ os.makedirs(output_dir, exist_ok=True)
217
+ files_created = set()
218
+
219
+ with Progress(
220
+ SpinnerColumn(),
221
+ TextColumn("[progress.description]{task.description}"),
222
+ TextColumn("{task.completed:,} rows"),
223
+ console=console,
224
+ ) as progress:
225
+ task = progress.add_task("Generating data...", total=None)
226
+
227
+ for table_name, batch_df in simulator.generate_all():
228
+ # Write to disk immediately
229
+ output_path = os.path.join(output_dir, f"{table_name}.csv")
230
+ mode = 'a' if table_name in files_created else 'w'
231
+ header = table_name not in files_created
232
+
233
+ batch_df.to_csv(output_path, mode=mode, header=header, index=False)
234
+ files_created.add(table_name)
235
+
236
+ batch_size = len(batch_df)
237
+ total_rows += batch_size
238
+ progress.update(task, advance=batch_size, description=f"Generating {table_name}...")
239
+
240
+ elapsed = time.time() - start_time
241
+
242
+ # Display summary
243
+ console.print("\n" + "="*70)
244
+ console.print(simulator.get_summary())
245
+ console.print("="*70)
246
+ console.print(f"\nā±ļø Generation time: [cyan]{elapsed:.2f} seconds[/cyan]")
247
+
248
+ # Calculate performance metrics
249
+ rows_per_sec = total_rows / elapsed if elapsed > 0 else 0
250
+ console.print(f"šŸš€ Performance: [green]{rows_per_sec:,.0f} rows/second[/green]")
251
+
252
+ console.print(f"\nšŸ’¾ Data saved to: [cyan]{output_dir}[/cyan]")
253
+ console.print("\n[bold green]āœ“ Done![/bold green]")
254
+
255
+
256
+ @main.command()
257
+ @click.argument("description")
258
+ @click.option(
259
+ "--output-dir",
260
+ "-o",
261
+ type=click.Path(),
262
+ default="./generated_data",
263
+ help="Output directory for CSV files",
264
+ )
265
+ def graph(description: str, output_dir: str) -> None:
266
+ """
267
+ REVERSE ENGINEERING: Generate data from a chart description.
268
+
269
+ Describe your desired chart pattern and get matching data.
270
+
271
+ Example:
272
+
273
+ misata graph "Monthly revenue from $100K to $1M over 2 years, with Q2 dips"
274
+ """
275
+ print_banner()
276
+
277
+ try:
278
+ from misata.llm_parser import LLMSchemaGenerator
279
+
280
+ console.print(f"šŸ“Š Graph description: [italic]{description}[/italic]")
281
+ console.print("\n🧠 [purple]Using LLM to reverse-engineer schema...[/purple]")
282
+
283
+ with console.status("[purple]Generating schema from chart description...[/purple]"):
284
+ llm = LLMSchemaGenerator()
285
+ schema_config = llm.generate_from_graph(description)
286
+
287
+ console.print("āœ… [green]Schema generated![/green]")
288
+ console.print(f"\nšŸ“‹ Schema: [bold]{schema_config.name}[/bold]")
289
+
290
+ # Generate data
291
+ simulator = DataSimulator(schema_config)
292
+ console.print("\nšŸ”§ Generating data...")
293
+
294
+ start_time = time.time()
295
+
296
+ import os
297
+ os.makedirs(output_dir, exist_ok=True)
298
+ files_created = set()
299
+ total_rows = 0
300
+
301
+ with Progress(
302
+ SpinnerColumn(),
303
+ TextColumn("[progress.description]{task.description}"),
304
+ console=console,
305
+ ) as progress:
306
+ task = progress.add_task("Generating...", total=None)
307
+ for table_name, batch_df in simulator.generate_all():
308
+ output_path = os.path.join(output_dir, f"{table_name}.csv")
309
+ mode = 'a' if table_name in files_created else 'w'
310
+ header = table_name not in files_created
311
+ batch_df.to_csv(output_path, mode=mode, header=header, index=False)
312
+ files_created.add(table_name)
313
+
314
+ total_rows += len(batch_df)
315
+ progress.update(task, advance=len(batch_df))
316
+
317
+ elapsed = time.time() - start_time
318
+
319
+ console.print(simulator.get_summary())
320
+ console.print(f"\nā±ļø Generation time: [cyan]{elapsed:.2f}s[/cyan]")
321
+ console.print(f"\n[bold green]āœ“ Data exported to {output_dir}[/bold green]")
322
+
323
+ except ValueError as e:
324
+ if "GROQ_API_KEY" in str(e):
325
+ console.print("\n[red]āŒ Groq API key required for graph mode.[/red]")
326
+ console.print(" Set your API key: [yellow]export GROQ_API_KEY=your_key[/yellow]")
327
+ console.print(" Get a free key: [cyan]https://console.groq.com[/cyan]")
328
+ sys.exit(1)
329
+ raise
330
+
331
+
332
+ @main.command()
333
+ @click.argument("story")
334
+ @click.option(
335
+ "--rows",
336
+ "-n",
337
+ type=int,
338
+ default=10000,
339
+ help="Default number of rows",
340
+ )
341
+ @click.option(
342
+ "--output",
343
+ "-o",
344
+ type=click.Path(),
345
+ default="config.yaml",
346
+ help="Output YAML configuration file",
347
+ )
348
+ @click.option(
349
+ "--use-llm",
350
+ is_flag=True,
351
+ default=False,
352
+ help="Use LLM for intelligent parsing",
353
+ )
354
+ def parse(story: str, rows: int, output: str, use_llm: bool) -> None:
355
+ """
356
+ Parse a story and output the generated configuration.
357
+
358
+ Useful for reviewing and editing the configuration before generation.
359
+
360
+ Example:
361
+
362
+ misata parse "SaaS company with 50K users" --output saas_config.yaml
363
+ """
364
+ print_banner()
365
+ console.print(f"Story: [italic]{story}[/italic]\n")
366
+
367
+ if use_llm:
368
+ try:
369
+ from misata.llm_parser import LLMSchemaGenerator
370
+
371
+ console.print("🧠 [purple]Using LLM for parsing...[/purple]")
372
+ with console.status("[purple]Generating with AI...[/purple]"):
373
+ llm = LLMSchemaGenerator()
374
+ schema_config = llm.generate_from_story(story, default_rows=rows)
375
+ except ValueError as e:
376
+ if "GROQ_API_KEY" in str(e):
377
+ console.print("\n[red]āŒ Groq API key required.[/red]")
378
+ console.print(" Falling back to rule-based parsing...")
379
+ parser = StoryParser()
380
+ schema_config = parser.parse(story, default_rows=rows)
381
+ else:
382
+ raise
383
+ else:
384
+ parser = StoryParser()
385
+ schema_config = parser.parse(story, default_rows=rows)
386
+
387
+ # Display summary
388
+ console.print("[bold]Generated Configuration:[/bold]")
389
+ console.print(f" Name: {schema_config.name}")
390
+ console.print(f" Tables: {len(schema_config.tables)}")
391
+ console.print(f" Relationships: {len(schema_config.relationships)}")
392
+ console.print(f" Events: {len(schema_config.events)}")
393
+
394
+ # Export to YAML
395
+ generator = ScriptGenerator(schema_config)
396
+ generator.generate_yaml_config(output)
397
+
398
+ console.print(f"\n[green]āœ“ Configuration saved to: {output}[/green]")
399
+ console.print(" Review and edit as needed, then run:")
400
+ console.print(f" [cyan]misata generate --config {output}[/cyan]")
401
+
402
+
403
+ @main.command()
404
+ @click.option("--port", "-p", type=int, default=8000, help="Port to run the API server")
405
+ @click.option("--host", "-h", type=str, default="0.0.0.0", help="Host to bind to")
406
+ def serve(port: int, host: str) -> None:
407
+ """
408
+ Start the Misata API server for the web UI.
409
+
410
+ Example:
411
+
412
+ misata serve --port 8000
413
+ """
414
+ print_banner()
415
+ console.print("\n🌐 Starting Misata API server...")
416
+ console.print(f" Host: [cyan]{host}[/cyan]")
417
+ console.print(f" Port: [cyan]{port}[/cyan]")
418
+ console.print(f"\nšŸ“ API Docs: [cyan]http://localhost:{port}/docs[/cyan]")
419
+ console.print("šŸŽØ Web UI: [cyan]http://localhost:3000[/cyan] (run 'npm run dev' in /web)")
420
+ console.print("\nPress [bold]Ctrl+C[/bold] to stop.\n")
421
+
422
+ import uvicorn
423
+ from misata.api import app
424
+ uvicorn.run(app, host=host, port=port)
425
+
426
+
427
+ @main.command()
428
+ def examples() -> None:
429
+ """
430
+ Show example stories and usage patterns.
431
+ """
432
+ print_banner()
433
+
434
+ examples_table = RichTable(show_header=True, header_style="bold purple")
435
+ examples_table.add_column("Scenario", style="cyan", width=30)
436
+ examples_table.add_column("Command", style="green", width=50)
437
+
438
+ examples_table.add_row(
439
+ "SaaS with churn (rule-based)",
440
+ 'misata generate -s "SaaS with 50K users, 20% churn in Q3"',
441
+ )
442
+ examples_table.add_row(
443
+ "SaaS with LLM",
444
+ 'misata generate -s "SaaS with churn patterns" --use-llm',
445
+ )
446
+ examples_table.add_row(
447
+ "E-commerce (LLM)",
448
+ 'misata generate -s "E-commerce with 100K orders" --use-llm',
449
+ )
450
+ examples_table.add_row(
451
+ "Pharma services",
452
+ 'misata generate -s "Pharma with 500 projects, 50K timesheets"',
453
+ )
454
+ examples_table.add_row(
455
+ "Graph reverse engineering",
456
+ 'misata graph "Revenue from $100K to $1M over 2 years"',
457
+ )
458
+ examples_table.add_row(
459
+ "Quick template",
460
+ 'misata template saas --users 10000',
461
+ )
462
+ examples_table.add_row(
463
+ "Start web UI",
464
+ 'misata serve --port 8000',
465
+ )
466
+
467
+ console.print(examples_table)
468
+
469
+ console.print("\n[bold]Story Syntax Tips:[/bold]")
470
+ console.print(" • Mention numbers: '50K users', '1M transactions'")
471
+ console.print(" • Specify domain: 'SaaS', 'e-commerce', 'pharma'")
472
+ console.print(" • Add events: 'growth', 'churn', 'crash in Q3'")
473
+ console.print(" • Be specific: '20% churn in Q3 2023'")
474
+
475
+ console.print("\n[bold]LLM Mode (--use-llm):[/bold]")
476
+ console.print(" Requires GROQ_API_KEY environment variable")
477
+ console.print(" Get free key: [cyan]https://console.groq.com[/cyan]")
478
+
479
+ console.print("\n[bold]Industry Templates:[/bold]")
480
+ console.print(" Available: saas, ecommerce, fitness, healthcare")
481
+ console.print(" Example: [cyan]misata template <name> [OPTIONS][/cyan]")
482
+
483
+
484
+ @main.command()
485
+ @click.argument("template_name")
486
+ @click.option(
487
+ "--output-dir",
488
+ "-o",
489
+ type=click.Path(),
490
+ default="./generated_data",
491
+ help="Output directory for CSV files",
492
+ )
493
+ @click.option(
494
+ "--scale",
495
+ "-s",
496
+ type=float,
497
+ default=1.0,
498
+ help="Row count multiplier (e.g., 0.1 for 10%, 2.0 for 2x)",
499
+ )
500
+ @click.option(
501
+ "--validate/--no-validate",
502
+ default=True,
503
+ help="Run post-generation validation",
504
+ )
505
+ def template(template_name: str, output_dir: str, scale: float, validate: bool) -> None:
506
+ """
507
+ Generate data from an industry template.
508
+
509
+ Available templates: saas, ecommerce, fitness, healthcare
510
+
511
+ Examples:
512
+
513
+ misata template saas
514
+ misata template ecommerce --scale 0.5
515
+ misata template fitness --output-dir ./fitness_data
516
+ """
517
+ print_banner()
518
+
519
+ try:
520
+ from misata.templates import template_to_schema, list_templates
521
+
522
+ available = list_templates()
523
+ if template_name not in available:
524
+ console.print(f"[red]Error: Unknown template '{template_name}'[/red]")
525
+ console.print(f"Available templates: [cyan]{', '.join(available)}[/cyan]")
526
+ return
527
+
528
+ console.print(f"šŸ“‹ Loading template: [bold purple]{template_name}[/bold purple]")
529
+ if scale != 1.0:
530
+ console.print(f" Scale: {scale}x")
531
+
532
+ schema_config = template_to_schema(template_name, row_multiplier=scale)
533
+
534
+ console.print(f"\nšŸ“Š Schema: [bold]{schema_config.name}[/bold]")
535
+ console.print(f" Tables: {len(schema_config.tables)}")
536
+ console.print(f" Relationships: {len(schema_config.relationships)}")
537
+
538
+ # Count reference vs transactional tables
539
+ ref_tables = sum(1 for t in schema_config.tables if t.is_reference)
540
+ trans_tables = len(schema_config.tables) - ref_tables
541
+ console.print(f" Reference tables: {ref_tables}")
542
+ console.print(f" Transactional tables: {trans_tables}")
543
+
544
+ # Generate data
545
+ console.print("\nāš™ļø Generating data...")
546
+ simulator = DataSimulator(schema_config)
547
+
548
+ start_time = time.time()
549
+
550
+ import os
551
+ os.makedirs(output_dir, exist_ok=True)
552
+ files_created = set()
553
+ total_rows = 0
554
+
555
+ with Progress(
556
+ SpinnerColumn(),
557
+ TextColumn("[progress.description]{task.description}"),
558
+ TextColumn("{task.completed:,} rows"),
559
+ console=console,
560
+ ) as progress:
561
+ task = progress.add_task("Generating...", total=None)
562
+
563
+ for table_name, batch_df in simulator.generate_all():
564
+ output_path = os.path.join(output_dir, f"{table_name}.csv")
565
+ mode = 'a' if table_name in files_created else 'w'
566
+ header = table_name not in files_created
567
+
568
+ batch_df.to_csv(output_path, mode=mode, header=header, index=False)
569
+ files_created.add(table_name)
570
+
571
+ total_rows += len(batch_df)
572
+ progress.update(task, advance=len(batch_df), description=f"Generating {table_name}...")
573
+
574
+ elapsed = time.time() - start_time
575
+
576
+ console.print(simulator.get_summary())
577
+ console.print(f"\nā±ļø Generation time: [cyan]{elapsed:.2f}s[/cyan]")
578
+ console.print(f"\n[bold green]āœ“ Data exported to {output_dir}[/bold green]")
579
+
580
+ # Run validation if enabled
581
+ if validate:
582
+ console.print("\nšŸ” Running validation on exported files...")
583
+ try:
584
+ # Validate by reading back files (or sample)
585
+ # For now, let's read the full files, assuming they fit in memory for small template demos
586
+ # In production, validation should support streaming or sampling
587
+ import pandas as pd
588
+ from pathlib import Path
589
+ from misata.validation import validate_data
590
+
591
+ tables = {}
592
+ data_path = Path(output_dir)
593
+ for csv_file in data_path.glob("*.csv"):
594
+ # Basic check if it's one of ours
595
+ if csv_file.stem in [t.name for t in schema_config.tables]:
596
+ # Warning: reading potentially large files
597
+ # TODO: implement scalable validation
598
+ tables[csv_file.stem] = pd.read_csv(csv_file)
599
+
600
+ report = validate_data(tables, schema_config)
601
+
602
+ if report.is_clean:
603
+ console.print("[green]āœ… All validations passed![/green]")
604
+ else:
605
+ console.print(report.summary())
606
+ except Exception as e:
607
+ console.print(f"[yellow]āš ļø Validation failed (memory issue or other): {e}[/yellow]")
608
+
609
+ except Exception as e:
610
+ console.print(f"[red]Error: {e}[/red]")
611
+ raise
612
+
613
+
614
+ @main.command()
615
+ @click.option(
616
+ "--data-dir",
617
+ "-d",
618
+ type=click.Path(exists=True),
619
+ required=True,
620
+ help="Directory containing CSV files to validate",
621
+ )
622
+ def validate_cmd(data_dir: str) -> None:
623
+ """
624
+ Validate existing CSV data files.
625
+
626
+ Example:
627
+
628
+ misata validate --data-dir ./generated_data
629
+ """
630
+ print_banner()
631
+
632
+ import pandas as pd
633
+ from misata.validation import validate_data
634
+
635
+ console.print(f"šŸ” Validating data in: [cyan]{data_dir}[/cyan]\\n")
636
+
637
+ # Load all CSVs
638
+ tables = {}
639
+ data_path = Path(data_dir)
640
+ for csv_file in data_path.glob("*.csv"):
641
+ table_name = csv_file.stem
642
+ tables[table_name] = pd.read_csv(csv_file)
643
+ console.print(f" Loaded {table_name}: {len(tables[table_name]):,} rows")
644
+
645
+ if not tables:
646
+ console.print("[yellow]No CSV files found in directory.[/yellow]")
647
+ return
648
+
649
+ console.print()
650
+ report = validate_data(tables)
651
+ console.print(report.summary())
652
+
653
+
654
+ @main.command()
655
+ def templates_list() -> None:
656
+ """
657
+ List available industry templates.
658
+ """
659
+ print_banner()
660
+
661
+ from misata.templates import TEMPLATES
662
+
663
+ console.print("[bold]Available Industry Templates:[/bold]\\n")
664
+
665
+ template_table = RichTable(show_header=True, header_style="bold purple")
666
+ template_table.add_column("Template", style="cyan")
667
+ template_table.add_column("Description")
668
+ template_table.add_column("Tables", justify="right")
669
+
670
+ for name, template in TEMPLATES.items():
671
+ table_count = len(template["tables"])
672
+ template_table.add_row(name, template["description"], str(table_count))
673
+
674
+ console.print(template_table)
675
+ console.print("\\nUsage: [cyan]misata template <name> [OPTIONS][/cyan]")
676
+
677
+
678
+ if __name__ == "__main__":
679
+ main()
680
+