sibi-flux 2026.1.4__py3-none-any.whl → 2026.1.6__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.
sibi_flux/cli.py CHANGED
@@ -34,6 +34,42 @@ def init(
34
34
  initialize_project(project_name, lib, app)
35
35
 
36
36
 
37
+ @app.command()
38
+ def create_app(
39
+ name: str = typer.Argument(..., help="Name of the application to create"),
40
+ ):
41
+ """
42
+ Create a new application within an existing Sibi Flux project.
43
+
44
+ Generates standard directory structure in `<project_root>/<name>`.
45
+ """
46
+ from sibi_flux.init.app import init_app
47
+ init_app(name)
48
+
49
+
50
+ @app.command()
51
+ def create_cubes(
52
+ app_name: str = typer.Argument(..., help="Name of the application"),
53
+ ):
54
+ """
55
+ Generate app-specific Datacube extensions from `<app_name>/datacubes/datacubes.yaml`.
56
+ """
57
+ from sibi_flux.init.cube_extender import create_cubes
58
+ create_cubes(app_name)
59
+
60
+
61
+ @app.command()
62
+ def propose_cubes(
63
+ db_domain: str = typer.Argument(..., help="Database domain to filter by (e.g., 'ibis_dev')"),
64
+ app_name: str = typer.Argument(..., help="Name of the target application"),
65
+ ):
66
+ """
67
+ Scan global registry for datacubes in <db_domain> and add them to <app_name>/datacubes/datacubes.yaml.
68
+ """
69
+ from sibi_flux.init.cube_proposer import propose_cubes
70
+ propose_cubes(db_domain, app_name)
71
+
72
+
37
73
  @app.command()
38
74
  def env(
39
75
  project_path: Path = typer.Argument(Path("."), help="Project root directory"),
sibi_flux/datacube/cli.py CHANGED
@@ -258,6 +258,42 @@ def sync(
258
258
 
259
259
  # Start with empty/default registry
260
260
  config_data = _load_and_resolve_config(config_path)
261
+
262
+ # Load existing Global Registry to preserve manual edits (e.g. custom_name)
263
+ # We must flatten the grouped structure (by config object) into a single tables dict
264
+ params = context.params or {}
265
+ reg_rel_path = params.get("paths", {}).get("repositories", {}).get(
266
+ "global_datacube_registry_file"
267
+ ) or params.get("global_datacube_registry_file")
268
+
269
+ if reg_rel_path:
270
+ reg_file = Path(reg_rel_path)
271
+ if not reg_file.is_absolute():
272
+ try:
273
+ # Heuristic: config_path is in generators/datacubes/, project root is 3 levels up
274
+ prj_root = config_path.parent.parent.parent
275
+ reg_file = prj_root / reg_rel_path
276
+ except Exception:
277
+ reg_file = Path.cwd() / reg_rel_path
278
+
279
+ if reg_file.exists():
280
+ try:
281
+ with open(reg_file, "r") as rf:
282
+ existing_reg_data = yaml.safe_load(rf) or {}
283
+
284
+ flat_tables = {}
285
+ for grp, tbls in existing_reg_data.items():
286
+ if isinstance(tbls, dict):
287
+ for t, t_meta in tbls.items():
288
+ # Inject the config object (group key) so DatacubeRegistry knows the connection
289
+ t_meta["connection_obj"] = grp
290
+ flat_tables[t] = t_meta
291
+
292
+ config_data["tables"] = flat_tables
293
+ console.print(f"[dim]Loaded {len(flat_tables)} existing registry entries for merge preservation.[/dim]")
294
+ except Exception as e:
295
+ console.print(f"[yellow]Warning: Could not load existing registry: {e}[/yellow]")
296
+
261
297
  registry = DatacubeRegistry(config_data, params=context.params)
262
298
 
263
299
  # --- Aggregation Phase ---
@@ -378,9 +414,16 @@ def sync(
378
414
  imp = [db_imp] if db_imp else registry.global_imports
379
415
  return resolve_db_url(conf_name, imp)
380
416
 
381
- _run_field_map_generation(
417
+ generated_maps = _run_field_map_generation(
382
418
  context, config_path, databases, get_url_safe, force=force
383
419
  )
420
+
421
+ if generated_maps:
422
+ for table_name, mod_path in generated_maps.items():
423
+ if table_name in registry.tables:
424
+ # console.print(f"[green]DEBUG: Updating {table_name} with field_map {mod_path}[/green]")
425
+ registry.tables[table_name]["field_map"] = mod_path
426
+
384
427
  # Ensure new modules are picked up
385
428
  importlib.invalidate_caches()
386
429
 
@@ -533,11 +576,18 @@ def sync(
533
576
  if conf_obj not in generated_registry:
534
577
  generated_registry[conf_obj] = {}
535
578
 
536
- generated_registry[conf_obj][t_name] = {
579
+ # Preserve custom_name from existing registry if present
580
+ existing_meta = registry.get_table_details(t_name)
581
+ custom_name = existing_meta.get("custom_name")
582
+
583
+ entry_data = {
537
584
  "class_name": cls_n,
538
585
  "path": str(rel_path),
586
+ "custom_name": custom_name,
539
587
  }
540
588
 
589
+ generated_registry[conf_obj][t_name] = entry_data
590
+
541
591
  console.print(summary_table)
542
592
 
543
593
  # --- Write Datacube Registry ---
@@ -1904,6 +1954,9 @@ def whitelist(
1904
1954
  # Merge: update rule defaults only if not set in existing
1905
1955
  merged = rule_meta.copy()
1906
1956
  merged.update(existing_meta) # Existing overwrites rule
1957
+
1958
+ # Legacy Cleanup: We moved custom_name to Registry
1959
+ merged.pop("custom_name", None)
1907
1960
 
1908
1961
  # Restore calculated paths (Enforce Relative)
1909
1962
  if "datacube_path" in rule_meta:
@@ -2099,6 +2152,8 @@ def _run_field_map_generation(
2099
2152
  "[yellow]Warning: Could not determine global field_maps_dir for clean build.[/yellow]"
2100
2153
  )
2101
2154
 
2155
+ generated_maps = {}
2156
+
2102
2157
  for db in target_dbs:
2103
2158
  # console.print(f"DEBUG: Processing DB entry: {db} (Type: {type(db)})")
2104
2159
  if isinstance(db, str):
@@ -2223,10 +2278,6 @@ def _run_field_map_generation(
2223
2278
  if not found:
2224
2279
  rules = []
2225
2280
 
2226
- # console.print(f"DEBUG: Loaded {len(rules)} rules from {rules_path} for {conn_obj}")
2227
-
2228
- # console.print(f"DEBUG: Loaded {len(rules)} rules from {rules_path}")
2229
-
2230
2281
  # Support List or Dict Format
2231
2282
  scoped_data = registry_data.get(conn_obj, {})
2232
2283
  if isinstance(scoped_data, list):
@@ -2448,6 +2499,21 @@ def _run_field_map_generation(
2448
2499
  with open(target_file, "w") as f:
2449
2500
  f.write("\n".join(lines))
2450
2501
 
2502
+ # Calculate Import Path
2503
+ try:
2504
+ # Ensure we get relative path to project root (which should be sys.path root)
2505
+ if "project_root" not in locals():
2506
+ project_root = Path.cwd()
2507
+
2508
+ rel_py_path = target_file.relative_to(project_root)
2509
+ module_path = str(rel_py_path.with_suffix("")).replace("/", ".")
2510
+ generated_maps[table_name] = f"{module_path}.field_map"
2511
+ except ValueError:
2512
+ # Fallback if outside project root?
2513
+ pass
2514
+ except Exception as e:
2515
+ pass
2516
+
2451
2517
  except Exception as e:
2452
2518
  console.print(f"[red]Error processing {table_name}: {e}[/red]")
2453
2519
  continue
@@ -2464,6 +2530,8 @@ def _run_field_map_generation(
2464
2530
  except Exception as e:
2465
2531
  console.print(f"[red]Failed to save Global Field Repository: {e}[/red]")
2466
2532
 
2533
+ return generated_maps
2534
+
2467
2535
 
2468
2536
  if __name__ == "__main__":
2469
2537
  app()
@@ -531,6 +531,15 @@ class DatacubeRegistry:
531
531
  or self.config.get("class_suffix")
532
532
  or self.params.get("class_suffix", "Dc")
533
533
  )
534
+ self._enforce_custom_names()
535
+
536
+ def _enforce_custom_names(self) -> None:
537
+ """
538
+ Ensures that if custom_name is set, it overrides class_name explicitly.
539
+ """
540
+ for table, meta in self.tables.items():
541
+ if meta.get("custom_name"):
542
+ meta["class_name"] = meta["custom_name"]
534
543
 
535
544
  def get_table_details(self, table_name: str) -> dict[str, Any]:
536
545
  return self.tables.get(table_name, {})
@@ -573,7 +582,10 @@ class DatacubeRegistry:
573
582
  elif k not in existing:
574
583
  existing[k] = v
575
584
 
576
- if "class_name" not in existing:
585
+ # Override class_name if custom_name is set
586
+ if existing.get("custom_name"):
587
+ existing["class_name"] = existing["custom_name"]
588
+ elif "class_name" not in existing:
577
589
  existing["class_name"] = new_details.get("class_name")
578
590
 
579
591
  self.tables[table] = existing
@@ -554,8 +554,23 @@ class DiscoveryOrchestrator:
554
554
  "[yellow]Prune active: Registry replaced with discovery results.[/]"
555
555
  )
556
556
  else:
557
- # Merge: Update existing keys, add new ones.
558
- current_data["tables"].update(new_entries)
557
+ # Smart Merge: Preserve 'custom_name' and 'class_name' from existing entries
558
+ # if they are not explicitly overridden by the new entry.
559
+ for table, new_meta in new_entries.items():
560
+ if table in current_data["tables"]:
561
+ existing = current_data["tables"][table]
562
+
563
+ # 1. Preserve custom_name if new entry doesn't specify one
564
+ if not new_meta.get("custom_name") and existing.get("custom_name"):
565
+ new_meta["custom_name"] = existing["custom_name"]
566
+
567
+ # 2. Also preserve class_name (since it's driven by custom_name)
568
+ # We only preserve class_name if custom_name was preserved
569
+ # AND new entry didn't explicitly change class_name logic (unlikely unless configured)
570
+ if existing.get("class_name"):
571
+ new_meta["class_name"] = existing["class_name"]
572
+
573
+ current_data["tables"][table] = new_meta
559
574
 
560
575
  # Sort tables for readability
561
576
  current_data["tables"] = dict(sorted(current_data["tables"].items()))
sibi_flux/init/app.py ADDED
@@ -0,0 +1,111 @@
1
+
2
+ from pathlib import Path
3
+ import os
4
+ from rich.console import Console
5
+ import typer
6
+ import yaml
7
+
8
+ console = Console()
9
+
10
+ def init_app(name: str) -> None:
11
+ """
12
+ Initialize a new application within the current project root.
13
+
14
+ Args:
15
+ name: The name of the application to create (e.g., 'inventory', 'pricing').
16
+ """
17
+
18
+ # 1. Validation: Ensure we are in a Sibi Flux project root (check for pyproject.toml as heuristic)
19
+ cwd = Path(os.getcwd())
20
+ if not (cwd / "pyproject.toml").exists():
21
+ console.print("[yellow]Warning: pyproject.toml not found. Are you in a project root?[/yellow]")
22
+
23
+ app_dir = cwd / name
24
+
25
+ if app_dir.exists():
26
+ console.print(f"[red]Error: Application directory '{name}' already exists.[/red]")
27
+ raise typer.Exit(code=1)
28
+
29
+ console.print(f"[bold blue]Initializing new application: {name}[/bold blue]")
30
+
31
+ # 2. Create Directory Structure
32
+ structure = [
33
+ "api",
34
+ "datacubes",
35
+ "readers",
36
+ "aggregators",
37
+ ]
38
+
39
+ app_dir.mkdir()
40
+ (app_dir / "__init__.py").touch()
41
+
42
+ for folder in structure:
43
+ path = app_dir / folder
44
+ path.mkdir()
45
+ (path / "__init__.py").touch()
46
+
47
+ # 2.1 Create datacubes extension registry template
48
+ datacubes_yaml = app_dir / "datacubes" / "datacubes.yaml"
49
+ template_yaml = """
50
+ cubes:
51
+ # Define your app-specific datacube extensions here
52
+ # - source: dataobjects.gencubes.ibis_dev.products.products_cubes.ProductsDc
53
+ # name: LogisticsProductsDc
54
+ # module: products # -> logistics/datacubes/products.py
55
+ """
56
+ datacubes_yaml.write_text(template_yaml.strip())
57
+
58
+ # 3. Create Basic Router Template
59
+ router_template = f"""
60
+ from fastapi import APIRouter
61
+
62
+ router = APIRouter()
63
+
64
+ @router.get("/")
65
+ async def root():
66
+ return {{"message": "Hello from {name}!"}}
67
+ """
68
+ (app_dir / "api" / "main.py").write_text(router_template.strip())
69
+
70
+ # 4. Register in conf/apps.yaml
71
+ conf_dir = cwd / "conf"
72
+ if not conf_dir.exists():
73
+ conf_dir.mkdir()
74
+
75
+ apps_yaml_path = conf_dir / "apps.yaml"
76
+
77
+ apps_data = {"apps": []}
78
+ if apps_yaml_path.exists():
79
+ try:
80
+ with open(apps_yaml_path, "r") as f:
81
+ loaded = yaml.safe_load(f)
82
+ if loaded and isinstance(loaded, dict) and "apps" in loaded:
83
+ apps_data = loaded
84
+ elif loaded is None:
85
+ pass # empty file
86
+ else:
87
+ console.print(f"[yellow]Warning: conf/apps.yaml has unexpected structure. Initializing with empty list.[/yellow]")
88
+ except Exception as e:
89
+ console.print(f"[yellow]Warning: Could not read existing apps.yaml: {e}[/yellow]")
90
+
91
+ if name not in apps_data["apps"]:
92
+ apps_data["apps"].append(name)
93
+ with open(apps_yaml_path, "w") as f:
94
+ yaml.dump(apps_data, f, default_flow_style=False)
95
+ console.print(f"[green]Registered '{name}' in conf/apps.yaml[/green]")
96
+ else:
97
+ console.print(f"[yellow]App '{name}' already registered in conf/apps.yaml[/yellow]")
98
+
99
+ # 5. Success Message & Instructions
100
+ console.print(f"[bold green]Successfully created application '{name}'![/bold green]")
101
+ console.print(f"Location: {name}/")
102
+ console.print("\n[yellow]Next Steps:[/yellow]")
103
+ console.print(f"1. Register your new router in [bold]main.py[/bold] (or wherever your app is defined):")
104
+
105
+ code_snippet = f"""
106
+ from {name}.api.main import router as {name}_router
107
+
108
+ app.include_router({name}_router, prefix="/{name}", tags=["{name}"])
109
+ """
110
+ console.print(code_snippet)
111
+
@@ -0,0 +1,149 @@
1
+
2
+ import yaml
3
+ from pathlib import Path
4
+ import os
5
+ from typing import List, Dict, Any
6
+ from rich.console import Console
7
+ import typer
8
+ import importlib.util
9
+ import sys
10
+
11
+ console = Console()
12
+
13
+ def create_cubes(app_name: str) -> None:
14
+ """
15
+ Generate app-specific Datacube extensions based on local configuration.
16
+
17
+ Args:
18
+ app_name: The name of the application (must exist in project root).
19
+ """
20
+ cwd = Path(os.getcwd())
21
+
22
+ # 1. Validate App Directory
23
+ app_dir = cwd / app_name
24
+ if not app_dir.exists() or not app_dir.is_dir():
25
+ console.print(f"[red]Error: Application directory '{app_name}' not found.[/red]")
26
+ raise typer.Exit(code=1)
27
+
28
+ datacubes_dir = app_dir / "datacubes"
29
+ if not datacubes_dir.exists():
30
+ console.print(f"[yellow]Creating 'datacubes' directory for {app_name}...[/yellow]")
31
+ datacubes_dir.mkdir(parents=True, exist_ok=True)
32
+ (datacubes_dir / "__init__.py").touch()
33
+
34
+ # 2. Load Registry (datacubes.yaml)
35
+ registry_path = datacubes_dir / "datacubes.yaml"
36
+ if not registry_path.exists():
37
+ console.print(f"[red]Error: Registry file not found at {registry_path}[/red]")
38
+ console.print("Please define your extensions in this file first.")
39
+ # Create a template if missing?
40
+ template = """
41
+ cubes:
42
+ # - source: dataobjects.gencubes.ibis_dev.products.products_cubes.ProductsDc
43
+ # name: LogisticsProductsDc
44
+ # module: products
45
+ """
46
+ registry_path.write_text(template.strip())
47
+ console.print(f"[green]Created template at {registry_path}. Edit it and run again.[/green]")
48
+ raise typer.Exit(code=1)
49
+
50
+ try:
51
+ with open(registry_path, "r") as f:
52
+ config = yaml.safe_load(f)
53
+ except Exception as e:
54
+ console.print(f"[red]Error parsing {registry_path}: {e}[/red]")
55
+ raise typer.Exit(code=1)
56
+
57
+ if not config or "cubes" not in config or not isinstance(config["cubes"], list):
58
+ console.print(f"[yellow]Warning: No 'cubes' list found in {registry_path}.[/yellow]")
59
+ return
60
+
61
+ # 3. Group by Module
62
+ from collections import defaultdict
63
+ cubes_by_module = defaultdict(list)
64
+ for entry in config["cubes"]:
65
+ mod = entry.get("module")
66
+ if mod:
67
+ cubes_by_module[mod].append(entry)
68
+
69
+ console.print(f"[bold blue]Processing {len(config['cubes'])} datacube extensions across {len(cubes_by_module)} modules...[/bold blue]")
70
+
71
+ for module_name, entries in cubes_by_module.items():
72
+ process_module_group(module_name, entries, datacubes_dir, app_name)
73
+
74
+
75
+ def process_module_group(module_name: str, entries: List[Dict[str, Any]], datacubes_dir: Path, app_name: str) -> None:
76
+ target_file = datacubes_dir / f"{module_name}.py"
77
+
78
+ # Read existing content if file exists
79
+ existing_content = ""
80
+ if target_file.exists():
81
+ existing_content = target_file.read_text()
82
+
83
+ new_imports = set()
84
+ new_classes = []
85
+
86
+ for entry in entries:
87
+ source_path = entry.get("source")
88
+ class_name = entry.get("name")
89
+
90
+ if not all([source_path, class_name]):
91
+ console.print(f"[red]Skipping invalid entry in {module_name}: {entry}[/red]")
92
+ continue
93
+
94
+ # Check for idempotency: does class definition already exist?
95
+ if f"class {class_name}" in existing_content:
96
+ console.print(f"[dim]Skipping {class_name} (already exists in {module_name}.py)[/dim]")
97
+ continue
98
+
99
+ # Parse Source
100
+ try:
101
+ source_module_str, source_class = source_path.rsplit(".", 1)
102
+ except ValueError:
103
+ console.print(f"[red]Invalid source format '{source_path}' for {class_name}[/red]")
104
+ continue
105
+
106
+ # Prepare Import
107
+ import_stmt = f"from {source_module_str} import {source_class}"
108
+
109
+ # Check if import exists in file or is already queued
110
+ if import_stmt not in existing_content:
111
+ new_imports.add(import_stmt)
112
+
113
+ # Prepare Class
114
+ class_code = f"""
115
+ class {class_name}({source_class}):
116
+ \"\"\"
117
+ App-specific extension of {source_class} for '{app_name}'.
118
+
119
+ Source: {source_path}
120
+ \"\"\"
121
+ pass
122
+ """
123
+ new_classes.append(class_code.strip())
124
+ console.print(f"[green]Queued generation for {class_name}[/green]")
125
+
126
+ if new_classes:
127
+ mode = "a" if target_file.exists() else "w"
128
+
129
+ content_parts = []
130
+
131
+ # Add imports first
132
+ if new_imports:
133
+ sorted_imports = sorted(list(new_imports))
134
+ content_parts.extend(sorted_imports)
135
+ content_parts.append("") # Spacer
136
+
137
+ # Add classes
138
+ content_parts.extend(new_classes)
139
+
140
+ with open(target_file, mode) as f:
141
+ if mode == "a" and existing_content.strip():
142
+ f.write("\n\n") # Ensure separation from previous content
143
+
144
+ f.write("\n\n".join(content_parts))
145
+ f.write("\n")
146
+
147
+ console.print(f"[green]Updated {target_file.name} with {len(new_classes)} new classes.[/green]")
148
+ else:
149
+ console.print(f"[dim]No changes needed for {target_file.name}[/dim]")
@@ -0,0 +1,134 @@
1
+
2
+ import yaml
3
+ from pathlib import Path
4
+ import os
5
+ from typing import List, Dict, Any
6
+ from rich.console import Console
7
+ import typer
8
+ import sys
9
+
10
+ console = Console()
11
+
12
+ def propose_cubes(db_domain: str, app_name: str) -> None:
13
+ """
14
+ Scans the global datacube registry for cubes matching 'db_domain'
15
+ and adds them to the app's datacubes.yaml.
16
+
17
+ Args:
18
+ db_domain: The database domain / folder name to filter by (e.g., 'ibis_dev', 'istmo360n').
19
+ app_name: The name of the target application.
20
+ """
21
+ cwd = Path(os.getcwd())
22
+
23
+ # 1. Validate App Directory
24
+ app_dir = cwd / app_name
25
+ if not app_dir.exists() or not app_dir.is_dir():
26
+ console.print(f"[red]Error: Application directory '{app_name}' not found.[/red]")
27
+ raise typer.Exit(code=1)
28
+
29
+ datacubes_dir = app_dir / "datacubes"
30
+ datacubes_dir.mkdir(parents=True, exist_ok=True) # Ensure exists
31
+
32
+ registry_path = datacubes_dir / "datacubes.yaml"
33
+
34
+ # 2. Locate Global Registry
35
+ # Heuristic: dataobjects/globals/datacube_registry.yaml
36
+ global_reg_path = cwd / "dataobjects/globals/datacube_registry.yaml"
37
+
38
+ if not global_reg_path.exists():
39
+ console.print(f"[red]Error: Global registry not found at {global_reg_path}[/red]")
40
+ console.print("Run 'uv run sibi-flux dc sync' first to generate the registry.")
41
+ raise typer.Exit(code=1)
42
+
43
+ try:
44
+ with open(global_reg_path, "r") as f:
45
+ global_data = yaml.safe_load(f)
46
+ except Exception as e:
47
+ console.print(f"[red]Error parsing global registry: {e}[/red]")
48
+ raise typer.Exit(code=1)
49
+
50
+ if not global_data:
51
+ console.print("[yellow]Global registry is empty.[/yellow]")
52
+ return
53
+
54
+ # 3. Find Matches
55
+ matches = []
56
+
57
+ # Registry Structure: {conf_obj: {table_name: {class_name: ..., path: ...}}}
58
+ for conf_obj, tables in global_data.items():
59
+ for table_name, meta in tables.items():
60
+ path_str = meta.get("path", "")
61
+ class_name = meta.get("class_name")
62
+
63
+ # Check if domain matches path logic
64
+ # e.g. path="dataobjects/gencubes/istmo360n/..." and db_domain="istmo360n"
65
+ if db_domain in path_str.split("/"):
66
+ # Clean source path for python import
67
+ # "dataobjects/gencubes/..." -> "dataobjects.gencubes...."
68
+ # remove extension .py
69
+ module_path = path_str.replace("/", ".").rstrip(".py")
70
+ source_path = f"{module_path}.{class_name}"
71
+
72
+ # Determine target module name (the last part of the path directory?)
73
+ # e.g. .../biometric/biometric_cubes.py -> "biometric"
74
+ try:
75
+ # heuristic: parent dir name or filename base?
76
+ # path: dataobjects/gencubes/istmo360n/biometric/biometric_cubes.py
77
+ # logical group: biometric
78
+
79
+ p = Path(path_str)
80
+ module_group = p.parent.name # biometric
81
+ if module_group == db_domain:
82
+ # fallback if nested directly under domain
83
+ module_group = p.stem.replace("_cubes", "")
84
+
85
+ except Exception:
86
+ module_group = "common"
87
+
88
+ matches.append({
89
+ "source": source_path,
90
+ "name": f"{app_name.capitalize()}{class_name}", # Default naming: App + Class
91
+ "module": module_group,
92
+ "original_class": class_name
93
+ })
94
+
95
+ if not matches:
96
+ console.print(f"[yellow]No datacubes found for domain '{db_domain}' in registry.[/yellow]")
97
+ return
98
+
99
+ console.print(f"[green]Found {len(matches)} matching datacubes.[/green]")
100
+
101
+ # 4. Update App Registry
102
+
103
+ # Load existing
104
+ current_config = {"cubes": []}
105
+ if registry_path.exists():
106
+ try:
107
+ with open(registry_path, "r") as f:
108
+ loaded = yaml.safe_load(f)
109
+ if loaded and "cubes" in loaded:
110
+ current_config = loaded
111
+ except Exception:
112
+ pass
113
+
114
+ # Deduplicate
115
+ existing_sources = {c.get("source") for c in current_config["cubes"]}
116
+ added_count = 0
117
+
118
+ for m in matches:
119
+ if m["source"] not in existing_sources:
120
+ entry = {
121
+ "source": m["source"],
122
+ "name": m["name"],
123
+ "module": m["module"]
124
+ }
125
+ current_config["cubes"].append(entry)
126
+ existing_sources.add(m["source"])
127
+ added_count += 1
128
+
129
+ # Save
130
+ with open(registry_path, "w") as f:
131
+ yaml.dump(current_config, f, sort_keys=False)
132
+
133
+ console.print(f"[bold green]Added {added_count} new entries to {registry_path}[/bold green]")
134
+ console.print(f"Next: Run [blue]uv run sibi-flux create-cubes {app_name}[/blue] to generate code.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sibi-flux
3
- Version: 2026.1.4
3
+ Version: 2026.1.6
4
4
  Summary: Sibi Toolkit: A collection of tools for Data Analysis/Engineering.
5
5
  Author: Luis Valverde
6
6
  Author-email: Luis Valverde <lvalverdeb@gmail.com>
@@ -52,30 +52,10 @@ Description-Content-Type: text/markdown
52
52
 
53
53
  **SibiFlux** is a production-grade resilient data engineering ecosystem designed to bridge the gap between local development, distributed computing, and agentic AI workflows. It provides a unified engine for hybrid data loading (batch + streaming), self-healing distributed operations, and native interfaces for AI agents via the Model Context Protocol (MCP).
54
54
 
55
- ```mermaid
56
- graph TD
57
- subgraph "Agentic Interface (MCP)"
58
- Agent["AI Agent / Claude"] <--> Router["MCP Router"]
59
- Router <--> Resources["SibiFlux Resources"]
60
- end
61
-
62
- subgraph "Solutions Layer (Business Logic)"
63
- Logistics["Logistics Solutions"]
64
- Enrichment["Enrichment Pipelines"]
65
- Cubes["DataCubes"]
66
- end
67
-
68
- subgraph "SibiFlux Core Engine"
69
- DfHelper["DfHelper (Unified Loader)"]
70
- Cluster["Resilient Dask Cluster"]
71
- Managed["ManagedResource Lifecycle"]
72
- end
73
-
74
- Resources --> Cubes
75
- Logistics --> DfHelper
76
- Cubes --> DfHelper
77
- DfHelper --> Cluster
78
- ```
55
+
56
+
57
+ ## Documentation
58
+ Full documentation is available in [src/docs/index.md](src/docs/index.md).
79
59
 
80
60
  ## Core Architecture
81
61
 
@@ -7,7 +7,7 @@ sibi_flux/artifacts/parquet_engine/__init__.py,sha256=L9OBs-v6HMTZ-_7hrzr7J_oYF-
7
7
  sibi_flux/artifacts/parquet_engine/executor.py,sha256=PZt-xoASMfJjrfC6Dpks9mC4ptJVj2Le28Xl6MKqVU8,6887
8
8
  sibi_flux/artifacts/parquet_engine/manifest.py,sha256=AoeIUFgLPfzvxmchExZIJ_gKP1wnLfYBKWbCYG5XYKk,3324
9
9
  sibi_flux/artifacts/parquet_engine/planner.py,sha256=c_oWfZKMvtpyKH5v-UYlJRRFidRrZR5nW4YquMyRGCs,21640
10
- sibi_flux/cli.py,sha256=W3REv_qKGKNXt2c2iwI-o0jpewdtQJsr6RZASKV4rtk,1794
10
+ sibi_flux/cli.py,sha256=mZazvWBwMnHcvCHtit5cAFNDTukNnPVZHaqg5GRpHxU,2871
11
11
  sibi_flux/config/__init__.py,sha256=fVgxXW03kzHuVo1zXQkLJoXg6Q6yiooRqujii3L817w,64
12
12
  sibi_flux/config/manager.py,sha256=aiGgkwr1maYCwprt_vjdCpfGSycawBCERAjY3jn6xS4,5093
13
13
  sibi_flux/config/settings.py,sha256=F0-Y4Mqwwt5ZOcmFtcPfsXj-tSUZJPzy6LER83nZA20,4560
@@ -23,13 +23,13 @@ sibi_flux/dask_cluster/exceptions.py,sha256=apQZaUMgac8k2ZTTsvUd-VlWdo9-Nrh5b6St
23
23
  sibi_flux/dask_cluster/utils.py,sha256=Pr2qaow6GVyvM0hqKSM0ZQpe2Ot5ayfGYQiNhNpYA8Y,1342
24
24
  sibi_flux/datacube/__init__.py,sha256=ODEoa4r5RtzynIp-PdVDaJ-4BcPBj1L9VkLIF7RYSPE,91
25
25
  sibi_flux/datacube/_data_cube.py,sha256=Ofgy3JlR7N0eKijpUzI-ixFlISUd3CFsxnKd6a4fguE,12629
26
- sibi_flux/datacube/cli.py,sha256=mQwyj-NmjkKwDRXkJOZeXB6VDHGXGvmHnqbuqOCiYfI,92637
26
+ sibi_flux/datacube/cli.py,sha256=0CtmsQCYwFUejILWWTt6s2FZhAKvNE6j1_1yvZfdM5Q,95709
27
27
  sibi_flux/datacube/config_engine.py,sha256=3cmxycCqMzjYJ1fWn8IWrHTNwSQmRqxnPoOzc1lUtDk,8340
28
28
  sibi_flux/datacube/field_factory.py,sha256=Z3Yp6tGzrZ13rvKSjMFr9jvW7fazeNi4K1fAalxLujM,6376
29
29
  sibi_flux/datacube/field_mapper.py,sha256=V6aFYunl28DI7gSvrF7tcidPNX9QtOYymVxbumzQqPs,9334
30
30
  sibi_flux/datacube/field_registry.py,sha256=VBTqxNIn2-eWMiZi1oQK-719oyJfn3yhfV7Bz_0iNyU,4355
31
- sibi_flux/datacube/generator.py,sha256=DECeAbee4nQHDQsrpBYo6FiQERXHQ7XDmOs86abd9mY,36432
32
- sibi_flux/datacube/orchestrator.py,sha256=37iWkqY9ShMkjFqYhpb3dpioYX2GXOTyo5p52cO5t7I,24278
31
+ sibi_flux/datacube/generator.py,sha256=gPGQW114N7jfyGwsSXc-jsJup_C4qSj75oM_CoWuP7Y,36943
32
+ sibi_flux/datacube/orchestrator.py,sha256=ZS2kh1Q6FF1sPtDaTkIvo8Q2S33OrYogjOWDl15-u-k,25254
33
33
  sibi_flux/datacube/router.py,sha256=SdG0J4O9CSZeUkHjyWHFC8B3oH83fSdDHfMXZfmHfi0,10914
34
34
  sibi_flux/dataset/__init__.py,sha256=zYpf4-nalcQNSUIOCDG0CqQu2SKkm89I9AF6Zy6z1sE,53
35
35
  sibi_flux/dataset/_dataset.py,sha256=DDAshH3QjgFDaXp9E75nkKy8_Ft-Z2zP_chmJQTU9hg,6484
@@ -65,7 +65,10 @@ sibi_flux/df_helper/core/_query_config.py,sha256=bS_k0qmvom1uVhkcJngPV2pc9pOhQ0F
65
65
  sibi_flux/df_validator/__init__.py,sha256=lkgTVmeCmH0yqfpZ4oPrRMZS8Jy9lm4OF7DjTSWbgTY,66
66
66
  sibi_flux/df_validator/_df_validator.py,sha256=P8ARf0htKo7L9PMcEjTnobpgT4RsGOiuG81tVEntS4o,8936
67
67
  sibi_flux/init/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
+ sibi_flux/init/app.py,sha256=Ofx_Sowvv6Ri-tDhLYkYllB_e1-4227J-MqhURTAmhI,3678
68
69
  sibi_flux/init/core.py,sha256=ahl4wVAyOrl1ko-f2NcYxeX9yQBQceYnobqeH_Gfp98,7785
70
+ sibi_flux/init/cube_extender.py,sha256=wQPM0zncr97AiGCVM2BYo7dyaLKWIndV-0GvGNXvAMA,5269
71
+ sibi_flux/init/cube_proposer.py,sha256=VA7WF-pDtto9-UdN1IlB7u6TiuxgMbgy79TRooyBPro,5071
69
72
  sibi_flux/init/discovery_updater.py,sha256=3WONU50EnaIItaLncumd3hC6h059ykvoqMfRC5NAvi8,5652
70
73
  sibi_flux/init/env.py,sha256=WMczkJqdaCejSbkjcpW8a44GZdKicdh5GVfUqKYltBI,3792
71
74
  sibi_flux/init/env_engine.py,sha256=HYBik80qgp-_2RgnQY2edaKA30VY1k_3uAZ4TqIID2M,6247
@@ -120,7 +123,7 @@ sibi_flux/utils/file_utils.py,sha256=7OHUW65OTe6HlQ6wkDagDd7d0SCQ_-NEGmHlOJguKYw
120
123
  sibi_flux/utils/filepath_generator/__init__.py,sha256=YVFJhIewjwksb9E2t43ojNC-W_AqUDhkKxQVBIBMkY8,91
121
124
  sibi_flux/utils/filepath_generator/_filepath_generator.py,sha256=4HG-Ubvjtv6luL0z-A-8B6_r3o9YqBwATFXhOXiTbKc,6789
122
125
  sibi_flux/utils/retry.py,sha256=45t0MF2IoMayN9xkn5_FtakMq4HwZlGvHVd6qv8x1AY,1227
123
- sibi_flux-2026.1.4.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
124
- sibi_flux-2026.1.4.dist-info/entry_points.txt,sha256=6xrq5zuz_8wodJj4s49raopnuC3Owy_leZRkWtcXpTk,49
125
- sibi_flux-2026.1.4.dist-info/METADATA,sha256=njzorcmcig-EDd4i0-oH-nQ1dfi01ZvDY6xwN6N03B8,10422
126
- sibi_flux-2026.1.4.dist-info/RECORD,,
126
+ sibi_flux-2026.1.6.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
127
+ sibi_flux-2026.1.6.dist-info/entry_points.txt,sha256=6xrq5zuz_8wodJj4s49raopnuC3Owy_leZRkWtcXpTk,49
128
+ sibi_flux-2026.1.6.dist-info/METADATA,sha256=al8yanEMaHe5u-iRLzp5wOjWwa3CMaFXtLM97-rq1NQ,9886
129
+ sibi_flux-2026.1.6.dist-info/RECORD,,