algomancy-quickstart 0.7.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.
Files changed (42) hide show
  1. algomancy_quickstart/__init__.py +2 -0
  2. algomancy_quickstart/asset_manager.py +202 -0
  3. algomancy_quickstart/data_inference.py +517 -0
  4. algomancy_quickstart/main.py +62 -0
  5. algomancy_quickstart/quickstart.py +683 -0
  6. algomancy_quickstart/styling_wizard.py +347 -0
  7. algomancy_quickstart/templates/__init__.py +0 -0
  8. algomancy_quickstart/templates/algorithm.py.jinja +104 -0
  9. algomancy_quickstart/templates/assets/CQM-logo-white.png +0 -0
  10. algomancy_quickstart/templates/assets/cqm-button-white.png +0 -0
  11. algomancy_quickstart/templates/assets/cqm-button.png +0 -0
  12. algomancy_quickstart/templates/assets/cqm-logo.png +0 -0
  13. algomancy_quickstart/templates/assets/css/button_colors.css +285 -0
  14. algomancy_quickstart/templates/assets/css/cqm_loader.css +47 -0
  15. algomancy_quickstart/templates/assets/css/sidebar_layout.css +189 -0
  16. algomancy_quickstart/templates/assets/css/theme_colors.css +90 -0
  17. algomancy_quickstart/templates/assets/letter-c.svg +4 -0
  18. algomancy_quickstart/templates/assets/letter-m.svg +4 -0
  19. algomancy_quickstart/templates/assets/letter-q.svg +4 -0
  20. algomancy_quickstart/templates/assets/letters/letter-c.png +0 -0
  21. algomancy_quickstart/templates/assets/letters/letter-m.png +0 -0
  22. algomancy_quickstart/templates/assets/letters/letter-q.png +0 -0
  23. algomancy_quickstart/templates/assets/pepsi_girl.jpeg +0 -0
  24. algomancy_quickstart/templates/assets/style.css +421 -0
  25. algomancy_quickstart/templates/compare_page.py.jinja +133 -0
  26. algomancy_quickstart/templates/data_page.py.jinja +94 -0
  27. algomancy_quickstart/templates/etl_factory.py.jinja +108 -0
  28. algomancy_quickstart/templates/etl_factory_generated.py.jinja +82 -0
  29. algomancy_quickstart/templates/generated_schemas.py.jinja +55 -0
  30. algomancy_quickstart/templates/home_page.py.jinja +65 -0
  31. algomancy_quickstart/templates/kpi.py.jinja +76 -0
  32. algomancy_quickstart/templates/main.py.jinja +42 -0
  33. algomancy_quickstart/templates/main_custom.py.jinja +55 -0
  34. algomancy_quickstart/templates/main_generated_etl.py.jinja +72 -0
  35. algomancy_quickstart/templates/main_with_styling.py.jinja +83 -0
  36. algomancy_quickstart/templates/overview_page.py.jinja +98 -0
  37. algomancy_quickstart/templates/scenario_page.py.jinja +77 -0
  38. algomancy_quickstart/templates/schema.py.jinja +58 -0
  39. algomancy_quickstart/templates/styling_config.py.jinja +53 -0
  40. algomancy_quickstart-0.7.0.dist-info/METADATA +29 -0
  41. algomancy_quickstart-0.7.0.dist-info/RECORD +42 -0
  42. algomancy_quickstart-0.7.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,683 @@
