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.
- algomancy_quickstart/__init__.py +2 -0
- algomancy_quickstart/asset_manager.py +202 -0
- algomancy_quickstart/data_inference.py +517 -0
- algomancy_quickstart/main.py +62 -0
- algomancy_quickstart/quickstart.py +683 -0
- algomancy_quickstart/styling_wizard.py +347 -0
- algomancy_quickstart/templates/__init__.py +0 -0
- algomancy_quickstart/templates/algorithm.py.jinja +104 -0
- algomancy_quickstart/templates/assets/CQM-logo-white.png +0 -0
- algomancy_quickstart/templates/assets/cqm-button-white.png +0 -0
- algomancy_quickstart/templates/assets/cqm-button.png +0 -0
- algomancy_quickstart/templates/assets/cqm-logo.png +0 -0
- algomancy_quickstart/templates/assets/css/button_colors.css +285 -0
- algomancy_quickstart/templates/assets/css/cqm_loader.css +47 -0
- algomancy_quickstart/templates/assets/css/sidebar_layout.css +189 -0
- algomancy_quickstart/templates/assets/css/theme_colors.css +90 -0
- algomancy_quickstart/templates/assets/letter-c.svg +4 -0
- algomancy_quickstart/templates/assets/letter-m.svg +4 -0
- algomancy_quickstart/templates/assets/letter-q.svg +4 -0
- algomancy_quickstart/templates/assets/letters/letter-c.png +0 -0
- algomancy_quickstart/templates/assets/letters/letter-m.png +0 -0
- algomancy_quickstart/templates/assets/letters/letter-q.png +0 -0
- algomancy_quickstart/templates/assets/pepsi_girl.jpeg +0 -0
- algomancy_quickstart/templates/assets/style.css +421 -0
- algomancy_quickstart/templates/compare_page.py.jinja +133 -0
- algomancy_quickstart/templates/data_page.py.jinja +94 -0
- algomancy_quickstart/templates/etl_factory.py.jinja +108 -0
- algomancy_quickstart/templates/etl_factory_generated.py.jinja +82 -0
- algomancy_quickstart/templates/generated_schemas.py.jinja +55 -0
- algomancy_quickstart/templates/home_page.py.jinja +65 -0
- algomancy_quickstart/templates/kpi.py.jinja +76 -0
- algomancy_quickstart/templates/main.py.jinja +42 -0
- algomancy_quickstart/templates/main_custom.py.jinja +55 -0
- algomancy_quickstart/templates/main_generated_etl.py.jinja +72 -0
- algomancy_quickstart/templates/main_with_styling.py.jinja +83 -0
- algomancy_quickstart/templates/overview_page.py.jinja +98 -0
- algomancy_quickstart/templates/scenario_page.py.jinja +77 -0
- algomancy_quickstart/templates/schema.py.jinja +58 -0
- algomancy_quickstart/templates/styling_config.py.jinja +53 -0
- algomancy_quickstart-0.7.0.dist-info/METADATA +29 -0
- algomancy_quickstart-0.7.0.dist-info/RECORD +42 -0
- 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()
|