g360-cli 1.6.1 → 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.
- package/README.md +71 -10
- package/package.json +1 -1
- package/py/pyproject.toml +34 -0
- package/py/src/g360_core/__init__.py +7 -0
- package/py/src/g360_core/flet/__init__.py +3 -0
- package/py/src/g360_core/flet/ingestion_panel.py +218 -0
- package/py/src/g360_core/ingestion.py +480 -0
- package/src/assets/ingestion/core/__init__.py +0 -0
- package/src/assets/ingestion/core/ingestion.py +480 -0
- package/src/assets/ingestion/ui/__init__.py +0 -0
- package/src/assets/ingestion/ui/ingestion_panel.py +221 -0
- package/src/assets/templates/python-flet/pyproject.toml +2 -0
- package/src/assets/templates/python-flet/src/core/ingestion.py +480 -0
- package/src/assets/templates/python-flet/src/main.py +22 -3
- package/src/assets/templates/python-flet/src/test_ingestion.py +121 -0
- package/src/assets/templates/python-flet/src/ui/ingestion_panel.py +221 -0
- package/src/commands/bring.js +87 -0
- package/src/commands/list.js +18 -1
|
@@ -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()
|
package/src/commands/bring.js
CHANGED
|
@@ -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'));
|
package/src/commands/list.js
CHANGED
|
@@ -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) {
|