synapseForge 0.1.24.dev2__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.
- pipeline/__init__.py +1 -0
- pipeline/init/__init__.py +7 -0
- pipeline/init/__main__.py +10 -0
- pipeline/init/config_handler.py +35 -0
- pipeline/init/input_handler.py +124 -0
- pipeline/init/logo_handler.py +33 -0
- pipeline/init/main.py +151 -0
- pipeline/init/placeholder_handler.py +162 -0
- pipeline/init/template_handler.py +89 -0
- pipeline/init/venv_handler.py +68 -0
- pipeline/launch/__init__.py +1 -0
- pipeline/launch/forge.py +701 -0
- pipeline/launch/templates/launcher.py +79 -0
- pipeline/template.zip +0 -0
- synapseforge/__init__.py +3 -0
- synapseforge/__main__.py +6 -0
- synapseforge/cli/__init__.py +1 -0
- synapseforge/cli/main.py +390 -0
- synapseforge/tk/__init__.py +1 -0
- synapseforge/tk/colors_app.py +305 -0
- synapseforge/tk/init_app.py +447 -0
- synapseforge/tk/logo.ico +0 -0
- synapseforge/tk/logo.png +0 -0
- synapseforge-0.1.24.dev2.dist-info/METADATA +187 -0
- synapseforge-0.1.24.dev2.dist-info/RECORD +29 -0
- synapseforge-0.1.24.dev2.dist-info/WHEEL +5 -0
- synapseforge-0.1.24.dev2.dist-info/entry_points.txt +2 -0
- synapseforge-0.1.24.dev2.dist-info/licenses/LICENSE +201 -0
- synapseforge-0.1.24.dev2.dist-info/top_level.txt +2 -0
pipeline/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""synapseForge — scaffolding pipeline and distribution builder."""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Step 5 — Save the user-provided config as ``config/replace.json`` and ``frontend/public/colors.json``."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
CONFIGURABLE_COLOR_KEYS = ("primary", "secondary", "primary_text", "gradient_secondary", "usar_gradiente")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def save_config(target: Path, config: dict) -> None:
|
|
11
|
+
"""Write the user config to ``{target}/config/replace.json`` and colors to ``frontend/public/colors.json``.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
target: Project root directory.
|
|
15
|
+
config: Validated user config dictionary.
|
|
16
|
+
"""
|
|
17
|
+
config_dir = target / "config"
|
|
18
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
|
|
20
|
+
dest = config_dir / "replace.json"
|
|
21
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
22
|
+
json.dump(config, f, indent=2, ensure_ascii=False)
|
|
23
|
+
|
|
24
|
+
print(f" Saved: {dest}")
|
|
25
|
+
|
|
26
|
+
# Also generate colors.json for runtime in frontend/public/
|
|
27
|
+
colors = config.get("colors", {})
|
|
28
|
+
runtime_colors = {k: colors.get(k) for k in CONFIGURABLE_COLOR_KEYS if colors.get(k)}
|
|
29
|
+
if runtime_colors:
|
|
30
|
+
public_dir = target / "frontend" / "public"
|
|
31
|
+
public_dir.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
colors_dest = public_dir / "colors.json"
|
|
33
|
+
with open(colors_dest, "w", encoding="utf-8") as f:
|
|
34
|
+
json.dump(runtime_colors, f, indent=2, ensure_ascii=False)
|
|
35
|
+
print(f" Saved: {colors_dest}")
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Step 1 — Interactive user input for all replace.json fields."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_user_input() -> Dict[str, object]:
|
|
10
|
+
"""Prompt the user for every field and return a validated config dict.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
A dictionary with the same structure as ``config/replace.json``.
|
|
14
|
+
"""
|
|
15
|
+
print("\nIngresá los datos del proyecto (dejá vacío para omitir):\n")
|
|
16
|
+
|
|
17
|
+
# ── Logo ────────────────────────────────────────────────────────────
|
|
18
|
+
logo_path: Optional[str] = _prompt("Logo de la empresa (para README, sin comillas)", required=True)
|
|
19
|
+
logo_resolved = _resolve_logo(logo_path)
|
|
20
|
+
|
|
21
|
+
# ── Project info ────────────────────────────────────────────────────
|
|
22
|
+
empresa: str = _prompt("Nombre de la empresa desarrolladora", required=True)
|
|
23
|
+
owner: str = _prompt("Owner del repo (usuario de GitHub)", required=True)
|
|
24
|
+
legal: str = _prompt("Nombre legal / razón social", required=True)
|
|
25
|
+
repo: str = _prompt("Nombre del repo", required=True)
|
|
26
|
+
cliente: str = _prompt("Nombre del cliente", required=True)
|
|
27
|
+
logo_cliente: str = _prompt("Logo del cliente (para la app, opcional, sin comillas)", default="")
|
|
28
|
+
descripcion: str = _prompt("Descripción del proyecto", required=True)
|
|
29
|
+
tarea: str = _prompt("Nombre de la tarea / rubro", required=True)
|
|
30
|
+
|
|
31
|
+
# ── Colores (obligatorios) ──────────────────────────────────────────
|
|
32
|
+
print("\nColores del proyecto:")
|
|
33
|
+
print(" Primary: color principal (botones, headers, burbujas)")
|
|
34
|
+
print(" Secondary: color secundario (hover, detalles light)")
|
|
35
|
+
print(" Primary Text: color de texto (botones, headers)")
|
|
36
|
+
print(" Gradient Secondary: color secundario del gradiente (igual al primary si no usás gradiente)\n")
|
|
37
|
+
|
|
38
|
+
colors: Dict[str, object] = {}
|
|
39
|
+
|
|
40
|
+
c = _prompt_hex("Color principal (ej: #D76F10)", required=True)
|
|
41
|
+
colors["primary"] = c
|
|
42
|
+
|
|
43
|
+
c = _prompt_hex("Color secundario (ej: #F0A347)", required=True)
|
|
44
|
+
colors["secondary"] = c
|
|
45
|
+
|
|
46
|
+
c = _prompt_hex("Color de texto (ej: #FFFFFF)", required=True)
|
|
47
|
+
colors["primary_text"] = c
|
|
48
|
+
|
|
49
|
+
# ── Gradient toggle ────────────────────────────────────────────────
|
|
50
|
+
usar = _prompt("¿Usar gradiente en botones/headers? (s/N)", default="n")
|
|
51
|
+
colors["usar_gradiente"] = usar.lower() in ("s", "si", "y", "yes", "1", "true")
|
|
52
|
+
if colors["usar_gradiente"]:
|
|
53
|
+
c = _prompt_hex("Color secundario del gradiente (ej: #F0A347)", required=True)
|
|
54
|
+
colors["gradient_secondary"] = c
|
|
55
|
+
else:
|
|
56
|
+
colors["gradient_secondary"] = colors["primary"]
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"logo": {
|
|
60
|
+
"path": str(logo_resolved),
|
|
61
|
+
},
|
|
62
|
+
"empresa": empresa,
|
|
63
|
+
"owner": owner,
|
|
64
|
+
"legal": legal,
|
|
65
|
+
"repo": repo,
|
|
66
|
+
"cliente": cliente,
|
|
67
|
+
"logo_cliente": logo_cliente or None,
|
|
68
|
+
"descripcion": descripcion,
|
|
69
|
+
"tarea": tarea,
|
|
70
|
+
"colors": colors,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Prompt helpers
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
_HEX_RE = re.compile(r"^#[0-9a-fA-F]{6}$")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _prompt(label: str, *, required: bool = False, default: str = "") -> str:
|
|
82
|
+
"""Ask for a text value. Loop until non-empty if *required*."""
|
|
83
|
+
hint = f" [{default}]" if default else ""
|
|
84
|
+
suffix = " *" if required else ""
|
|
85
|
+
while True:
|
|
86
|
+
value = input(f" {label}{suffix}{hint}: ").strip()
|
|
87
|
+
if not value:
|
|
88
|
+
value = default
|
|
89
|
+
if value:
|
|
90
|
+
return value
|
|
91
|
+
if not required:
|
|
92
|
+
return ""
|
|
93
|
+
print(" ⚠ Este campo es obligatorio.")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _prompt_hex(label: str, *, required: bool = False) -> str:
|
|
97
|
+
"""Ask for a hex color. Return hex string if provided.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
label: Prompt label.
|
|
101
|
+
required: When True, loop until a valid hex is entered.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
The hex color string, or ``""`` if not required and skipped.
|
|
105
|
+
"""
|
|
106
|
+
while True:
|
|
107
|
+
value = input(f" {label}: ").strip()
|
|
108
|
+
if not value and not required:
|
|
109
|
+
return ""
|
|
110
|
+
if not value:
|
|
111
|
+
print(" ⚠ Este campo es obligatorio.")
|
|
112
|
+
continue
|
|
113
|
+
if _HEX_RE.match(value):
|
|
114
|
+
return value
|
|
115
|
+
print(" ⚠ Formato inválido. Usá #RRGGBB (ej: #D76F10).")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _resolve_logo(raw: str) -> Path:
|
|
119
|
+
"""Resolve and validate the logo path."""
|
|
120
|
+
p = Path(raw).resolve()
|
|
121
|
+
if not p.is_file():
|
|
122
|
+
print(f" ⚠ Archivo no encontrado: {p}")
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
return p
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Steps 6-8 — Copy logo, generate .ico, extract colors."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def handle_logo(config: dict, logo_dest: Path, config_key: str = "logo.path") -> None:
|
|
9
|
+
"""Copy the user's logo to the template assets directory.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
config: User config dictionary.
|
|
13
|
+
logo_dest: Destination path for the logo.
|
|
14
|
+
config_key: Dotted config key to read the source from
|
|
15
|
+
(``logo.path`` for company, ``logo_cliente`` for client).
|
|
16
|
+
"""
|
|
17
|
+
if config_key == "logo_cliente":
|
|
18
|
+
logo_src_raw: Optional[str] = config.get("logo_cliente")
|
|
19
|
+
else:
|
|
20
|
+
logo_src_raw = config.get("logo", {}).get("path")
|
|
21
|
+
|
|
22
|
+
if not logo_src_raw:
|
|
23
|
+
print(f" WARNING: no logo path in config, skipping")
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
logo_src = Path(logo_src_raw).resolve()
|
|
27
|
+
if not logo_src.is_file():
|
|
28
|
+
print(f" WARNING: logo source not found: {logo_src}, skipping")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
logo_dest.parent.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
shutil.copy2(str(logo_src), str(logo_dest))
|
|
33
|
+
print(f" Copied: {logo_src} → {logo_dest}")
|
pipeline/init/main.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Orchestrates the full init pipeline (steps 1-10)."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .input_handler import get_user_input
|
|
9
|
+
from .template_handler import extract_template
|
|
10
|
+
from .venv_handler import setup_venv, install_requirements
|
|
11
|
+
from .config_handler import save_config
|
|
12
|
+
from .logo_handler import handle_logo
|
|
13
|
+
from .placeholder_handler import replace_all_placeholders
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run(target_dir: str, config: dict | None = None) -> None:
|
|
17
|
+
"""Execute the full init pipeline inside *target_dir*.
|
|
18
|
+
|
|
19
|
+
When *config* is provided the pipeline uses it directly (GUI mode)
|
|
20
|
+
and skips the interactive ``get_user_input()`` prompt.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
target_dir: Absolute or relative path to the (empty) project directory.
|
|
24
|
+
config: Pre-collected configuration dict (from GUI). When ``None``
|
|
25
|
+
the pipeline prompts via terminal.
|
|
26
|
+
"""
|
|
27
|
+
target = Path(target_dir).resolve()
|
|
28
|
+
|
|
29
|
+
if not target.is_dir():
|
|
30
|
+
print(f" Creating directory: {target}")
|
|
31
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
print("=" * 60)
|
|
34
|
+
print(" synapseForge — Init Pipeline")
|
|
35
|
+
print("=" * 60)
|
|
36
|
+
|
|
37
|
+
# Step 1: User input
|
|
38
|
+
if config is None:
|
|
39
|
+
print("\n[1/10] User input ...")
|
|
40
|
+
config = get_user_input()
|
|
41
|
+
else:
|
|
42
|
+
print("\n[1/10] Using provided configuration ...")
|
|
43
|
+
|
|
44
|
+
# Step 2: Download & extract template
|
|
45
|
+
print("\n[2/10] Downloading & extracting template ...")
|
|
46
|
+
extract_template(target)
|
|
47
|
+
|
|
48
|
+
# Step 3: Create virtual environment
|
|
49
|
+
print("\n[3/10] Creating virtual environment ...")
|
|
50
|
+
venv_path = setup_venv(target, config["repo"])
|
|
51
|
+
|
|
52
|
+
# Step 4: Install Python requirements
|
|
53
|
+
print("\n[4/10] Installing Python requirements ...")
|
|
54
|
+
install_requirements(venv_path, target)
|
|
55
|
+
|
|
56
|
+
# Step 5: npm install
|
|
57
|
+
print("\n[5/10] Installing npm dependencies ...")
|
|
58
|
+
_run_npm_install(target)
|
|
59
|
+
|
|
60
|
+
# Step 6: Copy logos
|
|
61
|
+
print("\n[6/10] Copying logos ...")
|
|
62
|
+
company_logo_dest = target / "frontend" / "src" / "assets" / "logo_empresa.png"
|
|
63
|
+
handle_logo(config, company_logo_dest, config_key="logo.path")
|
|
64
|
+
# Also copy to root/src for backward compatibility with template references
|
|
65
|
+
company_logo_root = target / "src" / "logo_empresa.png"
|
|
66
|
+
handle_logo(config, company_logo_root, config_key="logo.path")
|
|
67
|
+
client_logo_dest = target / "frontend" / "src" / "assets" / "logo_cliente.png"
|
|
68
|
+
handle_logo(config, client_logo_dest, config_key="logo_cliente")
|
|
69
|
+
|
|
70
|
+
# Step 7: Generate .ico from client logo
|
|
71
|
+
print("\n[7/10] Generating favicon (.ico) ...")
|
|
72
|
+
_run_generate_ico(venv_path, client_logo_dest)
|
|
73
|
+
|
|
74
|
+
# Step 8: Extract colors from client logo
|
|
75
|
+
print("\n[8/10] Resolving colors ...")
|
|
76
|
+
_resolve_colors(config, client_logo_dest)
|
|
77
|
+
|
|
78
|
+
# Step 9: Save user config (colors already in dict)
|
|
79
|
+
print("\n[9/10] Saving configuration ...")
|
|
80
|
+
save_config(target, config)
|
|
81
|
+
|
|
82
|
+
# Step 10: Replace placeholders
|
|
83
|
+
print("\n[10/10] Replacing placeholders ...")
|
|
84
|
+
replace_all_placeholders(target, config)
|
|
85
|
+
|
|
86
|
+
print("\n" + "=" * 60)
|
|
87
|
+
print(" Done! Project initialized in:", target)
|
|
88
|
+
print("=" * 60)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# Internal helpers
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _run_npm_install(target: Path) -> None:
|
|
97
|
+
"""Run ``npm install`` in the frontend directory."""
|
|
98
|
+
frontend_dir = target / "frontend"
|
|
99
|
+
if not (frontend_dir / "package.json").is_file():
|
|
100
|
+
print(" WARNING: package.json not found, skipping npm install")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
subprocess.run(
|
|
105
|
+
"npm install",
|
|
106
|
+
cwd=str(frontend_dir),
|
|
107
|
+
shell=True,
|
|
108
|
+
check=True,
|
|
109
|
+
)
|
|
110
|
+
print(" npm install completed")
|
|
111
|
+
except subprocess.CalledProcessError as exc:
|
|
112
|
+
print(f" WARNING: npm install failed (exit code {exc.returncode})")
|
|
113
|
+
except FileNotFoundError:
|
|
114
|
+
print(" WARNING: npm not found, skipping npm install")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _run_generate_ico(venv_path: Path, logo_png: Path) -> None:
|
|
118
|
+
"""Generate ``.ico`` from the logo PNG using Pillow (inline)."""
|
|
119
|
+
if not logo_png.is_file():
|
|
120
|
+
print(" WARNING: logo PNG not found, skipping .ico generation")
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
from PIL import Image
|
|
125
|
+
except ImportError:
|
|
126
|
+
print(" WARNING: Pillow not available, skipping .ico generation")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
ico_path = logo_png.with_suffix(".ico")
|
|
130
|
+
try:
|
|
131
|
+
img = Image.open(str(logo_png))
|
|
132
|
+
sizes = [(16, 16), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)]
|
|
133
|
+
img.save(str(ico_path), format="ICO", sizes=sizes)
|
|
134
|
+
print(f" Generated: {ico_path}")
|
|
135
|
+
except Exception as exc:
|
|
136
|
+
print(f" WARNING: .ico generation failed: {exc}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _resolve_colors(config: dict, logo_png: Path) -> None:
|
|
140
|
+
"""Validate that all 4 color fields exist in config."""
|
|
141
|
+
keys = ["primary", "secondary", "primary_text", "gradient_secondary"]
|
|
142
|
+
colors = config.setdefault("colors", {})
|
|
143
|
+
missing = [k for k in keys if not colors.get(k)]
|
|
144
|
+
if missing:
|
|
145
|
+
print(f" WARNING: missing colors in config: {missing}")
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
for k in keys:
|
|
149
|
+
print(f" {k}: {colors[k]}")
|
|
150
|
+
usar = colors.get("usar_gradiente", True)
|
|
151
|
+
print(f" gradient: {'ON' if usar else 'OFF'}")
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Step 9 — Replace every XML placeholder tag in the extracted template.
|
|
2
|
+
|
|
3
|
+
Scans all text files under the target directory and replaces occurrences
|
|
4
|
+
of ``<tag>value</tag>`` with the actual value from the user config.
|
|
5
|
+
|
|
6
|
+
Supports nested tags such as ``<descripcion>...</descripcion>``,
|
|
7
|
+
``<cliente>...</cliente>``, ``<color_primario>...</color_primario>``, etc.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, Pattern
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Tag → config key mapping
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
TAG_MAP: Dict[str, str] = {
|
|
18
|
+
"logo": "logo.path",
|
|
19
|
+
"empresa": "empresa",
|
|
20
|
+
"owner": "owner",
|
|
21
|
+
"legal": "legal",
|
|
22
|
+
"repo": "repo",
|
|
23
|
+
"cliente": "cliente",
|
|
24
|
+
"logo_cliente": "logo_cliente",
|
|
25
|
+
"descripcion": "descripcion",
|
|
26
|
+
"tarea": "tarea",
|
|
27
|
+
# Colors — 4 configurables + gradient toggle
|
|
28
|
+
"color_primario": "colors.primary",
|
|
29
|
+
"color_secundario": "colors.secondary",
|
|
30
|
+
"color_texto_primario": "colors.primary_text",
|
|
31
|
+
"color_secundario_gradiente": "colors.gradient_secondary",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Regex that matches <tag>anything</tag>. Tag names must be [a-z_]+.
|
|
35
|
+
_TAG_RE: Pattern = re.compile(r"<([a-z_]+)>([^<]*)</\1>")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def replace_all_placeholders(target: Path, config: dict) -> None:
|
|
39
|
+
"""Walk the entire *target* tree and replace XML placeholders.
|
|
40
|
+
|
|
41
|
+
Only processes text-like files (extensions: .py, .tsx, .ts, .js, .jsx,
|
|
42
|
+
.html, .css, .md, .json, .txt, .yaml, .yml, .toml, .ini, .cfg, .env).
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
target: Project root directory.
|
|
46
|
+
config: User config dictionary.
|
|
47
|
+
"""
|
|
48
|
+
replacements = _build_replacement_map(config)
|
|
49
|
+
|
|
50
|
+
# Track stats for final summary
|
|
51
|
+
modified_files = 0
|
|
52
|
+
total_replacements = 0
|
|
53
|
+
|
|
54
|
+
for file_path in target.rglob("*"):
|
|
55
|
+
if not file_path.is_file():
|
|
56
|
+
continue
|
|
57
|
+
if not _is_text_file(file_path):
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
# Skip node_modules, .git, __pycache__, .venv
|
|
61
|
+
if _should_skip(file_path):
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
original = file_path.read_bytes()
|
|
66
|
+
except Exception:
|
|
67
|
+
continue # skip binary or permission-denied
|
|
68
|
+
|
|
69
|
+
text = original.decode("utf-8", errors="replace")
|
|
70
|
+
|
|
71
|
+
new_text, count = _replace_tags(text, replacements)
|
|
72
|
+
|
|
73
|
+
if count > 0 and new_text != text:
|
|
74
|
+
file_path.write_bytes(new_text.encode("utf-8"))
|
|
75
|
+
modified_files += 1
|
|
76
|
+
total_replacements += count
|
|
77
|
+
|
|
78
|
+
print(f" Replaced {total_replacements} placeholder(s) across {modified_files} file(s)")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Internal helpers
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
def _build_replacement_map(config: dict) -> Dict[str, str]:
|
|
86
|
+
"""Flatten the nested config into ``{tag: value}``."""
|
|
87
|
+
replacements: Dict[str, str] = {}
|
|
88
|
+
|
|
89
|
+
for tag, key_path in TAG_MAP.items():
|
|
90
|
+
if tag == "logo":
|
|
91
|
+
# Path relativo al repo desde la raíz (el logo se copia a ambos lugares)
|
|
92
|
+
replacements[tag] = "src/logo_empresa.png"
|
|
93
|
+
else:
|
|
94
|
+
value = _deep_get(config, key_path)
|
|
95
|
+
if value is not None and value != "":
|
|
96
|
+
replacements[tag] = str(value)
|
|
97
|
+
|
|
98
|
+
return replacements
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _deep_get(d: dict, dotted: str):
|
|
102
|
+
"""Traverse a dict with a dotted path like ``colors.primary``."""
|
|
103
|
+
parts = dotted.split(".")
|
|
104
|
+
current: object = d
|
|
105
|
+
for part in parts:
|
|
106
|
+
if isinstance(current, dict):
|
|
107
|
+
current = current.get(part)
|
|
108
|
+
else:
|
|
109
|
+
return None
|
|
110
|
+
return current
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _replace_tags(text: str, replacements: Dict[str, str]) -> tuple:
|
|
114
|
+
"""Replace all ``<tag>...</tag>`` spans in *text* with the mapped value.
|
|
115
|
+
|
|
116
|
+
Returns ``(new_text, count)``.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def _replacer(m: re.Match) -> str:
|
|
120
|
+
tag = m.group(1)
|
|
121
|
+
if tag in replacements:
|
|
122
|
+
return replacements[tag]
|
|
123
|
+
# Unrecognised tag → leave untouched
|
|
124
|
+
return m.group(0)
|
|
125
|
+
|
|
126
|
+
new_text, count = _TAG_RE.subn(_replacer, text)
|
|
127
|
+
return new_text, count
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
_TEXT_EXTENSIONS = frozenset({
|
|
131
|
+
".py", ".tsx", ".ts", ".js", ".jsx",
|
|
132
|
+
".html", ".css", ".md", ".json", ".txt",
|
|
133
|
+
".yaml", ".yml", ".toml", ".ini", ".cfg",
|
|
134
|
+
".env", ".env.example", ".gitignore",
|
|
135
|
+
".ps1", ".bat", ".cmd", ".sh",
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
_NOEXT_NAMES = frozenset({
|
|
139
|
+
"LICENSE", "CHANGELOG", "CONTRIBUTORS",
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
_SKIP_DIRS = frozenset({
|
|
143
|
+
"node_modules", ".git", "__pycache__", ".venv",
|
|
144
|
+
".synapseForge", ".vite", ".vite-temp",
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _is_text_file(path: Path) -> bool:
|
|
149
|
+
"""Check extension against known text types."""
|
|
150
|
+
return (
|
|
151
|
+
path.suffix in _TEXT_EXTENSIONS
|
|
152
|
+
or path.name == ".gitignore"
|
|
153
|
+
or path.name in _NOEXT_NAMES
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _should_skip(path: Path) -> bool:
|
|
158
|
+
"""Return True if the path is inside a directory that should be skipped."""
|
|
159
|
+
for parent in path.parents:
|
|
160
|
+
if parent.name in _SKIP_DIRS:
|
|
161
|
+
return True
|
|
162
|
+
return False
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Step 2 — Locate bundled (or download) template.zip and extract into target."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import urllib.request
|
|
5
|
+
import zipfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from importlib.resources import files as resources_files
|
|
8
|
+
|
|
9
|
+
# ---------------------------------------------------------------------------
|
|
10
|
+
# URL used as fallback when the bundled zip is not available.
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
TEMPLATE_URL = (
|
|
13
|
+
"https://github.com/synapse-ai-hub/synapseForge/raw/main/pipeline/template.zip"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def extract_template(target: Path) -> None:
|
|
18
|
+
"""Extract the template zip into *target*.
|
|
19
|
+
|
|
20
|
+
Looks for a bundled copy first (from pip install or development mode),
|
|
21
|
+
otherwise downloads from the GitHub raw URL.
|
|
22
|
+
|
|
23
|
+
The bundled zip inside the installed package is **never** deleted so
|
|
24
|
+
that every invocation stays offline-capable.
|
|
25
|
+
"""
|
|
26
|
+
bundled = _get_bundled_zip()
|
|
27
|
+
was_downloaded = False
|
|
28
|
+
|
|
29
|
+
if bundled is not None and bundled.is_file():
|
|
30
|
+
zip_path = bundled
|
|
31
|
+
print(f" Using bundled template: {zip_path}")
|
|
32
|
+
else:
|
|
33
|
+
zip_path = _download_template(target)
|
|
34
|
+
if zip_path is None:
|
|
35
|
+
print(" ERROR: could not obtain template.zip")
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
was_downloaded = True
|
|
38
|
+
|
|
39
|
+
_safe_extract(zip_path, target)
|
|
40
|
+
|
|
41
|
+
# Only delete the zip if it was the downloaded copy —
|
|
42
|
+
# never touch the bundled copy inside site-packages.
|
|
43
|
+
if was_downloaded:
|
|
44
|
+
zip_path.unlink(missing_ok=True)
|
|
45
|
+
|
|
46
|
+
print(f" Extracted to: {target}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Internal helpers
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_bundled_zip() -> Path | None:
|
|
55
|
+
"""Locate the ``template.zip`` shipped inside the ``pipeline`` package.
|
|
56
|
+
|
|
57
|
+
Works both in development mode (``pip install -e .``) and when installed
|
|
58
|
+
from PyPI — the file lives in the ``pipeline`` directory of the package.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
return Path(resources_files("pipeline").joinpath("template.zip")) # type: ignore[arg-type]
|
|
62
|
+
except (ModuleNotFoundError, TypeError):
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _download_template(target: Path) -> Path | None:
|
|
67
|
+
"""Download template.zip from GitHub into *target*."""
|
|
68
|
+
try:
|
|
69
|
+
dest = target / "template.zip"
|
|
70
|
+
print(" Downloading template from GitHub …")
|
|
71
|
+
urllib.request.urlretrieve(TEMPLATE_URL, dest)
|
|
72
|
+
print(f" Downloaded: {dest}")
|
|
73
|
+
return dest
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
print(f" Download failed: {exc}")
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _safe_extract(zip_path: Path, target: Path) -> None:
|
|
80
|
+
"""Extract zip, overwriting existing files silently."""
|
|
81
|
+
try:
|
|
82
|
+
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
83
|
+
zf.extractall(path=target)
|
|
84
|
+
except zipfile.BadZipFile:
|
|
85
|
+
print(f" ERROR: corrupted zip file: {zip_path}")
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
print(f" ERROR extracting zip: {exc}")
|
|
89
|
+
sys.exit(1)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Steps 3-4 — Create virtual environment and install dependencies."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def setup_venv(target: Path, repo_name: str) -> Path:
|
|
9
|
+
"""Create a Python virtual environment at ``{target}/.{repo_name}``.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
target: Project root directory.
|
|
13
|
+
repo_name: Used as the venv folder name (``.{{repo_name}}``).
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Path to the venv root.
|
|
17
|
+
"""
|
|
18
|
+
venv_dir = target / f".{repo_name}"
|
|
19
|
+
if venv_dir.is_dir():
|
|
20
|
+
print(f" Virtual env already exists: {venv_dir}")
|
|
21
|
+
return venv_dir
|
|
22
|
+
|
|
23
|
+
result = subprocess.run(
|
|
24
|
+
[sys.executable, "-m", "venv", str(venv_dir)],
|
|
25
|
+
capture_output=True,
|
|
26
|
+
text=True,
|
|
27
|
+
)
|
|
28
|
+
if result.returncode != 0:
|
|
29
|
+
print(f" ERROR creating venv: {result.stderr.strip()}")
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
|
|
32
|
+
print(f" Created: {venv_dir}")
|
|
33
|
+
return venv_dir
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def install_requirements(venv_path: Path, target: Path) -> None:
|
|
37
|
+
"""Run ``pip install -r requirements.txt`` inside the venv.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
venv_path: Path to the virtual environment root.
|
|
41
|
+
target: Project root (where ``requirements.txt`` lives).
|
|
42
|
+
"""
|
|
43
|
+
req_file = target / "requirements.txt"
|
|
44
|
+
if not req_file.is_file():
|
|
45
|
+
print(" WARNING: requirements.txt not found, skipping install")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
python = _venv_python(venv_path)
|
|
49
|
+
result = subprocess.run(
|
|
50
|
+
[str(python), "-m", "pip", "install", "-r", str(req_file)],
|
|
51
|
+
capture_output=True,
|
|
52
|
+
text=True,
|
|
53
|
+
)
|
|
54
|
+
if result.returncode != 0:
|
|
55
|
+
print(f" WARNING: pip install failed:\n{result.stderr.strip()}")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
# Print last line of pip output (usually "Successfully installed ...")
|
|
59
|
+
last_line = result.stdout.strip().splitlines()[-1] if result.stdout.strip() else ""
|
|
60
|
+
if last_line:
|
|
61
|
+
print(f" {last_line}")
|
|
62
|
+
else:
|
|
63
|
+
print(" Requirements installed.")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _venv_python(venv_path: Path) -> Path:
|
|
67
|
+
"""Return the path to the venv's Python executable."""
|
|
68
|
+
return venv_path / "Scripts" / "python.exe"
|