g360-cli 1.6.0 → 1.7.0

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.
@@ -0,0 +1,221 @@
1
+ import threading
2
+ from pathlib import Path
3
+
4
+ import flet as ft
5
+ import pandas as pd
6
+
7
+ try:
8
+ from g360_core.ingestion import estabilizar_excel_crudo
9
+ except ImportError:
10
+ from core.ingestion import estabilizar_excel_crudo
11
+
12
+
13
+ class IngestionPanel(ft.Container):
14
+ def __init__(self, theme, on_data_loaded=None):
15
+ super().__init__()
16
+ self.theme = theme
17
+ self.on_data_loaded = on_data_loaded
18
+ self.df: pd.DataFrame | None = None
19
+ self.metadata: dict | None = None
20
+ self._build()
21
+
22
+ def _build(self):
23
+ self.file_picker = ft.FilePicker(on_result=self._on_file_result)
24
+ self.status_text = ft.Text(
25
+ "Selecciona un archivo .xls o .xlsx del ERP",
26
+ size=14,
27
+ color=self.theme.muted,
28
+ )
29
+ self.stats_container = ft.Column(spacing=4, visible=False)
30
+ self.alertas_container = ft.Column(spacing=2, visible=False)
31
+ self.preview_table = ft.Column(scroll=ft.ScrollMode.AUTO, visible=False)
32
+
33
+ self.content = ft.Column(
34
+ controls=[
35
+ self.file_picker,
36
+ ft.Row(
37
+ controls=[
38
+ self.theme.accent_button(
39
+ text="Cargar Archivo Excel",
40
+ on_click=self._open_picker,
41
+ ),
42
+ ft.Container(width=12),
43
+ self.status_text,
44
+ ],
45
+ alignment=ft.MainAxisAlignment.START,
46
+ vertical_alignment=ft.CrossAxisAlignment.CENTER,
47
+ ),
48
+ ft.Container(height=12),
49
+ self.stats_container,
50
+ ft.Container(height=8),
51
+ self.alertas_container,
52
+ ft.Container(height=8),
53
+ self.preview_table,
54
+ ],
55
+ spacing=0,
56
+ )
57
+ self.bgcolor = self.theme.surface
58
+ self.border_radius = self.theme.rounded
59
+ self.padding = 20
60
+ self.expand = True
61
+
62
+ def _open_picker(self, e):
63
+ self.file_picker.pick_files(
64
+ file_type=ft.FilePickerFileType.CUSTOM,
65
+ allowed_extensions=["xls", "xlsx", "xlsm"],
66
+ dialog_title="Seleccionar reporte del ERP",
67
+ )
68
+
69
+ def _on_file_result(self, e: ft.FilePickerResultEvent):
70
+ if not e.files:
71
+ self.status_text.value = "No se selecciono ningun archivo."
72
+ self.status_text.color = self.theme.warning
73
+ self.update()
74
+ return
75
+
76
+ archivo = e.files[0]
77
+ ruta = archivo.path
78
+ nombre = archivo.name
79
+ self.status_text.value = f"Procesando: {nombre}..."
80
+ self.status_text.color = self.theme.text
81
+ self.update()
82
+
83
+ self._show_loading(True)
84
+
85
+ def _procesar():
86
+ try:
87
+ df, metadata = estabilizar_excel_crudo(ruta)
88
+ self.df = df
89
+ self.metadata = metadata
90
+ self._mostrar_resultado(df, metadata, nombre)
91
+ if self.on_data_loaded:
92
+ self.on_data_loaded(df, metadata)
93
+ except Exception as exc:
94
+ self.status_text.value = f"Error: {exc}"
95
+ self.status_text.color = self.theme.error
96
+ self._show_loading(False)
97
+ self.update()
98
+
99
+ thread = threading.Thread(target=_procesar, daemon=True)
100
+ thread.start()
101
+
102
+ def _show_loading(self, visible: bool):
103
+ pass
104
+
105
+ def _mostrar_resultado(self, df: pd.DataFrame, metadata: dict, nombre: str):
106
+ self.status_text.value = f"OK: {nombre} ({len(df):,} filas)"
107
+ self.status_text.color = self.theme.success
108
+
109
+ columnas = metadata.get("columnas_finales", metadata.get("columnas", []))
110
+ transformaciones = metadata.get("transformaciones", [])
111
+ alertas = metadata.get("alertas", [])
112
+ columnas_nuevas = metadata.get("columnas_nuevas", [])
113
+
114
+ self.stats_container.controls = [
115
+ ft.Text("Resumen de Ingesta", size=16, weight=ft.FontWeight.BOLD, color=self.theme.text),
116
+ ft.Text(f"Filas estabilizadas: {len(df):,}", size=13, color=self.theme.muted),
117
+ ft.Text(f"Columnas originales: {metadata.get('filas_originales', 0)}", size=13, color=self.theme.muted),
118
+ ft.Text(f"Columnas finales: {len(columnas)}", size=13, color=self.theme.muted),
119
+ ft.Text(f"Archivo: {metadata.get('archivo', 'N/A')}", size=13, color=self.theme.muted),
120
+ ft.Text(f"Moneda: {metadata.get('moneda', 'N/A')}", size=13, color=self.theme.muted),
121
+ ft.Divider(color=self.theme.bg, height=8),
122
+ ]
123
+
124
+ if columnas_nuevas:
125
+ nuevas_text = ft.Text(
126
+ "Columnas derivadas:",
127
+ size=13, weight=ft.FontWeight.BOLD, color=self.theme.accent,
128
+ )
129
+ chips = ft.Row(
130
+ controls=[
131
+ ft.Container(
132
+ content=ft.Text(col, size=10, color=self.theme.bg),
133
+ bgcolor=self.theme.accent,
134
+ border_radius=12,
135
+ padding=ft.padding.only(left=8, right=8, top=3, bottom=3),
136
+ )
137
+ for col in columnas_nuevas
138
+ ],
139
+ spacing=6,
140
+ wrap=True,
141
+ )
142
+ self.stats_container.controls.extend([nuevas_text, chips])
143
+
144
+ if transformaciones:
145
+ self.stats_container.controls.append(
146
+ ft.Divider(color=self.theme.bg, height=8)
147
+ )
148
+ self.stats_container.controls.append(
149
+ ft.Text("Transformaciones:", size=13, weight=ft.FontWeight.BOLD, color=self.theme.text)
150
+ )
151
+ for t in transformaciones[-8:]:
152
+ self.stats_container.controls.append(
153
+ ft.Row(
154
+ controls=[
155
+ ft.Container(
156
+ content=ft.Text(">", size=11, color=self.theme.accent),
157
+ width=16,
158
+ ),
159
+ ft.Text(t, size=11, color=self.theme.muted),
160
+ ],
161
+ spacing=0,
162
+ )
163
+ )
164
+
165
+ self.stats_container.visible = True
166
+
167
+ if alertas:
168
+ self.alertas_container.controls = [
169
+ ft.Divider(color=self.theme.bg, height=4),
170
+ ft.Text("Alertas:", size=13, weight=ft.FontWeight.BOLD, color=self.theme.warning),
171
+ ]
172
+ for a in alertas:
173
+ self.alertas_container.controls.append(
174
+ ft.Row(
175
+ controls=[
176
+ ft.Icon(ft.icons.WARNING_AMBER_ROUNDED, size=14, color=self.theme.warning),
177
+ ft.Text(a, size=11, color=self.theme.warning),
178
+ ],
179
+ spacing=4,
180
+ )
181
+ )
182
+ self.alertas_container.visible = True
183
+
184
+ filas_preview = df.head(5)
185
+ columnas_preview = columnas[:8]
186
+
187
+ data_rows = []
188
+ for _, row in filas_preview.iterrows():
189
+ cells = []
190
+ for col in columnas_preview:
191
+ val = row.get(col, "")
192
+ v_str = str(val)[:30] if not pd.isna(val) else "-"
193
+ cells.append(
194
+ ft.DataCell(ft.Text(v_str, size=10, color=self.theme.text))
195
+ )
196
+ data_rows.append(ft.DataRow(cells=cells))
197
+
198
+ header_cells = [
199
+ ft.DataColumn(ft.Text(col[:16], size=10, color=self.theme.accent, weight=ft.FontWeight.BOLD))
200
+ for col in columnas_preview
201
+ ]
202
+
203
+ tabla = ft.DataTable(
204
+ columns=header_cells,
205
+ rows=data_rows,
206
+ heading_text_color=self.theme.accent,
207
+ horizontal_margin=4,
208
+ column_spacing=16,
209
+ )
210
+
211
+ self.preview_table.controls = [
212
+ ft.Text("Vista previa (5 primeras filas):", size=13, weight=ft.FontWeight.BOLD, color=self.theme.text),
213
+ ft.Container(
214
+ content=tabla,
215
+ bgcolor=self.theme.bg,
216
+ border_radius=8,
217
+ padding=8,
218
+ ),
219
+ ]
220
+ self.preview_table.visible = True
221
+ self.update()
@@ -5,6 +5,12 @@ import { fileURLToPath } from 'url';
5
5
  import { progress } from '../lib/progress.js';
