viperx 0.9.14__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.
- viperx/__init__.py +2 -0
- viperx/config_engine.py +141 -0
- viperx/constants.py +35 -0
- viperx/core.py +416 -0
- viperx/licenses.py +248 -0
- viperx/main.py +278 -0
- viperx/templates/Base.ipynb.j2 +119 -0
- viperx/templates/Base_General.ipynb.j2 +119 -0
- viperx/templates/Base_Kaggle.ipynb.j2 +114 -0
- viperx/templates/README.md.j2 +122 -0
- viperx/templates/__init__.py.j2 +8 -0
- viperx/templates/config.py.j2 +40 -0
- viperx/templates/config.yaml.j2 +14 -0
- viperx/templates/data_loader.py.j2 +112 -0
- viperx/templates/main.py.j2 +13 -0
- viperx/templates/pyproject.toml.j2 +59 -0
- viperx/templates/viperx_config.yaml.j2 +45 -0
- viperx/utils.py +47 -0
- viperx-0.9.14.dist-info/METADATA +236 -0
- viperx-0.9.14.dist-info/RECORD +22 -0
- viperx-0.9.14.dist-info/WHEEL +4 -0
- viperx-0.9.14.dist-info/entry_points.txt +3 -0
viperx/__init__.py
ADDED
viperx/config_engine.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
|
|
6
|
+
from viperx.core import ProjectGenerator
|
|
7
|
+
from viperx.constants import DEFAULT_LICENSE, DEFAULT_BUILDER, TYPE_CLASSIC, FRAMEWORK_PYTORCH
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
class ConfigEngine:
|
|
12
|
+
"""
|
|
13
|
+
Orchestrates project creation and updates based on a declarative YAML config.
|
|
14
|
+
Implements the 'Infrastructure as Code' pattern for ViperX.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, config_path: Path, verbose: bool = False):
|
|
18
|
+
self.config_path = config_path
|
|
19
|
+
self.verbose = verbose
|
|
20
|
+
self.config = self._load_config()
|
|
21
|
+
self.root_path = Path.cwd()
|
|
22
|
+
|
|
23
|
+
def _load_config(self) -> dict:
|
|
24
|
+
"""Load and validate the YAML configuration."""
|
|
25
|
+
if not self.config_path.exists():
|
|
26
|
+
console.print(f"[bold red]Error:[/bold red] Config file not found at {self.config_path}")
|
|
27
|
+
raise FileNotFoundError(f"Config file not found: {self.config_path}")
|
|
28
|
+
|
|
29
|
+
with open(self.config_path, "r") as f:
|
|
30
|
+
try:
|
|
31
|
+
data = yaml.safe_load(f)
|
|
32
|
+
except yaml.YAMLError as e:
|
|
33
|
+
console.print(f"[bold red]Error:[/bold red] Invalid YAML format: {e}")
|
|
34
|
+
raise ValueError("Invalid YAML")
|
|
35
|
+
|
|
36
|
+
# Basic Validation
|
|
37
|
+
if "project" not in data or "name" not in data["project"]:
|
|
38
|
+
console.print("[bold red]Error:[/bold red] Config must contain 'project.name'")
|
|
39
|
+
raise ValueError("Missing project.name")
|
|
40
|
+
|
|
41
|
+
return data
|
|
42
|
+
|
|
43
|
+
def apply(self):
|
|
44
|
+
"""Apply the configuration to the current directory."""
|
|
45
|
+
project_conf = self.config.get("project", {})
|
|
46
|
+
settings_conf = self.config.get("settings", {})
|
|
47
|
+
workspace_conf = self.config.get("workspace", {})
|
|
48
|
+
|
|
49
|
+
project_name = project_conf.get("name")
|
|
50
|
+
target_dir = self.root_path / project_name
|
|
51
|
+
|
|
52
|
+
# 1. Root Project Handling
|
|
53
|
+
# If we are NOT already in the project dir (checking name), we might need to create it
|
|
54
|
+
# Or if we are running in the root of where `viperx init` is called.
|
|
55
|
+
|
|
56
|
+
# Heuristic: Are we already in a folder named 'project_name'?
|
|
57
|
+
if self.root_path.name == project_name:
|
|
58
|
+
# We are inside the project folder
|
|
59
|
+
if not (self.root_path / "pyproject.toml").exists():
|
|
60
|
+
console.print(Panel(f"⚠️ [bold yellow]Current directory matches name but is not initialized. Hydrating:[/bold yellow] {project_name}", border_style="yellow"))
|
|
61
|
+
gen = ProjectGenerator(
|
|
62
|
+
name=project_name,
|
|
63
|
+
description=project_conf.get("description", ""),
|
|
64
|
+
type=settings_conf.get("type", TYPE_CLASSIC),
|
|
65
|
+
author=project_conf.get("author", None),
|
|
66
|
+
license=project_conf.get("license", DEFAULT_LICENSE),
|
|
67
|
+
builder=project_conf.get("builder", DEFAULT_BUILDER),
|
|
68
|
+
use_env=settings_conf.get("use_env", False),
|
|
69
|
+
use_config=settings_conf.get("use_config", True),
|
|
70
|
+
use_tests=settings_conf.get("use_tests", True),
|
|
71
|
+
framework=settings_conf.get("framework", FRAMEWORK_PYTORCH),
|
|
72
|
+
verbose=self.verbose
|
|
73
|
+
)
|
|
74
|
+
# generate() expects parent dir, and will operate on parent/name (which is self.root_path)
|
|
75
|
+
gen.generate(self.root_path.parent)
|
|
76
|
+
else:
|
|
77
|
+
console.print(Panel(f"♻️ [bold blue]Syncing Project:[/bold blue] {project_name}", border_style="blue"))
|
|
78
|
+
current_root = self.root_path
|
|
79
|
+
else:
|
|
80
|
+
# We are outside, check if it exists
|
|
81
|
+
if target_dir.exists() and (target_dir / "pyproject.toml").exists():
|
|
82
|
+
console.print(Panel(f"♻️ [bold blue]Updating Existing Project:[/bold blue] {project_name}", border_style="blue"))
|
|
83
|
+
current_root = target_dir
|
|
84
|
+
else:
|
|
85
|
+
if target_dir.exists():
|
|
86
|
+
console.print(Panel(f"⚠️ [bold yellow]Directory exists but not initialized. Hydrating:[/bold yellow] {project_name}", border_style="yellow"))
|
|
87
|
+
else:
|
|
88
|
+
console.print(Panel(f"🚀 [bold green]Creating New Project:[/bold green] {project_name}", border_style="green"))
|
|
89
|
+
|
|
90
|
+
# Create Root (or Hydrate)
|
|
91
|
+
gen = ProjectGenerator(
|
|
92
|
+
name=project_name,
|
|
93
|
+
description=project_conf.get("description", ""),
|
|
94
|
+
type=settings_conf.get("type", TYPE_CLASSIC),
|
|
95
|
+
author=project_conf.get("author", None),
|
|
96
|
+
license=project_conf.get("license", DEFAULT_LICENSE),
|
|
97
|
+
builder=project_conf.get("builder", DEFAULT_BUILDER),
|
|
98
|
+
use_env=settings_conf.get("use_env", False),
|
|
99
|
+
use_config=settings_conf.get("use_config", True),
|
|
100
|
+
use_tests=settings_conf.get("use_tests", True),
|
|
101
|
+
framework=settings_conf.get("framework", FRAMEWORK_PYTORCH),
|
|
102
|
+
verbose=self.verbose
|
|
103
|
+
)
|
|
104
|
+
gen.generate(self.root_path)
|
|
105
|
+
current_root = target_dir
|
|
106
|
+
|
|
107
|
+
# 2. Copy Config to Root (Source of Truth)
|
|
108
|
+
# Only if we aren't reading the one already there
|
|
109
|
+
system_config_path = current_root / "viperx.yaml"
|
|
110
|
+
if self.config_path.absolute() != system_config_path.absolute():
|
|
111
|
+
import shutil
|
|
112
|
+
shutil.copy2(self.config_path, system_config_path)
|
|
113
|
+
console.print(f"[dim]Saved configuration to {system_config_path.name}[/dim]")
|
|
114
|
+
|
|
115
|
+
# 3. Handle Workspace Packages
|
|
116
|
+
packages = workspace_conf.get("packages", [])
|
|
117
|
+
if packages:
|
|
118
|
+
console.print(f"\n📦 [bold]Processing {len(packages)} workspace packages...[/bold]")
|
|
119
|
+
|
|
120
|
+
for pkg in packages:
|
|
121
|
+
pkg_name = pkg.get("name")
|
|
122
|
+
pkg_path = current_root / "src" / pkg_name.replace("-", "_") # Approximate check
|
|
123
|
+
|
|
124
|
+
# We instantiate a generator for this package
|
|
125
|
+
pkg_gen = ProjectGenerator(
|
|
126
|
+
name=pkg_name,
|
|
127
|
+
description=pkg.get("description", ""),
|
|
128
|
+
type=pkg.get("type", TYPE_CLASSIC),
|
|
129
|
+
author=project_conf.get("author", "Your Name"), # Inherit author
|
|
130
|
+
use_env=pkg.get("use_env", settings_conf.get("use_env", False)), # Inherit settings or default False
|
|
131
|
+
use_config=pkg.get("use_config", settings_conf.get("use_config", True)), # Inherit or default True
|
|
132
|
+
use_readme=pkg.get("use_readme", True),
|
|
133
|
+
use_tests=pkg.get("use_tests", settings_conf.get("use_tests", True)),
|
|
134
|
+
framework=pkg.get("framework", FRAMEWORK_PYTORCH),
|
|
135
|
+
verbose=self.verbose
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Check if package seems to exist (ProjectGenerator handles upgrade logic too)
|
|
139
|
+
pkg_gen.add_to_workspace(current_root)
|
|
140
|
+
|
|
141
|
+
console.print(Panel(f"✨ [bold green]Configuration Applied Successfully![/bold green]\nProject is up to date with {self.config_path.name}", border_style="green"))
|
viperx/constants.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
# Project Defaults
|
|
4
|
+
DEFAULT_VERSION = "0.1.0"
|
|
5
|
+
DEFAULT_PYTHON_VERSION = "3.11"
|
|
6
|
+
DEFAULT_LICENSE = "MIT"
|
|
7
|
+
DEFAULT_BUILDER = "uv"
|
|
8
|
+
|
|
9
|
+
# File Names
|
|
10
|
+
CONFIG_FILENAME = "config.yaml"
|
|
11
|
+
PYPROJECT_FILENAME = "pyproject.toml"
|
|
12
|
+
PACKAGE_NAME = "viperx"
|
|
13
|
+
README_FILENAME = "README.md"
|
|
14
|
+
INIT_FILENAME = "__init__.py"
|
|
15
|
+
MAIN_FILENAME = "main.py"
|
|
16
|
+
|
|
17
|
+
# Directory Names
|
|
18
|
+
SRC_DIR = "src"
|
|
19
|
+
NOTEBOOKS_DIR = "notebooks"
|
|
20
|
+
TESTS_DIR = "tests"
|
|
21
|
+
|
|
22
|
+
# Templates
|
|
23
|
+
TEMPLATE_DIR_NAME = "templates"
|
|
24
|
+
TEMPLATES_DIR = Path(__file__).parent / TEMPLATE_DIR_NAME
|
|
25
|
+
|
|
26
|
+
# Types
|
|
27
|
+
TYPE_CLASSIC = "classic"
|
|
28
|
+
TYPE_ML = "ml"
|
|
29
|
+
TYPE_DL = "dl"
|
|
30
|
+
PROJECT_TYPES = [TYPE_CLASSIC, TYPE_ML, TYPE_DL]
|
|
31
|
+
|
|
32
|
+
# DL Frameworks
|
|
33
|
+
FRAMEWORK_PYTORCH = "pytorch"
|
|
34
|
+
FRAMEWORK_TENSORFLOW = "tensorflow"
|
|
35
|
+
DL_FRAMEWORKS = [FRAMEWORK_PYTORCH, FRAMEWORK_TENSORFLOW]
|
viperx/core.py
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from jinja2 import Environment, PackageLoader, select_autoescape
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from viperx.constants import (
|
|
12
|
+
TEMPLATES_DIR,
|
|
13
|
+
DEFAULT_VERSION,
|
|
14
|
+
DEFAULT_PYTHON_VERSION,
|
|
15
|
+
DEFAULT_LICENSE,
|
|
16
|
+
DEFAULT_BUILDER,
|
|
17
|
+
TYPE_CLASSIC,
|
|
18
|
+
TYPE_ML,
|
|
19
|
+
TYPE_DL,
|
|
20
|
+
SRC_DIR,
|
|
21
|
+
NOTEBOOKS_DIR,
|
|
22
|
+
TESTS_DIR,
|
|
23
|
+
)
|
|
24
|
+
from .utils import sanitize_project_name, get_author_from_git
|
|
25
|
+
from .licenses import LICENSES
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
class ProjectGenerator:
|
|
30
|
+
def __init__(self, name: str, description: str, type: str,
|
|
31
|
+
author: str,
|
|
32
|
+
use_env: bool = False, use_config: bool = True,
|
|
33
|
+
use_readme: bool = True, use_tests: bool = True,
|
|
34
|
+
license: str = DEFAULT_LICENSE,
|
|
35
|
+
builder: str = DEFAULT_BUILDER,
|
|
36
|
+
framework: str = "pytorch",
|
|
37
|
+
verbose: bool = False):
|
|
38
|
+
self.raw_name = name
|
|
39
|
+
self.project_name = sanitize_project_name(name)
|
|
40
|
+
self.description = description or name
|
|
41
|
+
self.type = type
|
|
42
|
+
self.framework = framework
|
|
43
|
+
self.author = author
|
|
44
|
+
if not self.author or self.author == "Your Name":
|
|
45
|
+
self.author, self.author_email = get_author_from_git()
|
|
46
|
+
else:
|
|
47
|
+
self.author_email = "your.email@example.com"
|
|
48
|
+
|
|
49
|
+
self.license = license
|
|
50
|
+
self.builder = builder
|
|
51
|
+
self.use_env = use_env
|
|
52
|
+
self.use_config = use_config
|
|
53
|
+
self.use_readme = use_readme
|
|
54
|
+
self.use_tests = use_tests
|
|
55
|
+
self.verbose = verbose
|
|
56
|
+
|
|
57
|
+
# Detect System Python
|
|
58
|
+
self.python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
59
|
+
|
|
60
|
+
# Jinja Setup
|
|
61
|
+
self.env = Environment(
|
|
62
|
+
loader=PackageLoader("viperx", "templates"),
|
|
63
|
+
autoescape=select_autoescape()
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def log(self, message: str, style: str = "dim"):
|
|
67
|
+
if self.verbose:
|
|
68
|
+
console.print(f" [{style}]{message}[/{style}]")
|
|
69
|
+
|
|
70
|
+
def generate(self, target_dir: Optional[Path] = None):
|
|
71
|
+
"""Main generation flow using uv init."""
|
|
72
|
+
if target_dir is None:
|
|
73
|
+
target_dir = Path.cwd()
|
|
74
|
+
|
|
75
|
+
project_dir = target_dir / self.raw_name
|
|
76
|
+
|
|
77
|
+
if project_dir.exists():
|
|
78
|
+
console.print(f"[bold red]Error:[/bold red] Directory {project_dir} already exists.")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
console.print(f"[bold green]Creating project {self.raw_name} ({self.type})...[/bold green]")
|
|
82
|
+
|
|
83
|
+
self.log(f"Target directory: {project_dir}")
|
|
84
|
+
self.log(f"Python version: {self.python_version}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def generate(self, target_dir: Path, is_subpackage: bool = False):
|
|
88
|
+
"""Main generation flow using uv init."""
|
|
89
|
+
project_dir = target_dir / self.raw_name
|
|
90
|
+
|
|
91
|
+
# 1. Scaffolding with uv init
|
|
92
|
+
try:
|
|
93
|
+
if project_dir.exists():
|
|
94
|
+
if (project_dir / "pyproject.toml").exists():
|
|
95
|
+
console.print(f"[bold red]Error:[/bold red] Directory {project_dir} is already a project.")
|
|
96
|
+
return
|
|
97
|
+
# Hydrate existing directory
|
|
98
|
+
console.print(f" [yellow]Hydrating existing directory {project_dir}...[/yellow]")
|
|
99
|
+
subprocess.run(
|
|
100
|
+
["uv", "init", "--package", "--no-workspace"],
|
|
101
|
+
check=True, cwd=project_dir, capture_output=True
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
# Create new
|
|
105
|
+
subprocess.run(
|
|
106
|
+
["uv", "init", "--package", "--no-workspace", self.raw_name],
|
|
107
|
+
check=True, cwd=target_dir, capture_output=True
|
|
108
|
+
)
|
|
109
|
+
console.print(" [blue]✓ Scaffolding created with uv init[/blue]")
|
|
110
|
+
except subprocess.CalledProcessError as e:
|
|
111
|
+
console.print(f"[bold red]Error running uv init:[/bold red] {e}")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# 2. Restructure / Clean up
|
|
115
|
+
# If is_subpackage, convert to Ultra-Flat Layout (Code at Root)
|
|
116
|
+
# Target: root/__init__.py (and siblings)
|
|
117
|
+
# Source: root/src/pkg/__init__.py (from uv init --lib)
|
|
118
|
+
if is_subpackage:
|
|
119
|
+
src_pkg_path = project_dir / "src" / self.project_name
|
|
120
|
+
import shutil
|
|
121
|
+
|
|
122
|
+
if src_pkg_path.exists():
|
|
123
|
+
# Move children of src/pkg to root
|
|
124
|
+
for item in src_pkg_path.iterdir():
|
|
125
|
+
shutil.move(str(item), str(project_dir))
|
|
126
|
+
|
|
127
|
+
# Cleanup src/pkg and src
|
|
128
|
+
shutil.rmtree(src_pkg_path)
|
|
129
|
+
if (project_dir / "src").exists() and not any((project_dir / "src").iterdir()):
|
|
130
|
+
shutil.rmtree(project_dir / "src")
|
|
131
|
+
self.log("Converted to Ultra-Flat Layout (Code at Root)")
|
|
132
|
+
|
|
133
|
+
# 3. Create extra directories (First, so templates have target dirs)
|
|
134
|
+
self._create_extra_dirs(project_dir, is_subpackage)
|
|
135
|
+
|
|
136
|
+
# 4. Overwrite/Add Files
|
|
137
|
+
self._generate_files(project_dir, is_subpackage)
|
|
138
|
+
|
|
139
|
+
# 5. Git & Final Steps
|
|
140
|
+
console.print(f"\n[bold green]✓ Project {self.raw_name} created successfully![/bold green]")
|
|
141
|
+
if not is_subpackage:
|
|
142
|
+
console.print(f" [dim]cd {self.raw_name} && uv sync[/dim]")
|
|
143
|
+
|
|
144
|
+
def _create_extra_dirs(self, root: Path, is_subpackage: bool = False):
|
|
145
|
+
if is_subpackage:
|
|
146
|
+
# Ultra-Flat Layout: root IS the package root
|
|
147
|
+
pkg_root = root
|
|
148
|
+
else:
|
|
149
|
+
# Standard Layout: root / src / package_name
|
|
150
|
+
pkg_root = root / SRC_DIR / self.project_name
|
|
151
|
+
|
|
152
|
+
# Notebooks for ML/DL (Only for Root Project usually)
|
|
153
|
+
if not is_subpackage and self.type in [TYPE_ML, TYPE_DL]:
|
|
154
|
+
(root / NOTEBOOKS_DIR).mkdir(exist_ok=True)
|
|
155
|
+
self.log("Created notebooks directory")
|
|
156
|
+
|
|
157
|
+
# Tests
|
|
158
|
+
if self.use_tests:
|
|
159
|
+
# For Flat Layout (Subpackage), tests usually go to `tests/` at root
|
|
160
|
+
# For Src Layout (Root), inside `src/pkg/tests`?
|
|
161
|
+
# User request: "create dossier tests ... que ce soit au init general ou pour sous package"
|
|
162
|
+
# User request: "tests/ pour le package principal est dans src/name_package/ tout est isolé"
|
|
163
|
+
# So tests are INSIDE the package.
|
|
164
|
+
tests_dir = pkg_root / TESTS_DIR
|
|
165
|
+
tests_dir.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
|
|
167
|
+
with open(tests_dir / "__init__.py", "w") as f:
|
|
168
|
+
pass
|
|
169
|
+
with open(tests_dir / "test_core.py", "w") as f:
|
|
170
|
+
f.write("def test_dummy():\n assert True\n")
|
|
171
|
+
self.log(f"Created tests directory at {tests_dir.relative_to(root)}")
|
|
172
|
+
|
|
173
|
+
def _generate_files(self, root: Path, is_subpackage: bool = False):
|
|
174
|
+
self.log(f"Generating files for {self.project_name}...")
|
|
175
|
+
context = {
|
|
176
|
+
"project_name": self.raw_name,
|
|
177
|
+
"package_name": self.project_name,
|
|
178
|
+
"description": self.description,
|
|
179
|
+
"version": DEFAULT_VERSION,
|
|
180
|
+
"python_version": self.python_version or DEFAULT_PYTHON_VERSION,
|
|
181
|
+
"author_name": self.author,
|
|
182
|
+
"author_email": self.author_email,
|
|
183
|
+
"license": self.license,
|
|
184
|
+
"project_type": self.type,
|
|
185
|
+
"use_uv": self.builder == "uv",
|
|
186
|
+
"has_config": self.use_config,
|
|
187
|
+
"use_readme": self.use_readme,
|
|
188
|
+
"use_env": self.use_env,
|
|
189
|
+
"framework": self.framework,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# pyproject.toml (Overwrite uv's basic one to add our specific deps)
|
|
193
|
+
self._render("pyproject.toml.j2", root / "pyproject.toml", context)
|
|
194
|
+
|
|
195
|
+
# Determine Package Root
|
|
196
|
+
if is_subpackage:
|
|
197
|
+
# Ultra-Flat Layout: root IS the package root
|
|
198
|
+
pkg_root = root
|
|
199
|
+
else:
|
|
200
|
+
# Standard Layout: root / src / package_name
|
|
201
|
+
pkg_root = root / SRC_DIR / self.project_name
|
|
202
|
+
|
|
203
|
+
# Ensure pkg root exists
|
|
204
|
+
if not pkg_root.exists():
|
|
205
|
+
# If we messed up logic or uv failed
|
|
206
|
+
pkg_root.mkdir(parents=True, exist_ok=True)
|
|
207
|
+
self.log(f"Created package root {pkg_root}")
|
|
208
|
+
|
|
209
|
+
# __init__.py
|
|
210
|
+
self._render("__init__.py.j2", pkg_root / "__init__.py", context)
|
|
211
|
+
|
|
212
|
+
# README.md
|
|
213
|
+
if self.use_readme:
|
|
214
|
+
self._render("README.md.j2", root / "README.md", context)
|
|
215
|
+
else:
|
|
216
|
+
if (root / "README.md").exists():
|
|
217
|
+
(root / "README.md").unlink()
|
|
218
|
+
self.log("Removed default README.md (requested --no-readme)")
|
|
219
|
+
|
|
220
|
+
# LICENSE
|
|
221
|
+
license_text = LICENSES.get(self.license, LICENSES["MIT"])
|
|
222
|
+
license_text = license_text.format(year=datetime.now().year, author=self.author)
|
|
223
|
+
with open(root / "LICENSE", "w") as f:
|
|
224
|
+
f.write(license_text)
|
|
225
|
+
self.log(f"Generated LICENSE ({self.license})")
|
|
226
|
+
|
|
227
|
+
# Config files
|
|
228
|
+
if self.use_config:
|
|
229
|
+
self._render("config.yaml.j2", pkg_root / "config.yaml", context)
|
|
230
|
+
self._render("config.py.j2", pkg_root / "config.py", context)
|
|
231
|
+
|
|
232
|
+
# Entry points & Logic
|
|
233
|
+
self._render("main.py.j2", pkg_root / "main.py", context)
|
|
234
|
+
|
|
235
|
+
if not is_subpackage and self.type in [TYPE_ML, TYPE_DL]:
|
|
236
|
+
# Render Notebooks
|
|
237
|
+
self._render("Base_Kaggle.ipynb.j2", root / NOTEBOOKS_DIR / "Base_Kaggle.ipynb", context)
|
|
238
|
+
self._render("Base_General.ipynb.j2", root / NOTEBOOKS_DIR / "Base_General.ipynb", context)
|
|
239
|
+
# Render Data Loader
|
|
240
|
+
self._render("data_loader.py.j2", pkg_root / "data_loader.py", context)
|
|
241
|
+
self.log("Generated wrappers: Base_Kaggle.ipynb, Base_General.ipynb, data_loader.py")
|
|
242
|
+
|
|
243
|
+
# .env (Strict Isolation: In pkg_root)
|
|
244
|
+
if self.use_env:
|
|
245
|
+
with open(pkg_root / ".env", "w") as f:
|
|
246
|
+
f.write("# Environment Variables (Isolated)\n")
|
|
247
|
+
with open(pkg_root / ".env.example", "w") as f:
|
|
248
|
+
f.write("# Environment Variables Example\n")
|
|
249
|
+
self.log(f"Created .env and .env.example in {pkg_root.relative_to(root)}")
|
|
250
|
+
|
|
251
|
+
# .gitignore
|
|
252
|
+
with open(root / ".gitignore", "a") as f:
|
|
253
|
+
# Add data/ to gitignore but allow .gitkeep
|
|
254
|
+
f.write("\n# ViperX specific\n.ipynb_checkpoints/\n# Isolated Env\nsrc/**/.env\n# Data (Local)\ndata/*\n!data/.gitkeep\n")
|
|
255
|
+
self.log("Updated .gitignore")
|
|
256
|
+
|
|
257
|
+
def _render(self, template_name: str, target_path: Path, context: dict):
|
|
258
|
+
template = self.env.get_template(template_name)
|
|
259
|
+
content = template.render(**context)
|
|
260
|
+
with open(target_path, "w") as f:
|
|
261
|
+
f.write(content)
|
|
262
|
+
self.log(f"Rendered {target_path.name}")
|
|
263
|
+
|
|
264
|
+
def add_to_workspace(self, workspace_root: Path):
|
|
265
|
+
"""Add a new package to an existing workspace."""
|
|
266
|
+
console.print(f"[bold green]Adding package {self.raw_name} to workspace...[/bold green]")
|
|
267
|
+
|
|
268
|
+
pyproject_path = workspace_root / "pyproject.toml"
|
|
269
|
+
if not pyproject_path.exists():
|
|
270
|
+
console.print("[red]Error: Not in a valid project root (pyproject.toml missing).[/red]")
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
# Read pyproject.toml
|
|
274
|
+
with open(pyproject_path, "r") as f:
|
|
275
|
+
content = f.read()
|
|
276
|
+
|
|
277
|
+
# Check if it's already a workspace
|
|
278
|
+
is_workspace = "[tool.uv.workspace]" in content
|
|
279
|
+
|
|
280
|
+
if not is_workspace:
|
|
281
|
+
console.print("[yellow]Upgrading project to Workspace...[/yellow]")
|
|
282
|
+
# Append workspace definition
|
|
283
|
+
with open(pyproject_path, "a") as f:
|
|
284
|
+
f.write(f"\n[tool.uv.workspace]\nmembers = [\"src/{self.raw_name}\"]\n")
|
|
285
|
+
self.log("Added [tool.uv.workspace] section")
|
|
286
|
+
else:
|
|
287
|
+
# Add member to specific list if it exists
|
|
288
|
+
# We use a simple regex approach to find 'members = [...]'
|
|
289
|
+
import re
|
|
290
|
+
members_pattern = r'members\s*=\s*\[(.*?)\]'
|
|
291
|
+
match = re.search(members_pattern, content, re.DOTALL)
|
|
292
|
+
|
|
293
|
+
if match:
|
|
294
|
+
current_members = match.group(1)
|
|
295
|
+
# Check if already present
|
|
296
|
+
if f'"{self.raw_name}"' in current_members or f"'{self.raw_name}'" in current_members:
|
|
297
|
+
self.log(f"Package {self.raw_name} is already in workspace members.")
|
|
298
|
+
else:
|
|
299
|
+
# Append new member
|
|
300
|
+
# We inject it into the list
|
|
301
|
+
self.log("Adding member to existing workspace list")
|
|
302
|
+
# Naively replace the closing bracket
|
|
303
|
+
# Better: parse, but for now robust string insertion
|
|
304
|
+
# Cleanest way without breaking formatting involves finding the last element
|
|
305
|
+
# Cleanest way without breaking formatting involves finding the last element
|
|
306
|
+
new_member = f', "src/{self.raw_name}"'
|
|
307
|
+
# Warning: This regex replace is basic. `uv` handles toml well, maybe we should just edit safely.
|
|
308
|
+
# Let's try to append to the end of the content of the list
|
|
309
|
+
new_content = re.sub(members_pattern, lambda m: f'members = [{m.group(1)}{new_member}]', content, flags=re.DOTALL)
|
|
310
|
+
with open(pyproject_path, "w") as f:
|
|
311
|
+
f.write(new_content)
|
|
312
|
+
else:
|
|
313
|
+
# Section exists but members key might be missing? Or weird formatting.
|
|
314
|
+
# Append to section?
|
|
315
|
+
# Safe fallback
|
|
316
|
+
console.print("[yellow]Warning: Could not parse members list. Adding manually at end.[/yellow]")
|
|
317
|
+
with open(pyproject_path, "a") as f:
|
|
318
|
+
f.write(f"\n# Added by viperx\n[tool.uv.workspace]\nmembers = [\"{self.raw_name}\"]\n")
|
|
319
|
+
|
|
320
|
+
# Generate the package in the root IF it doesn't exist
|
|
321
|
+
pkg_dir = workspace_root / SRC_DIR / self.raw_name
|
|
322
|
+
if pkg_dir.exists():
|
|
323
|
+
self.log(f"Package directory {self.raw_name} exists. Skipping generation.")
|
|
324
|
+
else:
|
|
325
|
+
# Generate as SUBPACKAGE (Flat Layout)
|
|
326
|
+
# We pass workspace_root / SRC_DIR as the target for generation
|
|
327
|
+
# self.generate(target = root/src) -> uv init root/src/pkg -> moves to flat
|
|
328
|
+
self.generate(workspace_root / SRC_DIR, is_subpackage=True)
|
|
329
|
+
|
|
330
|
+
# Post-generation: Ensure root knows about it
|
|
331
|
+
console.print(f"[bold green]✓ Synced {self.raw_name} with workspace.[/bold green]")
|
|
332
|
+
# console.print(f" Run [bold]uv sync[/bold] to link the new package.")
|
|
333
|
+
|
|
334
|
+
def delete_from_workspace(self, workspace_root: Path):
|
|
335
|
+
"""Remove a package from the workspace."""
|
|
336
|
+
console.print(f"[bold red]Removing package {self.raw_name} from workspace...[/bold red]")
|
|
337
|
+
|
|
338
|
+
target_dir = workspace_root / SRC_DIR / self.raw_name
|
|
339
|
+
pyproject_path = workspace_root / "pyproject.toml"
|
|
340
|
+
|
|
341
|
+
# 1. Remove directory
|
|
342
|
+
if target_dir.exists():
|
|
343
|
+
import shutil
|
|
344
|
+
shutil.rmtree(target_dir)
|
|
345
|
+
self.log(f"Removed directory {target_dir}")
|
|
346
|
+
else:
|
|
347
|
+
console.print(f"[yellow]Directory {target_dir} not found.[/yellow]")
|
|
348
|
+
|
|
349
|
+
# 2. Update pyproject.toml
|
|
350
|
+
if pyproject_path.exists():
|
|
351
|
+
with open(pyproject_path, "r") as f:
|
|
352
|
+
content = f.read()
|
|
353
|
+
|
|
354
|
+
# Regex to remove member
|
|
355
|
+
# This handles "member", 'member' and checks for commas
|
|
356
|
+
import re
|
|
357
|
+
# Patter matches the member string with optional surrounding whitespace and optional following comma
|
|
358
|
+
# It's tricky to be perfect with regex on TOML logic, but reasonably safe for standard lists
|
|
359
|
+
# We look for the exact string inside the brackets
|
|
360
|
+
|
|
361
|
+
# Simple approach: Load content, replace the specific member string literal with empty
|
|
362
|
+
# But we need to handle commas.
|
|
363
|
+
# Let's try to just remove the string and cleanup commas?
|
|
364
|
+
# Or better: specific regex for the element.
|
|
365
|
+
|
|
366
|
+
member_str_double = f'"src/{self.raw_name}"'
|
|
367
|
+
member_str_single = f"'src/{self.raw_name}'"
|
|
368
|
+
|
|
369
|
+
new_content = content
|
|
370
|
+
if member_str_double in new_content:
|
|
371
|
+
new_content = new_content.replace(member_str_double, "")
|
|
372
|
+
elif member_str_single in new_content:
|
|
373
|
+
new_content = new_content.replace(member_str_single, "")
|
|
374
|
+
else:
|
|
375
|
+
self.log("Member not found in pyproject.toml list string")
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
# Cleanup double commas or trailing commas/empty items inside the list [ , , ]
|
|
379
|
+
# This is "dirty" but works for simple lists
|
|
380
|
+
new_content = re.sub(r'\[\s*,', '[', new_content) # Leading comma
|
|
381
|
+
new_content = re.sub(r',\s*,', ',', new_content) # Double comma
|
|
382
|
+
new_content = re.sub(r',\s*\]', ']', new_content) # Trailing comma
|
|
383
|
+
|
|
384
|
+
with open(pyproject_path, "w") as f:
|
|
385
|
+
f.write(new_content)
|
|
386
|
+
self.log("Removed member from pyproject.toml")
|
|
387
|
+
|
|
388
|
+
console.print(f"[bold green]✓ Removed {self.raw_name} successfully.[/bold green]")
|
|
389
|
+
|
|
390
|
+
def update_package(self, workspace_root: Path):
|
|
391
|
+
"""Update a package (dependencies)."""
|
|
392
|
+
console.print(f"[bold blue]Updating package {self.raw_name}...[/bold blue]")
|
|
393
|
+
|
|
394
|
+
target_dir = workspace_root / SRC_DIR / self.raw_name
|
|
395
|
+
if not target_dir.exists():
|
|
396
|
+
console.print(f"[red]Error: Package {self.raw_name} does not exist.[/red]")
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
# Run uv lock --upgrade
|
|
400
|
+
# Actually in a workspace, usually we run sync from root or lock from root?
|
|
401
|
+
# If it's a workspace member, `uv lock` at root updates everything.
|
|
402
|
+
# But maybe we want specific update?
|
|
403
|
+
# Let's run `uv lock --upgrade-package name` if supported or just universal update.
|
|
404
|
+
# For now, let's assume we run `uv lock --upgrade` inside the package or root.
|
|
405
|
+
# Running inside the package dir usually affects the workspace lock if using "workspaces".
|
|
406
|
+
|
|
407
|
+
import subprocess
|
|
408
|
+
try:
|
|
409
|
+
cmd = ["uv", "lock", "--upgrade"]
|
|
410
|
+
self.log(f"Running {' '.join(cmd)}")
|
|
411
|
+
subprocess.run(cmd, cwd=target_dir, check=True)
|
|
412
|
+
console.print(f"[bold green]✓ Updated {self.raw_name} dependencies.[/bold green]")
|
|
413
|
+
except subprocess.CalledProcessError:
|
|
414
|
+
console.print(f"[red]Failed to update {self.raw_name}.[/red]")
|
|
415
|
+
|
|
416
|
+
|