1
+ import click
2
+ from pathlib import Path
3
+ from jinja2 import Environment, PackageLoader, select_autoescape
4
+ from algomancy_data import FileExtension
5
+
6
+ from .data_inference import SchemaInferenceEngine, DataFileInfo
7
+ from .asset_manager import AssetManager
8
+ from .styling_wizard import StylingWizard
9
+
10
+
11
+ class QuickstartWizard:
12
+ """Main wizard for setting up an Algomancy application."""
13
+
14
+ def __init__(self, skip_confirmation: bool = False, title: str | None = None):
15
+ self.skip_confirmation = skip_confirmation
16
+ self.title = title
17
+ self.current_dir = Path.cwd()
18
+
19
+ # Will be set in step 2
20
+ self.project_name = None
21
+ self.class_name = None
22
+ self.filename = None
23
+
24
+ # Will be set in step 3
25
+ self.detected_files: list[DataFileInfo] = []
26
+ self.inference_engine = SchemaInferenceEngine(sample_rows=100)
27
+
28
+ # Asset manager for step 4
29
+ self.asset_manager = AssetManager(self.current_dir)
30
+
31
+ # Styling wizard for step 5
32
+ self.styling_wizard = StylingWizard()
33
+
34
+ # Track what was generated
35
+ self.has_custom_implementations = False
36
+ self.has_generated_etl = False
37
+ self.host = "127.0.0.1"
38
+ self.port = 8050
39
+
40
+ # Set up Jinja2 environment
41
+ self.jinja_env = Environment(
42
+ loader=PackageLoader("algomancy_quickstart", "templates"),
43
+ autoescape=select_autoescape(["html", "xml"]),
44
+ trim_blocks=True,
45
+ lstrip_blocks=True,
46
+ )
47
+
48
+ def run(self):
49
+ """Execute the full quickstart wizard."""
50
+ click.echo("Starting Algomancy application setup...")
51
+ click.echo()
52
+
53
+ # Step 1: Create folder structure and basic main method
54
+ self.step_1_create_structure()
55
+
56
+ click.echo()
57
+
58
+ # Step 2: Generate custom implementation shells (optional)
59
+ if click.confirm(
60
+ "Do you want to generate custom implementation templates?", default=True
61
+ ):
62
+ self.step_2_generate_implementations()
63
+ self.has_custom_implementations = True
64
+
65
+ click.echo()
66
+
67
+ # Step 3: Scan data folder and generate ETL pipeline (optional)
68
+ if click.confirm(
69
+ "Do you want to scan your data folder and generate an ETL pipeline?",
70
+ default=True,
71
+ ):
72
+ self.step_3_generate_etl_from_data()
73
+ if self.detected_files: # Only set if files were actually processed
74
+ self.has_generated_etl = True
75
+
76
+ click.echo()
77
+
78
+ # Step 4: Install assets (optional)
79
+ if click.confirm(
80
+ "Do you want to install default assets (CSS, images)?", default=True
81
+ ):
82
+ self.step_4_install_assets()
83
+
84
+ click.echo()
85
+
86
+ # Step 5: Configure styling (optional)
87
+ if click.confirm(
88
+ "Do you want to configure custom styling (colors, themes)?", default=True
89
+ ):
90
+ self.step_5_configure_styling()
91
+
92
+ click.echo()
93
+ click.echo(click.style(" Setup complete!", fg="green", bold=True))
94
+ click.echo(
95
+ f"Your Algomancy application has been created in: {self.current_dir}"
96
+ )
97
+ click.echo()
98
+ click.echo("Next steps:")
99
+ click.echo(" 1. Review and customize the generated files")
100
+ click.echo(" 2. Run: python main.py")
101
+ click.echo(f" 3. Open your browser at http://{self.host}:{self.port}")
102
+
103
+ def step_1_create_structure(self):
104
+ """Step 1: Create folder structure and generate basic main.py"""
105
+ click.echo(
106
+ click.style(" Step 1: Creating folder structure", fg="blue", bold=True)
107
+ )
108
+ click.echo()
109
+
110
+ # Get project title
111
+ if not self.title:
112
+ self.title = click.prompt(
113
+ "What is your project title?",
114
+ default="My Algomancy Dashboard",
115
+ type=str,
116
+ )
117
+
118
+ # Get host and port
119
+ self.host = click.prompt("Host address", default="127.0.0.1", type=str)
120
+ self.port = click.prompt("Port number", default=8050, type=int)
121
+
122
+ # Define folder structure - include data/setup and src/styling
123
+ folders = [
124
+ "assets",
125
+ "data",
126
+ "data/setup",
127
+ "src",
128
+ "src/data_handling",
129
+ "src/pages",
130
+ "src/templates",
131
+ "src/templates/kpi",
132
+ "src/templates/algorithm",
133
+ ]
134
+
135
+ # Check if any folders already exist
136
+ existing_folders = [f for f in folders if (self.current_dir / f).exists()]
137
+
138
+ if existing_folders and not self.skip_confirmation:
139
+ click.echo(
140
+ click.style(
141
+ " Warning: The following folders already exist:", fg="yellow"
142
+ )
143
+ )
144
+ for folder in existing_folders:
145
+ click.echo(f" - {folder}")
146
+ click.echo()
147
+
148
+ if not click.confirm(
149
+ "Do you want to continue? (existing files will not be overwritten)"
150
+ ):
151
+ click.echo("Setup cancelled.")
152
+ raise SystemExit(0)
153
+
154
+ # Create folders
155
+ click.echo("Creating folder structure...")
156
+ for folder in folders:
157
+ folder_path = self.current_dir / folder
158
+ folder_path.mkdir(parents=True, exist_ok=True)
159
+
160
+ # Create __init__.py for Python packages
161
+ if folder.startswith("src"):
162
+ init_file = folder_path / "__init__.py"
163
+ if not init_file.exists():
164
+ init_file.touch()
165
+
166
+ click.echo(f" ✓ {folder}/")
167
+
168
+ # Check if main.py already exists
169
+ main_py_path = self.current_dir / "main.py"
170
+ if main_py_path.exists():
171
+ click.echo()
172
+ click.echo(click.style(" Warning: main.py already exists!", fg="yellow"))
173
+
174
+ if not self.skip_confirmation:
175
+ if not click.confirm("Do you want to overwrite it?"):
176
+ click.echo("Skipping main.py generation.")
177
+ return
178
+
179
+ # Generate main.py from template
180
+ click.echo()
181
+ click.echo("Generating main.py...")
182
+ self._generate_main_py(self.title, self.host, self.port)
183
+ click.echo(" ✓ main.py created")
184
+
185
+ click.echo()
186
+ click.echo(click.style(" Step 1 complete!", fg="green"))
187
+
188
+ def step_2_generate_implementations(self):
189
+ """Step 2: Generate custom implementation shells."""
190
+ click.echo(
191
+ click.style(
192
+ " Step 2: Generating custom implementation templates",
193
+ fg="blue",
194
+ bold=True,
195
+ )
196
+ )
197
+ click.echo()
198
+
199
+ # Get project name for class naming
200
+ self.project_name = click.prompt(
201
+ "What is your project/domain name? (e.g., Sales, Inventory, Logistics)",
202
+ type=str,
203
+ )
204
+
205
+ # Generate class name (PascalCase)
206
+ self.class_name = self._to_pascal_case(self.project_name)
207
+
208
+ # Generate filename (snake_case)
209
+ self.filename = self._to_snake_case(self.project_name)
210
+
211
+ click.echo()
212
+ click.echo(f"Using class name: {self.class_name}")
213
+ click.echo(f"Using filename: {self.filename}")
214
+ click.echo()
215
+
216
+ # Generate each component
217
+ components = [
218
+ ("schema", "src/data_handling", "schemas.py"),
219
+ ("algorithm", "src/templates/algorithm", f"{self.filename}_algorithm.py"),
220
+ ("kpi", "src/templates/kpi", f"{self.filename}_kpi.py"),
221
+ ("etl_factory", "src/data_handling", "etl_factory.py"),
222
+ ("home_page", "src/pages", "home_page.py"),
223
+ ("data_page", "src/pages", "data_page.py"),
224
+ ("scenario_page", "src/pages", "scenario_page.py"),
225
+ ("compare_page", "src/pages", "compare_page.py"),
226
+ ("overview_page", "src/pages", "overview_page.py"),
227
+ ]
228
+
229
+ click.echo("Generating implementation templates...")
230
+
231
+ for template_name, target_dir, target_file in components:
232
+ self._generate_implementation_file(template_name, target_dir, target_file)
233
+ click.echo(f" ✓ {target_dir}/{target_file}")
234
+
235
+ # Update main.py to use custom implementations
236
+ click.echo()
237
+ click.echo("Updating main.py to use custom implementations...")
238
+ self._update_main_py_with_custom_implementations()
239
+ click.echo(" ✓ main.py updated")
240
+
241
+ click.echo()
242
+ click.echo(click.style(" Step 2 complete!", fg="green"))
243
+ click.echo()
244
+ click.echo(
245
+ click.style(
246
+ " Next: Customize the TODO items in the generated files.", fg="cyan"
247
+ )
248
+ )
249
+
250
+ def step_3_generate_etl_from_data(self):
251
+ """Step 3: Scan data folder and generate ETL pipeline."""
252
+ click.echo(
253
+ click.style(
254
+ " Step 3: Scanning data folder and generating ETL pipeline",
255
+ fg="blue",
256
+ bold=True,
257
+ )
258
+ )
259
+ click.echo()
260
+
261
+ data_setup_dir = self.current_dir / "data" / "setup"
262
+
263
+ # Check if data/setup exists
264
+ if not data_setup_dir.exists():
265
+ click.echo(
266
+ click.style(" Directory data/setup/ does not exist!", fg="yellow")
267
+ )
268
+ return
269
+
270
+ # Scan for files with retry logic
271
+ while True:
272
+ detected_files = self.inference_engine.scan_directory(data_setup_dir)
273
+
274
+ if not detected_files:
275
+ click.echo(
276
+ click.style(" No data files found in data/setup/", fg="yellow")
277
+ )
278
+ click.echo()
279
+ click.echo("Supported file types: CSV, XLSX, JSON")
280
+ click.echo()
281
+
282
+ choice = click.prompt(
283
+ "What would you like to do?",
284
+ type=click.Choice(["retry", "skip"], case_sensitive=False),
285
+ default="retry",
286
+ )
287
+
288
+ if choice == "skip":
289
+ click.echo("Skipping ETL generation.")
290
+ return
291
+ else:
292
+ click.echo()
293
+ click.echo(
294
+ "Please add your data files to data/setup/ and press Enter to retry..."
295
+ )
296
+ input()
297
+ continue
298
+ else:
299
+ break
300
+
301
+ # Display detected files
302
+ click.echo(f"Found {len(detected_files)} data file(s):")
303
+ for file_info in detected_files:
304
+ sheets_info = (
305
+ f" ({len(file_info.sheet_names)} sheets)"
306
+ if file_info.sheet_names
307
+ else ""
308
+ )
309
+ click.echo(
310
+ f" • {file_info.file_name}{file_info.file_path.suffix} - {file_info.extension.value}{sheets_info}"
311
+ )
312
+
313
+ # Interactive schema inference for each file
314
+ click.echo()
315
+ click.echo(click.style("Let's configure each file...", fg="cyan"))
316
+
317
+ for file_info in detected_files:
318
+ success = self.inference_engine.infer_schema_interactive(file_info)
319
+
320
+ if success and not file_info.skip_file:
321
+ # Add metadata for template rendering
322
+ file_info.class_name = self._to_pascal_case(file_info.file_name)
323
+ file_info.snake_name = self._to_snake_case(file_info.file_name)
324
+ file_info.total_columns = sum(
325
+ len(cols) for cols in file_info.inferred_schemas.values()
326
+ )
327
+
328
+ # Filter out skipped files
329
+ self.detected_files = [f for f in detected_files if not f.skip_file]
330
+
331
+ if not self.detected_files:
332
+ click.echo()
333
+ click.echo(
334
+ click.style(" No files selected for ETL pipeline.", fg="yellow")
335
+ )
336
+ return
337
+
338
+ click.echo()
339
+ click.echo(click.style("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", fg="cyan"))
340
+ click.echo()
341
+
342
+ # Display summary of inferred schemas
343
+ self._display_inferred_schemas_summary()
344
+
345
+ # Ask for final confirmation
346
+ if not self.skip_confirmation:
347
+ click.echo()
348
+ if not click.confirm(
349
+ "Generate ETL pipeline with these configurations?", default=True
350
+ ):
351
+ click.echo("Skipping ETL generation.")
352
+ return
353
+
354
+ click.echo()
355
+ click.echo("Generating schema and ETL files...")
356
+
357
+ # Generate schemas file
358
+ self._generate_schemas_file()
359
+ click.echo(" ✓ src/data_handling/generated_schemas.py")
360
+
361
+ # Generate or update ETL factory
362
+ self._generate_etl_factory_file()
363
+ click.echo(" ✓ src/data_handling/etl_factory.py")
364
+
365
+ # Update main.py to use generated schemas
366
+ click.echo()
367
+ click.echo("Updating main.py to use generated schemas...")
368
+ self._update_main_py_with_generated_etl()
369
+ click.echo(" ✓ main.py updated")
370
+
371
+ click.echo()
372
+ click.echo(click.style(" Step 3 complete!", fg="green"))
373
+ click.echo()
374
+ click.echo(
375
+ click.style(
376
+ " Generated files can be customized in src/data_handling/", fg="cyan"
377
+ )
378
+ )
379
+
380
+ def step_4_install_assets(self):
381
+ """Step 4: Install default assets from GitHub or bundled fallback."""
382
+ try:
383
+ success = self.asset_manager.install_assets(
384
+ skip_confirmation=self.skip_confirmation
385
+ )
386
+
387
+ if success:
388
+ click.echo()
389
+ click.echo(click.style(" Step 4 complete!", fg="green"))
390
+ click.echo()
391
+ click.echo(
392
+ click.style(
393
+ " Assets installed. You can customize them in the assets/ folder.",
394
+ fg="cyan",
395
+ )
396
+ )
397
+ else:
398
+ click.echo()
399
+ click.echo(
400
+ click.style(
401
+ " Step 4 incomplete - no assets installed", fg="yellow"
402
+ )
403
+ )
404
+
405
+ except Exception as e:
406
+ click.echo()
407
+ click.echo(click.style(f" Error in Step 4: {e}", fg="red"))
408
+
409
+ def step_5_configure_styling(self):
410
+ """Step 5: Configure custom styling."""
411
+ try:
412
+ # Run the styling wizard
413
+ styling_config = self.styling_wizard.run()
414
+
415
+ click.echo()
416
+ click.echo("Generating styling configuration...")
417
+
418
+ # Generate styling_config.py
419
+ self._generate_styling_config(styling_config)
420
+ click.echo(" ✓ src/styling_config.py")
421
+
422
+ # Update main.py to use styling
423
+ click.echo()
424
+ click.echo("Updating main.py to use custom styling...")
425
+ self._update_main_py_with_styling()
426
+ click.echo(" ✓ main.py updated")
427
+
428
+ click.echo()
429
+ click.echo(click.style(" Step 5 complete!", fg="green"))
430
+ click.echo()
431
+ click.echo(
432
+ click.style(
433
+ " You can customize styling further in src/styling_config.py",
434
+ fg="cyan",
435
+ )
436
+ )
437
+
438
+ except Exception as e:
439
+ click.echo()
440
+ click.echo(click.style(f" Error in Step 5: {e}", fg="red"))
441
+
442
+ def _generate_styling_config(self, config: dict):
443
+ """Generate styling_config.py file."""
444
+ template = self.jinja_env.get_template("styling_config.py.jinja")
445
+
446
+ content = template.render(
447
+ project_name=self.project_name or "Project",
448
+ background=config["background"],
449
+ primary=config["primary"],
450
+ secondary=config["secondary"],
451
+ text=config["text"],
452
+ text_highlight=config["text_highlight"],
453
+ text_selected=config["text_selected"],
454
+ button_mode=config["button_mode"].name,
455
+ card_mode=config["card_mode"].name,
456
+ logo_path=config.get("logo_path"),
457
+ button_path=config.get("button_path"),
458
+ )
459
+
460
+ config_path = self.current_dir / "src" / "styling_config.py"
461
+ config_path.write_text(content, encoding="utf-8")
462
+
463
+ def _update_main_py_with_styling(self):
464
+ """Update main.py to import and use styling configuration."""
465
+ template = self.jinja_env.get_template("main_with_styling.py.jinja")
466
+
467
+ content = template.render(
468
+ title=self.title,
469
+ host=self.host,
470
+ port=self.port,
471
+ class_name=self.class_name or "Custom",
472
+ filename=self.filename or "custom",
473
+ has_custom_implementations=self.has_custom_implementations,
474
+ has_generated_etl=self.has_generated_etl,
475
+ )
476
+
477
+ main_py_path = self.current_dir / "main.py"
478
+ main_py_path.write_text(content, encoding="utf-8")
479
+
480
+ def _display_inferred_schemas_summary(self):
481
+ """Display a summary of all inferred schemas."""
482
+ click.echo(click.style("Summary of detected schemas:", fg="cyan", bold=True))
483
+ click.echo()
484
+
485
+ for file_info in self.detected_files:
486
+ click.echo(
487
+ click.style(
488
+ f"📄 {file_info.file_name}{file_info.file_path.suffix}",
489
+ fg="cyan",
490
+ bold=True,
491
+ )
492
+ )
493
+
494
+ # Show configuration
495
+ if file_info.extension == FileExtension.CSV:
496
+ click.echo(f" Config: CSV separator = '{file_info.csv_separator}'")
497
+ elif file_info.extension == FileExtension.XLSX:
498
+ click.echo(
499
+ f" Config: Extracting {len(file_info.sheets_to_extract)} sheet(s)"
500
+ )
501
+
502
+ # Show schemas
503
+ for schema_name, columns in file_info.inferred_schemas.items():
504
+ if file_info.is_multi_sheet:
505
+ click.echo(f" Sheet: {schema_name}")
506
+
507
+ # Show first few columns
508
+ col_items = list(columns.items())
509
+ show_count = min(10, len(col_items))
510
+
511
+ for col_name, data_type in col_items[:show_count]:
512
+ type_color = self._get_type_color(data_type)
513
+ click.echo(
514
+ f" • {col_name}: {click.style(data_type.value, fg=type_color)}"
515
+ )
516
+
517
+ if len(col_items) > show_count:
518
+ click.echo(
519
+ f" ... and {len(col_items) - show_count} more column(s)"
520
+ )
521
+
522
+ click.echo()
523
+
524
+ def _get_type_color(self, data_type) -> str:
525
+ """Get color for a data type."""
526
+ from algomancy_data import DataType
527
+
528
+ color_map = {
529
+ DataType.INTEGER: "blue",
530
+ DataType.FLOAT: "blue",
531
+ DataType.STRING: "green",
532
+ DataType.BOOLEAN: "magenta",
533
+ DataType.DATETIME: "yellow",
534
+ }
535
+ return color_map.get(data_type, "white")
536
+
537
+ def _generate_schemas_file(self):
538
+ """Generate the schemas file from detected data."""
539
+ template = self.jinja_env.get_template("generated_schemas.py.jinja")
540
+
541
+ content = template.render(
542
+ project_name=self.project_name or "Project",
543
+ files=self.detected_files,
544
+ )
545
+
546
+ schemas_path = (
547
+ self.current_dir / "src" / "data_handling" / "generated_schemas.py"
548
+ )
549
+ schemas_path.write_text(content, encoding="utf-8")
550
+
551
+ def _generate_etl_factory_file(self):
552
+ """Generate the ETL factory file with extractors.
553
+
554
+ Files are partitioned into registry-default (handled by
555
+ ``super().create_extraction_sequence``) vs custom (hand-wired with
556
+ an explicit extractor) based on whether they need non-default
557
+ constructor arguments — CSV with a non-comma separator, or a
558
+ single-sheet XLSX where the sheet must be specified by name.
559
+ """
560
+ template = self.jinja_env.get_template("etl_factory_generated.py.jinja")
561
+
562
+ default_files: list = []
563
+ custom_files: list = []
564
+ needs_csv_extractor = False
565
+ needs_xlsx_single_extractor = False
566
+
567
+ for file_info in self.detected_files:
568
+ ext = file_info.extension.name
569
+ if file_info.is_multi_sheet or ext == "JSON":
570
+ default_files.append(file_info)
571
+ elif ext == "CSV":
572
+ if (file_info.csv_separator or ",") == ",":
573
+ default_files.append(file_info)
574
+ else:
575
+ custom_files.append(file_info)
576
+ needs_csv_extractor = True
577
+ elif ext == "XLSX":
578
+ # Single-sheet XLSX: pin the sheet by name explicitly so we
579
+ # don't depend on the registry default's sheet selection.
580
+ custom_files.append(file_info)
581
+ needs_xlsx_single_extractor = True
582
+ else:
583
+ default_files.append(file_info)
584
+
585
+ content = template.render(
586
+ project_name=self.project_name or "Project",
587
+ class_name=self.class_name or "Custom",
588
+ files=self.detected_files,
589
+ file_count=len(self.detected_files),
590
+ default_files=default_files,
591
+ custom_files=custom_files,
592
+ needs_csv_extractor=needs_csv_extractor,
593
+ needs_xlsx_single_extractor=needs_xlsx_single_extractor,
594
+ )
595
+
596
+ etl_path = self.current_dir / "src" / "data_handling" / "etl_factory.py"
597
+
598
+ # Check if file exists
599
+ if etl_path.exists() and not self.skip_confirmation:
600
+ if not click.confirm(f"File {etl_path.name} exists. Overwrite?"):
601
+ return
602
+
603
+ etl_path.write_text(content, encoding="utf-8")
604
+
605
+ def _update_main_py_with_generated_etl(self):
606
+ """Update main.py to use generated ETL and schemas."""
607
+ template = self.jinja_env.get_template("main_generated_etl.py.jinja")
608
+
609
+ content = template.render(
610
+ title=self.title,
611
+ host="127.0.0.1",
612
+ port=8050,
613
+ filename=self.filename or "custom",
614
+ class_name=self.class_name or "Custom",
615
+ has_custom_implementations=self.has_custom_implementations,
616
+ )
617
+
618
+ main_py_path = self.current_dir / "main.py"
619
+ main_py_path.write_text(content, encoding="utf-8")
620
+
621
+ def _generate_main_py(self, title: str, host: str, port: int):
622
+ """Generate main.py from Jinja2 template."""
623
+ template = self.jinja_env.get_template("main.py.jinja")
624
+
625
+ content = template.render(title=title, host=host, port=port)
626
+
627
+ main_py_path = self.current_dir / "main.py"
628
+ main_py_path.write_text(content, encoding="utf-8")
629
+
630
+ def _generate_implementation_file(
631
+ self, template_name: str, target_dir: str, target_file: str
632
+ ):
633
+ """Generate an implementation file from a Jinja2 template."""
634
+ template = self.jinja_env.get_template(f"{template_name}.py.jinja")
635
+
636
+ content = template.render(
637
+ project_name=self.project_name,
638
+ class_name=self.class_name,
639
+ filename=self.filename,
640
+ )
641
+
642
+ file_path = self.current_dir / target_dir / target_file
643
+
644
+ # Ensure the target directory exists
645
+ file_path.parent.mkdir(parents=True, exist_ok=True)
646
+
647
+ # Don't overwrite existing files
648
+ if file_path.exists() and not self.skip_confirmation:
649
+ if not click.confirm(f"File {target_dir}/{target_file} exists. Overwrite?"):
650
+ return
651
+
652
+ file_path.write_text(content, encoding="utf-8")
653
+
654
+ def _update_main_py_with_custom_implementations(self):
655
+ """Update main.py to import and use custom implementations."""
656
+ template = self.jinja_env.get_template("main_custom.py.jinja")
657
+
658
+ content = template.render(
659
+ title=self.title,
660
+ host="127.0.0.1",
661
+ port=8050,
662
+ class_name=self.class_name,
663
+ filename=self.filename,
664
+ )
665
+
666
+ main_py_path = self.current_dir / "main.py"
667
+ main_py_path.write_text(content, encoding="utf-8")
668
+
669
+ @staticmethod
670
+ def _to_pascal_case(text: str) -> str:
671
+ """Convert text to PascalCase."""
672
+ return "".join(word.capitalize() for word in text.split())
673
+
674
+ @staticmethod
675
+ def _to_snake_case(text: str) -> str:
676
+ """Convert text to snake_case."""
677
+ return "_".join(text.lower().split())
678
+
679
+
680
+ def run_quickstart(skip_confirmation: bool = False, title: str | None = None):
681
+ """Entry point for running the quickstart wizard."""
682
+ wizard = QuickstartWizard(skip_confirmation=skip_confirmation, title=title)
683
+ wizard.run()