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/__init__.py +48 -0
- misata/api.py +460 -0
- misata/audit.py +415 -0
- misata/benchmark.py +376 -0
- misata/cli.py +680 -0
- misata/codegen.py +153 -0
- misata/curve_fitting.py +106 -0
- misata/customization.py +256 -0
- misata/feedback.py +433 -0
- misata/formulas.py +362 -0
- misata/generators.py +247 -0
- misata/hybrid.py +398 -0
- misata/llm_parser.py +493 -0
- misata/noise.py +346 -0
- misata/schema.py +252 -0
- misata/semantic.py +185 -0
- misata/simulator.py +742 -0
- misata/story_parser.py +425 -0
- misata/templates/__init__.py +444 -0
- misata/validation.py +313 -0
- misata-0.1.0b0.dist-info/METADATA +291 -0
- misata-0.1.0b0.dist-info/RECORD +25 -0
- misata-0.1.0b0.dist-info/WHEEL +5 -0
- misata-0.1.0b0.dist-info/entry_points.txt +2 -0
- misata-0.1.0b0.dist-info/top_level.txt +1 -0
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
|
+
|