6
6
 
7
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const INGESTION_FILES = [
9
+ { src: 'ingestion/core/ingestion.py', dest: 'src/core/ingestion.py' },
10
+ { src: 'ingestion/core/__init__.py', dest: 'src/core/__init__.py' },
11
+ { src: 'ingestion/ui/ingestion_panel.py', dest: 'src/ui/ingestion_panel.py' },
12
+ { src: 'ingestion/ui/__init__.py', dest: 'src/ui/__init__.py' },
13
+ ];
8
14
 
9
15
  export async function bring(asset, options) {
10
16
  const { path: targetPath = '.', dryRun = false, force = false } = options;
@@ -26,6 +32,10 @@ export async function bring(asset, options) {
26
32
  return;
27
33
  }
28
34
 
35
+ if (asset === 'ingestion') {
36
+ return installIngestion(targetDir, dryRun, force);
37
+ }
38
+
29
39
  await copyAssets(assetPath, targetDir, dryRun, force);
30
40
 
31
41
  if (asset.startsWith('brand')) {
@@ -104,6 +114,83 @@ async function applyBrand(targetDir, brandName, dryRun) {
104
114
  console.log(chalk.gray(` Signature: ${brand.signature?.text || 'none'}`));
105
115
  }
106
116
 
117
+ async function installIngestion(targetDir, dryRun, force) {
118
+ const assetsPath = path.join(__dirname, '../assets');
119
+ const ingestionDir = path.join(assetsPath, 'ingestion');
120
+
121
+ if (!fs.existsSync(ingestionDir)) {
122
+ console.error(chalk.red('āŒ Ingestion asset not found in package.'));
123
+ return;
124
+ }
125
+
126
+ const hasFlet = (
127
+ fs.existsSync(path.join(targetDir, 'pyproject.toml')) &&
128
+ fs.existsSync(path.join(targetDir, 'src', 'main.py'))
129
+ );
130
+
131
+ if (!hasFlet) {
132
+ console.error(chalk.red('āŒ This command requires a G360 Flet project.'));
133
+ console.log(chalk.gray(' Run "g360 init <name> --template python-flet" first.'));
134
+ return;
135
+ }
136
+
137
+ const srcCore = path.join(targetDir, 'src', 'core');
138
+ const srcUi = path.join(targetDir, 'src', 'ui');
139
+
140
+ if (!fs.existsSync(srcCore)) {
141
+ if (!dryRun) fs.mkdirpSync(srcCore);
142
+ console.log(chalk.gray(' Created src/core/'));
143
+ }
144
+ if (!fs.existsSync(srcUi)) {
145
+ if (!dryRun) fs.mkdirpSync(srcUi);
146
+ console.log(chalk.gray(' Created src/ui/'));
147
+ }
148
+
149
+ if (dryRun) {
150
+ console.log(chalk.yellow('\nšŸ“‹ DRY RUN - Would install:'));
151
+ for (const f of INGESTION_FILES) {
152
+ console.log(chalk.gray(` ${f.dest}`));
153
+ }
154
+ return;
155
+ }
156
+
157
+ console.log(chalk.cyan('\nšŸ“¦ Option A — Install via pip (recommended):'));
158
+ console.log(chalk.white(' pip install g360-core'));
159
+
160
+ const progressBar = progress('Installing local fallback files...');
161
+
162
+ try {
163
+ let copied = 0;
164
+ for (const f of INGESTION_FILES) {
165
+ const srcFile = path.join(assetsPath, f.src);
166
+ const destFile = path.join(targetDir, f.dest);
167
+
168
+ if (fs.existsSync(destFile) && !force) {
169
+ console.log(chalk.yellow(`\n⚠ ${f.dest} exists. Use --force to overwrite.`));
170
+ continue;
171
+ }
172
+
173
+ await fs.copy(srcFile, destFile, { overwrite: force });
174
+ copied++;
175
+ }
176
+
177
+ progressBar.stop();
178
+
179
+ if (copied > 0) {
180
+ console.log(chalk.green(`\nāœ… ${copied} local fallback file(s) installed\n`));
181
+ console.log(chalk.gray(' These files are used when g360-core pip package is not available.'));
182
+ console.log(chalk.cyan('\n Import (auto-detects pip package → local fallback):'));
183
+ console.log(chalk.white(' from ui.ingestion_panel import IngestionPanel'));
184
+ }
185
+
186
+ console.log(chalk.cyan('\n Or add to your pyproject.toml:'));
187
+ console.log(chalk.white(' "g360-core>=0.1.0"'));
188
+ } catch (error) {
189
+ progressBar.stop();
190
+ console.error(chalk.red(`\nāŒ Error: ${error.message}`));
191
+ }
192
+ }
193
+
107
194
  async function copyAssets(src, dest, dryRun, force) {
108
195
  if (dryRun) {
109
196
  console.log(chalk.yellow('šŸ“‹ DRY RUN - No files will be copied\n'));
@@ -15,7 +15,8 @@ export async function list(type, options) {
15
15
  templates: [],
16
16
  components: [],
17
17
  skills: [],
18
- brands: []
18
+ brands: [],
19
+ ingestion: []
19
20
  };
20
21
 
21
22
  const templatesPath = path.join(assetsPath, 'templates');
@@ -47,6 +48,11 @@ export async function list(type, options) {
47
48
  }
48
49
  }
49
50
 
51
+ const ingestionPath = path.join(assetsPath, 'ingestion');
52
+ if (fs.existsSync(ingestionPath)) {
53
+ assets.ingestion = ['ingestion'];
54
+ }
55
+
50
56
  if (fs.existsSync(snippetsPath)) {
51
57
  const snippetsJsonPath = path.join(snippetsPath, 'snippets.json');
52
58
  if (fs.existsSync(snippetsJsonPath)) {
@@ -115,6 +121,17 @@ export async function list(type, options) {
115
121
  }
116
122
  }
117
123
 
124
+ if (!type || type === 'all' || type === 'ingestion') {
125
+ console.log(chalk.bold.yellow('\nšŸ“„ Ingestion Modules:'));
126
+ const ingestionPath = path.join(assetsPath, 'ingestion');
127
+ if (fs.existsSync(ingestionPath)) {
128
+ console.log(chalk.gray(' - ingestion (ERP data normalizer for Flet)'));
129
+ console.log(chalk.gray(' Run: g360 bring ingestion'));
130
+ } else {
131
+ console.log(chalk.gray(' No ingestion module found'));
132
+ }
133
+ }
134
+
118
135
  if (!type || type === 'all' || type === 'brands') {
119
136
  console.log(chalk.bold.yellow('\nšŸŽØ Brands:'));
120
137
  if (assets.brands.length) {
@@ -231,12 +231,29 @@ async function installFlet(targetDir, { force, mode, version, position }) {
231
231
  footerCode = `\n # Firma G360\n page.add(g360_footer(${versionAttr.trim() ? versionAttr : ''}))\n`;
232
232
  }
233
233
 
234
- if (!mainContent.includes('g360_signature')) {
235
- mainContent = mainContent.replace(
236
- /(from\s+core\.\w+.*\n)/,
237
- `$1${importLine}\n`
238
- );
234
+ if (!mainContent.includes('from core.components.g360_signature')) {
235
+ // Intentar agregar import despues de otros imports de core
236
+ const importRegex = /(from\s+core\.\w+.*\n)/;
237
+ if (importRegex.test(mainContent)) {
238
+ mainContent = mainContent.replace(importRegex, `$1${importLine}\n`);
239
+ } else {
240
+ // Si no hay imports de core, agregar despues del ultimo import
241
+ const lastImport = mainContent.lastIndexOf('import ');
242
+ if (lastImport !== -1) {
243
+ const endOfLine = mainContent.indexOf('\n', lastImport);
244
+ if (endOfLine !== -1) {
245
+ mainContent = mainContent.slice(0, endOfLine + 1) + importLine + '\n' + mainContent.slice(endOfLine + 1);
246
+ } else {
247
+ mainContent = mainContent + '\n' + importLine + '\n';
248
+ }
249
+ } else {
250
+ // Si no hay imports, agregar al inicio
251
+ mainContent = importLine + '\n\n' + mainContent;
252
+ }
253
+ }
254
+ }
239
255
 
256
+ if (!mainContent.includes('g360_footer()') && !mainContent.includes('G360Signature')) {
240
257
  if (mainContent.includes('page.add')) {
241
258
  const lastAdd = mainContent.lastIndexOf('page.add');
242
259
  const endOfLine = mainContent.indexOf('\n', lastAdd);
@@ -244,9 +261,9 @@ async function installFlet(targetDir, { force, mode, version, position }) {
244
261
  } else {
245
262
  mainContent += footerCode;
246
263
  }
247
-
248
- fs.writeFileSync(mainPyPath, mainContent, 'utf8');
249
- console.log(chalk.gray(' main.py actualizado'));
250
264
  }
265
+
266
+ fs.writeFileSync(mainPyPath, mainContent, 'utf8');
267
+ console.log(chalk.gray(' main.py actualizado'));
251
268
  }
252
269
  }