viperx 0.9.26__py3-none-any.whl → 0.9.50__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/config_engine.py +77 -19
- viperx/core.py +72 -89
- viperx/main.py +7 -0
- viperx/templates/README.md.j2 +34 -14
- viperx/templates/__init__.py.j2 +4 -4
- viperx/templates/config.yaml.j2 +3 -0
- viperx/templates/main.py.j2 +7 -7
- viperx/templates/pyproject.toml.j2 +20 -19
- viperx/templates/viperx_config.yaml.j2 +1 -1
- viperx/utils.py +17 -1
- {viperx-0.9.26.dist-info → viperx-0.9.50.dist-info}/METADATA +1 -1
- viperx-0.9.50.dist-info/RECORD +22 -0
- viperx-0.9.26.dist-info/RECORD +0 -22
- {viperx-0.9.26.dist-info → viperx-0.9.50.dist-info}/WHEEL +0 -0
- {viperx-0.9.26.dist-info → viperx-0.9.50.dist-info}/entry_points.txt +0 -0
viperx/config_engine.py
CHANGED
|
@@ -4,7 +4,7 @@ from rich.console import Console
|
|
|
4
4
|
from rich.panel import Panel
|
|
5
5
|
|
|
6
6
|
from viperx.core import ProjectGenerator
|
|
7
|
-
from viperx.constants import DEFAULT_LICENSE, DEFAULT_BUILDER, TYPE_CLASSIC, FRAMEWORK_PYTORCH
|
|
7
|
+
from viperx.constants import DEFAULT_LICENSE, DEFAULT_BUILDER, TYPE_CLASSIC, TYPE_ML, TYPE_DL, FRAMEWORK_PYTORCH
|
|
8
8
|
|
|
9
9
|
console = Console()
|
|
10
10
|
|
|
@@ -47,15 +47,22 @@ class ConfigEngine:
|
|
|
47
47
|
workspace_conf = self.config.get("workspace", {})
|
|
48
48
|
|
|
49
49
|
project_name = project_conf.get("name")
|
|
50
|
-
target_dir = self.root_path / project_name
|
|
51
50
|
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
# STRICT NAMING: Always calculate the expected root path using sanitized name
|
|
52
|
+
from viperx.utils import sanitize_project_name
|
|
53
|
+
clean_name = sanitize_project_name(project_name)
|
|
55
54
|
|
|
56
|
-
#
|
|
57
|
-
|
|
55
|
+
# Default assumption: current_root is the target directory (folder with underscores)
|
|
56
|
+
current_root = self.root_path / clean_name
|
|
57
|
+
target_dir = current_root
|
|
58
|
+
|
|
59
|
+
# 1. Root Project Handling
|
|
60
|
+
# Heuristic: Are we already in a folder matching the raw name OR sanitized name?
|
|
61
|
+
# e.g. inside test_classic/
|
|
62
|
+
if self.root_path.name == project_name or self.root_path.name == clean_name:
|
|
58
63
|
# We are inside the project folder
|
|
64
|
+
current_root = self.root_path
|
|
65
|
+
|
|
59
66
|
if not (self.root_path / "pyproject.toml").exists():
|
|
60
67
|
console.print(Panel(f"⚠️ [bold yellow]Current directory matches name but is not initialized. Hydrating:[/bold yellow] {project_name}", border_style="yellow"))
|
|
61
68
|
gen = ProjectGenerator(
|
|
@@ -69,27 +76,69 @@ class ConfigEngine:
|
|
|
69
76
|
use_config=settings_conf.get("use_config", True),
|
|
70
77
|
use_tests=settings_conf.get("use_tests", True),
|
|
71
78
|
framework=settings_conf.get("framework", FRAMEWORK_PYTORCH),
|
|
79
|
+
scripts={project_name: f"{clean_name}.main:main"},
|
|
72
80
|
verbose=self.verbose
|
|
73
81
|
)
|
|
74
|
-
# generate() expects parent dir, and will operate on parent/name (which is self.root_path)
|
|
75
82
|
gen.generate(self.root_path.parent)
|
|
76
83
|
else:
|
|
77
84
|
console.print(Panel(f"♻️ [bold blue]Syncing Project:[/bold blue] {project_name}", border_style="blue"))
|
|
78
|
-
|
|
85
|
+
|
|
79
86
|
else:
|
|
80
|
-
# We are outside
|
|
87
|
+
# We are outside
|
|
88
|
+
# target_dir (clean) is already set as current_root default
|
|
89
|
+
|
|
81
90
|
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
|
|
91
|
+
console.print(Panel(f"♻️ [bold blue]Updating Existing Project:[/bold blue] {project_name} ({target_dir.name})", border_style="blue"))
|
|
84
92
|
else:
|
|
85
93
|
if target_dir.exists():
|
|
86
94
|
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
95
|
|
|
90
|
-
#
|
|
96
|
+
# Prepare Scripts & Dependency Context
|
|
97
|
+
packages = workspace_conf.get("packages", [])
|
|
98
|
+
|
|
99
|
+
# --- Aggregate Global Dependencies ---
|
|
100
|
+
root_use_config = settings_conf.get("use_config", True)
|
|
101
|
+
root_use_env = settings_conf.get("use_env", False)
|
|
102
|
+
root_type = settings_conf.get("type", TYPE_CLASSIC)
|
|
103
|
+
root_framework = settings_conf.get("framework", FRAMEWORK_PYTORCH)
|
|
104
|
+
|
|
105
|
+
glob_has_config = root_use_config
|
|
106
|
+
glob_has_env = root_use_env
|
|
107
|
+
glob_is_ml_dl = root_type in [TYPE_ML, TYPE_DL]
|
|
108
|
+
glob_is_dl = root_type == TYPE_DL
|
|
109
|
+
glob_frameworks = {root_framework} if glob_is_dl else set()
|
|
110
|
+
|
|
111
|
+
project_scripts = {project_name: f"{clean_name}.main:main"} # Use clean mapping
|
|
112
|
+
|
|
113
|
+
for pkg in packages:
|
|
114
|
+
# Scripts
|
|
115
|
+
pkg_name = pkg.get("name")
|
|
116
|
+
pkg_name_clean = sanitize_project_name(pkg_name)
|
|
117
|
+
project_scripts[pkg_name] = f"{pkg_name_clean}.main:main"
|
|
118
|
+
|
|
119
|
+
# Dependency Aggregation
|
|
120
|
+
p_config = pkg.get("use_config", settings_conf.get("use_config", True))
|
|
121
|
+
p_env = pkg.get("use_env", settings_conf.get("use_env", False))
|
|
122
|
+
p_type = pkg.get("type", TYPE_CLASSIC)
|
|
123
|
+
p_framework = pkg.get("framework", FRAMEWORK_PYTORCH)
|
|
124
|
+
|
|
125
|
+
if p_config: glob_has_config = True
|
|
126
|
+
if p_env: glob_has_env = True
|
|
127
|
+
if p_type in [TYPE_ML, TYPE_DL]: glob_is_ml_dl = True
|
|
128
|
+
if p_type == TYPE_DL:
|
|
129
|
+
glob_is_dl = True
|
|
130
|
+
glob_frameworks.add(p_framework)
|
|
131
|
+
|
|
132
|
+
dep_context = {
|
|
133
|
+
"has_config": glob_has_config,
|
|
134
|
+
"has_env": glob_has_env,
|
|
135
|
+
"is_ml_dl": glob_is_ml_dl,
|
|
136
|
+
"is_dl": glob_is_dl,
|
|
137
|
+
"frameworks": list(glob_frameworks)
|
|
138
|
+
}
|
|
139
|
+
|
|
91
140
|
gen = ProjectGenerator(
|
|
92
|
-
name=project_name,
|
|
141
|
+
name=project_name, # Raw name
|
|
93
142
|
description=project_conf.get("description", ""),
|
|
94
143
|
type=settings_conf.get("type", TYPE_CLASSIC),
|
|
95
144
|
author=project_conf.get("author", None),
|
|
@@ -99,10 +148,19 @@ class ConfigEngine:
|
|
|
99
148
|
use_config=settings_conf.get("use_config", True),
|
|
100
149
|
use_tests=settings_conf.get("use_tests", True),
|
|
101
150
|
framework=settings_conf.get("framework", FRAMEWORK_PYTORCH),
|
|
151
|
+
scripts=project_scripts,
|
|
152
|
+
dependency_context=dep_context,
|
|
102
153
|
verbose=self.verbose
|
|
103
154
|
)
|
|
104
155
|
gen.generate(self.root_path)
|
|
105
|
-
|
|
156
|
+
|
|
157
|
+
# Verify creation
|
|
158
|
+
if not current_root.exists():
|
|
159
|
+
if (self.root_path / project_name).exists():
|
|
160
|
+
current_root = self.root_path / project_name
|
|
161
|
+
|
|
162
|
+
if self.verbose:
|
|
163
|
+
console.print(f"[debug] Project Root resolves to: {current_root}")
|
|
106
164
|
|
|
107
165
|
# 2. Copy Config to Root (Source of Truth)
|
|
108
166
|
# Only if we aren't reading the one already there
|
|
@@ -115,7 +173,7 @@ class ConfigEngine:
|
|
|
115
173
|
# 3. Handle Workspace Packages
|
|
116
174
|
packages = workspace_conf.get("packages", [])
|
|
117
175
|
if packages:
|
|
118
|
-
console.print(f"
|
|
176
|
+
console.print(f"\\n📦 [bold]Processing {len(packages)} workspace packages...[/bold]")
|
|
119
177
|
|
|
120
178
|
for pkg in packages:
|
|
121
179
|
pkg_name = pkg.get("name")
|
|
@@ -138,4 +196,4 @@ class ConfigEngine:
|
|
|
138
196
|
# Check if package seems to exist (ProjectGenerator handles upgrade logic too)
|
|
139
197
|
pkg_gen.add_to_workspace(current_root)
|
|
140
198
|
|
|
141
|
-
console.print(Panel(f"✨ [bold green]Configuration Applied Successfully![/bold green]
|
|
199
|
+
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/core.py
CHANGED
|
@@ -34,12 +34,32 @@ class ProjectGenerator:
|
|
|
34
34
|
license: str = DEFAULT_LICENSE,
|
|
35
35
|
builder: str = DEFAULT_BUILDER,
|
|
36
36
|
framework: str = "pytorch",
|
|
37
|
+
scripts: Optional[dict] = None,
|
|
38
|
+
dependency_context: Optional[dict] = None,
|
|
37
39
|
verbose: bool = False):
|
|
38
40
|
self.raw_name = name
|
|
39
41
|
self.project_name = sanitize_project_name(name)
|
|
40
42
|
self.description = description or name
|
|
41
43
|
self.type = type
|
|
42
44
|
self.framework = framework
|
|
45
|
+
self.scripts = scripts or {}
|
|
46
|
+
# Dependency Context (Global workspace features)
|
|
47
|
+
self.dependency_context = dependency_context or {
|
|
48
|
+
"has_config": use_config,
|
|
49
|
+
"has_env": use_env,
|
|
50
|
+
"is_ml_dl": type in ["ml", "dl"],
|
|
51
|
+
"is_dl": type == "dl",
|
|
52
|
+
"frameworks": [framework] if type == "dl" else []
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Default script for the main package if none provided (and it's a root project mostly)
|
|
56
|
+
self.scripts = scripts or {}
|
|
57
|
+
# Default script for the main package if none provided (and it's a root project mostly)
|
|
58
|
+
if not self.scripts:
|
|
59
|
+
# Key = Raw Name (CLI command, e.g. test-classic)
|
|
60
|
+
# Value = Sanitized Path (Module, e.g. test_classic.main:main)
|
|
61
|
+
self.scripts = {self.raw_name: f"{self.project_name}.main:main"}
|
|
62
|
+
|
|
43
63
|
self.author = author
|
|
44
64
|
if not self.author or self.author == "Your Name":
|
|
45
65
|
self.author, self.author_email = get_author_from_git()
|
|
@@ -54,46 +74,53 @@ class ProjectGenerator:
|
|
|
54
74
|
self.use_tests = use_tests
|
|
55
75
|
self.verbose = verbose
|
|
56
76
|
|
|
57
|
-
# Detect System Python
|
|
58
|
-
self.
|
|
77
|
+
# Detect System Python (For logging/diagnostics)
|
|
78
|
+
self.system_python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
79
|
+
|
|
80
|
+
# Project Python Version (For requires-python in pyproject.toml)
|
|
81
|
+
# Driven by package constants to ensure compatibility/evolution
|
|
82
|
+
from viperx.constants import DEFAULT_PYTHON_VERSION
|
|
83
|
+
self.python_version = DEFAULT_PYTHON_VERSION
|
|
59
84
|
|
|
85
|
+
|
|
86
|
+
# Validate Choices
|
|
87
|
+
from viperx.utils import validate_choice, check_builder_installed
|
|
88
|
+
from viperx.constants import PROJECT_TYPES, DL_FRAMEWORKS, TYPE_DL
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
validate_choice(self.type, PROJECT_TYPES, "project type")
|
|
92
|
+
if self.type == TYPE_DL:
|
|
93
|
+
validate_choice(self.framework, DL_FRAMEWORKS, "framework")
|
|
94
|
+
|
|
95
|
+
# Validate Builder Existence & Support
|
|
96
|
+
if not check_builder_installed(self.builder):
|
|
97
|
+
from viperx.constants import SUPPORTED_BUILDERS
|
|
98
|
+
if self.builder not in SUPPORTED_BUILDERS:
|
|
99
|
+
console.print(f"[bold red]Error:[/bold red] Invalid builder '[bold]{self.builder}[/bold]'.")
|
|
100
|
+
console.print(f"Supported builders: [green]{', '.join(SUPPORTED_BUILDERS)}[/green]")
|
|
101
|
+
else:
|
|
102
|
+
console.print(f"[bold red]Error:[/bold red] The builder '[bold]{self.builder}[/bold]' is not installed or not in PATH.")
|
|
103
|
+
console.print(f"Please install it (e.g., `pip install {self.builder}` or `curl -LsSf https://astral.sh/uv/install.sh | sh` for uv).")
|
|
104
|
+
sys.exit(1)
|
|
105
|
+
|
|
106
|
+
except ValueError as e:
|
|
107
|
+
console.print(f"[bold red]Configuration Error:[/bold red] {e}")
|
|
108
|
+
sys.exit(1)
|
|
109
|
+
|
|
60
110
|
# Jinja Setup
|
|
61
111
|
self.env = Environment(
|
|
62
112
|
loader=PackageLoader("viperx", "templates"),
|
|
63
113
|
autoescape=select_autoescape()
|
|
64
114
|
)
|
|
65
|
-
|
|
66
|
-
# Validate Builder
|
|
67
|
-
from viperx.utils import check_builder_installed
|
|
68
|
-
if not check_builder_installed(self.builder):
|
|
69
|
-
console.print(f"[bold red]Error:[/bold red] The requested builder '[bold]{self.builder}[/bold]' is not installed or not in PATH.")
|
|
70
|
-
console.print(f"Please install it (e.g., `pip install {self.builder}` or `curl -LsSf https://astral.sh/uv/install.sh | sh` for uv).")
|
|
71
|
-
sys.exit(1)
|
|
72
115
|
|
|
73
116
|
def log(self, message: str, style: str = "dim"):
|
|
74
117
|
if self.verbose:
|
|
75
118
|
console.print(f" [{style}]{message}[/{style}]")
|
|
76
119
|
|
|
77
|
-
def generate(self, target_dir: Optional[Path] = None):
|
|
78
|
-
"""Main generation flow using uv init."""
|
|
79
|
-
if target_dir is None:
|
|
80
|
-
target_dir = Path.cwd()
|
|
81
|
-
|
|
82
|
-
project_dir = target_dir / self.raw_name
|
|
83
|
-
|
|
84
|
-
if project_dir.exists():
|
|
85
|
-
console.print(f"[bold red]Error:[/bold red] Directory {project_dir} already exists.")
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
console.print(f"[bold green]Creating project {self.raw_name} ({self.type})...[/bold green]")
|
|
89
|
-
|
|
90
|
-
self.log(f"Target directory: {project_dir}")
|
|
91
|
-
self.log(f"Python version: {self.python_version}")
|
|
92
|
-
|
|
93
|
-
|
|
94
120
|
def generate(self, target_dir: Path, is_subpackage: bool = False):
|
|
95
121
|
"""Main generation flow using uv init."""
|
|
96
|
-
|
|
122
|
+
# STRICT DIRECTORY NAMING: Always use sanitized name
|
|
123
|
+
project_dir = target_dir / self.project_name
|
|
97
124
|
|
|
98
125
|
# 1. Scaffolding with uv init
|
|
99
126
|
try:
|
|
@@ -109,8 +136,13 @@ class ProjectGenerator:
|
|
|
109
136
|
)
|
|
110
137
|
else:
|
|
111
138
|
# Create new
|
|
139
|
+
# STRICT DIR NAMING: Use self.project_name (underscores) for directory
|
|
140
|
+
# But use self.raw_name (hyphens) for the package Name metadata if possible?
|
|
141
|
+
# uv init [NAME] creates directory NAME.
|
|
142
|
+
# If we want dir=test_classic but name=test-classic:
|
|
143
|
+
# uv init test_classic --name test-classic
|
|
112
144
|
subprocess.run(
|
|
113
|
-
["uv", "init", "--package", "--no-workspace", self.raw_name],
|
|
145
|
+
["uv", "init", "--package", "--no-workspace", self.project_name, "--name", self.raw_name],
|
|
114
146
|
check=True, cwd=target_dir, capture_output=True
|
|
115
147
|
)
|
|
116
148
|
console.print(" [blue]✓ Scaffolding created with uv init[/blue]")
|
|
@@ -151,9 +183,9 @@ class ProjectGenerator:
|
|
|
151
183
|
(project_dir / f).unlink()
|
|
152
184
|
|
|
153
185
|
# 5. Git & Final Steps
|
|
154
|
-
console.print(f"\n[bold green]✓ Project {self.raw_name} created successfully![/bold green]")
|
|
186
|
+
console.print(f"\n[bold green]✓ Project {self.raw_name} created in {self.project_name}/ successfully![/bold green]")
|
|
155
187
|
if not is_subpackage:
|
|
156
|
-
console.print(f" [dim]cd {self.
|
|
188
|
+
console.print(f" [dim]cd {self.project_name} && uv sync[/dim]")
|
|
157
189
|
|
|
158
190
|
def _create_extra_dirs(self, root: Path, is_subpackage: bool = False):
|
|
159
191
|
if is_subpackage:
|
|
@@ -200,17 +232,16 @@ class ProjectGenerator:
|
|
|
200
232
|
"has_config": self.use_config,
|
|
201
233
|
"use_readme": self.use_readme,
|
|
202
234
|
"use_env": self.use_env,
|
|
235
|
+
"use_env": self.use_env,
|
|
203
236
|
"framework": self.framework,
|
|
237
|
+
"scripts": self.scripts,
|
|
204
238
|
}
|
|
239
|
+
# Merge dependency context overrides
|
|
240
|
+
context.update(self.dependency_context)
|
|
205
241
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
else:
|
|
210
|
-
# Subpackages: remove default pyproject.toml from uv init
|
|
211
|
-
if (root / "pyproject.toml").exists():
|
|
212
|
-
(root / "pyproject.toml").unlink()
|
|
213
|
-
self.log("Removed default pyproject.toml (Subpackage: Code Only)")
|
|
242
|
+
# pyproject.toml (Overwrite uv's basic one to add our specific deps)
|
|
243
|
+
# Even subpackages need this if they are Workspace Members (which they are in our model)
|
|
244
|
+
self._render("pyproject.toml.j2", root / "pyproject.toml", context)
|
|
214
245
|
|
|
215
246
|
# Determine Package Root
|
|
216
247
|
if is_subpackage:
|
|
@@ -301,57 +332,9 @@ class ProjectGenerator:
|
|
|
301
332
|
"""Add a new package to an existing workspace."""
|
|
302
333
|
console.print(f"[bold green]Adding package {self.raw_name} to workspace...[/bold green]")
|
|
303
334
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
return
|
|
308
|
-
|
|
309
|
-
# Read pyproject.toml
|
|
310
|
-
with open(pyproject_path, "r") as f:
|
|
311
|
-
content = f.read()
|
|
312
|
-
|
|
313
|
-
# Check if it's already a workspace
|
|
314
|
-
is_workspace = "[tool.uv.workspace]" in content
|
|
315
|
-
|
|
316
|
-
if not is_workspace:
|
|
317
|
-
console.print("[yellow]Upgrading project to Workspace...[/yellow]")
|
|
318
|
-
# Append workspace definition
|
|
319
|
-
with open(pyproject_path, "a") as f:
|
|
320
|
-
f.write(f"\n[tool.uv.workspace]\nmembers = [\"src/{self.raw_name}\"]\n")
|
|
321
|
-
self.log("Added [tool.uv.workspace] section")
|
|
322
|
-
else:
|
|
323
|
-
# Add member to specific list if it exists
|
|
324
|
-
# We use a simple regex approach to find 'members = [...]'
|
|
325
|
-
import re
|
|
326
|
-
members_pattern = r'members\s*=\s*\[(.*?)\]'
|
|
327
|
-
match = re.search(members_pattern, content, re.DOTALL)
|
|
328
|
-
|
|
329
|
-
if match:
|
|
330
|
-
current_members = match.group(1)
|
|
331
|
-
# Check if already present
|
|
332
|
-
if f'"{self.raw_name}"' in current_members or f"'{self.raw_name}'" in current_members:
|
|
333
|
-
self.log(f"Package {self.raw_name} is already in workspace members.")
|
|
334
|
-
else:
|
|
335
|
-
# Append new member
|
|
336
|
-
# We inject it into the list
|
|
337
|
-
self.log("Adding member to existing workspace list")
|
|
338
|
-
# Naively replace the closing bracket
|
|
339
|
-
# Better: parse, but for now robust string insertion
|
|
340
|
-
# Cleanest way without breaking formatting involves finding the last element
|
|
341
|
-
# Cleanest way without breaking formatting involves finding the last element
|
|
342
|
-
new_member = f', "src/{self.raw_name}"'
|
|
343
|
-
# Warning: This regex replace is basic. `uv` handles toml well, maybe we should just edit safely.
|
|
344
|
-
# Let's try to append to the end of the content of the list
|
|
345
|
-
new_content = re.sub(members_pattern, lambda m: f'members = [{m.group(1)}{new_member}]', content, flags=re.DOTALL)
|
|
346
|
-
with open(pyproject_path, "w") as f:
|
|
347
|
-
f.write(new_content)
|
|
348
|
-
else:
|
|
349
|
-
# Section exists but members key might be missing? Or weird formatting.
|
|
350
|
-
# Append to section?
|
|
351
|
-
# Safe fallback
|
|
352
|
-
console.print("[yellow]Warning: Could not parse members list. Adding manually at end.[/yellow]")
|
|
353
|
-
with open(pyproject_path, "a") as f:
|
|
354
|
-
f.write(f"\n# Added by viperx\n[tool.uv.workspace]\nmembers = [\"{self.raw_name}\"]\n")
|
|
335
|
+
# User Request: Do not modify root pyproject.toml to add workspace members.
|
|
336
|
+
# "uv est assez intelligent"
|
|
337
|
+
pass
|
|
355
338
|
|
|
356
339
|
# Generate the package in the root IF it doesn't exist
|
|
357
340
|
pkg_dir = workspace_root / SRC_DIR / self.raw_name
|
viperx/main.py
CHANGED
|
@@ -152,6 +152,13 @@ def config_main(
|
|
|
152
152
|
|
|
153
153
|
console.print(Panel(f"Initializing [bold blue]{name}[/bold blue]", border_style="blue"))
|
|
154
154
|
|
|
155
|
+
# Check if target directory (sanitized) exists
|
|
156
|
+
from viperx.utils import sanitize_project_name
|
|
157
|
+
name_clean = sanitize_project_name(name)
|
|
158
|
+
target_dir = Path.cwd() / name_clean
|
|
159
|
+
if target_dir.exists():
|
|
160
|
+
console.print(f"[bold yellow]Warning:[/bold yellow] Directory {name_clean} already exists. Updating.")
|
|
161
|
+
|
|
155
162
|
generator = ProjectGenerator(
|
|
156
163
|
name=name,
|
|
157
164
|
description=description,
|
viperx/templates/README.md.j2
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
## 🧐 Philosophy & Architecture
|
|
8
8
|
|
|
9
9
|
Values transparency and standard tooling over "black box" magic.
|
|
10
|
-
This project was generated with [ViperX](https://github.com/kpihx/viperx).sh/uv/)**, the extremely fast Python package and project manager written in Rust.
|
|
10
|
+
This project was generated with [ViperX](https://github.com/kpihx/viperx), using **[uv](https://docs.astral.sh/uv/)**, the extremely fast Python package and project manager written in Rust.
|
|
11
11
|
|
|
12
12
|
### Why `uv`?
|
|
13
13
|
Unlike traditional workflows (pip, poetry, venv mixing), `uv` manages the **entire lifecycle**:
|
|
@@ -18,17 +18,25 @@ Unlike traditional workflows (pip, poetry, venv mixing), `uv` manages the **enti
|
|
|
18
18
|
{% endif %}
|
|
19
19
|
- **Environment**: Virtual environments are managed internally, you just run `uv run`.
|
|
20
20
|
|
|
21
|
+
{% if has_config or use_env %}
|
|
21
22
|
### ⚙️ Configuration
|
|
22
23
|
|
|
23
|
-
- **Config**: `src/{{ package_name }}/config.yaml` (Loaded automatically)
|
|
24
24
|
{% if has_config %}
|
|
25
|
+
- **Config**: `src/{{ package_name }}/config.yaml` (Loaded automatically)
|
|
26
|
+
{% endif %}
|
|
27
|
+
{% if use_env %}
|
|
25
28
|
- **Environment**: `src/{{ package_name }}/.env` (Isolated variables)
|
|
26
29
|
{% endif %}
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
The project uses a **Config-in-Package** architecture:
|
|
32
|
+
{% if has_config %}
|
|
29
33
|
1. `config.yaml` is inside the package.
|
|
30
34
|
2. `config.py` loads it safely (even in production wheels).
|
|
35
|
+
{% endif %}
|
|
36
|
+
{% if use_env %}
|
|
31
37
|
3. `.env` is isolated within the package source.
|
|
38
|
+
{% endif %}
|
|
39
|
+
{% endif %}
|
|
32
40
|
|
|
33
41
|
---
|
|
34
42
|
|
|
@@ -42,8 +50,7 @@ No need to install Python or create venvs manually.
|
|
|
42
50
|
### Installation
|
|
43
51
|
|
|
44
52
|
```bash
|
|
45
|
-
#
|
|
46
|
-
git clone https://github.com/{{ author_name | lower | replace(" ", "") }}/{{ project_name }}.git
|
|
53
|
+
# Ensure you are in the project directory
|
|
47
54
|
cd {{ project_name }}
|
|
48
55
|
|
|
49
56
|
# Sync dependencies (creates .venv and installs python if needed)
|
|
@@ -52,15 +59,13 @@ uv sync
|
|
|
52
59
|
|
|
53
60
|
## 🧑💻 Usage
|
|
54
61
|
|
|
55
|
-
The entry point depends on your project type.
|
|
56
|
-
|
|
57
62
|
### For Developers (Code)
|
|
58
63
|
|
|
59
64
|
To run the package entry point or scripts:
|
|
60
65
|
|
|
61
66
|
```bash
|
|
62
67
|
# Run the main package
|
|
63
|
-
uv run {{
|
|
68
|
+
uv run {{ project_name }}
|
|
64
69
|
|
|
65
70
|
# Or run a specific script
|
|
66
71
|
uv run python src/{{ package_name }}/main.py
|
|
@@ -77,29 +82,36 @@ uv run jupyter notebook
|
|
|
77
82
|
|
|
78
83
|
- Open `notebooks/Base.ipynb`.
|
|
79
84
|
- Note how it imports `config` from the package.
|
|
80
|
-
{% endif %}
|
|
81
85
|
|
|
82
86
|
### ☁️ Cloud (Colab / Kaggle)
|
|
83
87
|
|
|
84
88
|
You can use the code and config from this repository directly in cloud environments without cloning.
|
|
85
89
|
|
|
86
|
-
**Step 1: Install directly from
|
|
90
|
+
**Step 1: Install directly from Git**
|
|
87
91
|
```python
|
|
88
|
-
!pip install git+https://
|
|
92
|
+
!pip install git+https://your-git-provider.com/username/repo.git
|
|
89
93
|
```
|
|
90
94
|
|
|
91
95
|
**Step 2: Use the unified config**
|
|
92
96
|
```python
|
|
97
|
+
{% if project_type == 'classic' %}
|
|
98
|
+
from {{ package_name }} import SETTINGS
|
|
99
|
+
{% else %}
|
|
93
100
|
from {{ package_name }} import get_dataset_path, SETTINGS
|
|
94
101
|
import kagglehub as kh
|
|
102
|
+
{% endif %}
|
|
95
103
|
|
|
96
104
|
# Transparency: You can inspect what was loaded
|
|
97
|
-
print(f"Loaded config for: {SETTINGS
|
|
105
|
+
print(f"Loaded config for: {SETTINGS.get('project_name', 'Unknown')}")
|
|
98
106
|
|
|
107
|
+
{% if project_type != 'classic' %}
|
|
99
108
|
# Download datasets defined in config.yaml
|
|
100
109
|
# The key 'titanic' maps to 'heptapod/titanic' in the yaml
|
|
101
|
-
|
|
110
|
+
if 'datasets' in SETTINGS and 'titanic' in SETTINGS['datasets']:
|
|
111
|
+
path = kh.dataset_download(SETTINGS['datasets']['titanic'])
|
|
112
|
+
{% endif %}
|
|
102
113
|
```
|
|
114
|
+
{% endif %}
|
|
103
115
|
|
|
104
116
|
## 🔧 Internal Structure
|
|
105
117
|
|
|
@@ -109,14 +121,22 @@ path = kh.dataset_download(SETTINGS['datasets']['titanic'])
|
|
|
109
121
|
├── uv.lock # Exact versions lockfile
|
|
110
122
|
├── .python-version # Pinned Python version
|
|
111
123
|
├── src/
|
|
112
|
-
|
|
124
|
+
{% for name, entry in scripts.items() %}
|
|
125
|
+
{% set pkg_dir = entry.split('.')[0] %}
|
|
126
|
+
│ └── {{ pkg_dir }}/
|
|
113
127
|
│ ├── __init__.py
|
|
128
|
+
{% if has_config %}
|
|
114
129
|
│ ├── config.yaml # EDIT THIS for project settings
|
|
115
130
|
│ ├── config.py # Code that loads the yaml above
|
|
131
|
+
{% endif %}
|
|
132
|
+
{% if use_tests %}
|
|
116
133
|
│ └── tests/ # Unit tests
|
|
134
|
+
{% endif %}
|
|
135
|
+
{% endfor %}
|
|
117
136
|
{% if project_type in ['ml', 'dl'] %}
|
|
118
137
|
└── notebooks/ # Experimentation (Jupyter)
|
|
119
138
|
{% endif %}
|
|
120
139
|
```
|
|
121
140
|
|
|
122
141
|
|
|
142
|
+
|
viperx/templates/__init__.py.j2
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{% if has_config %}
|
|
2
|
-
from .config import SETTINGS, get_config
|
|
2
|
+
from .config import SETTINGS, get_config
|
|
3
|
+
{% if project_type != 'classic' %}
|
|
4
|
+
from .config import get_dataset_path
|
|
5
|
+
{% endif %}
|
|
3
6
|
{% endif %}
|
|
4
7
|
|
|
5
|
-
__version__ = "{{ version }}"
|
|
6
8
|
|
|
7
|
-
def hello():
|
|
8
|
-
print(f"Hello from {{ project_name }} v{__version__}!")
|
viperx/templates/config.yaml.j2
CHANGED
viperx/templates/main.py.j2
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
{% if has_config %}
|
|
1
|
+
{%- if has_config %}
|
|
3
2
|
from {{ package_name }} import SETTINGS
|
|
4
|
-
{
|
|
3
|
+
{%- endif %}
|
|
5
4
|
|
|
6
5
|
def main():
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
{%- if has_config %}
|
|
7
|
+
print(f"Hi from {SETTINGS['project_name']}!")
|
|
8
|
+
{%- else %}
|
|
9
|
+
print("Hi from viperx!")
|
|
10
|
+
{%- endif %}
|
|
11
11
|
|
|
12
12
|
if __name__ == "__main__":
|
|
13
13
|
main()
|
|
@@ -11,10 +11,14 @@ authors = [
|
|
|
11
11
|
]
|
|
12
12
|
license = { text = "{{ license }}" }
|
|
13
13
|
dependencies = [
|
|
14
|
+
{%- if has_config %}
|
|
14
15
|
"pyyaml>=6.0",
|
|
16
|
+
{%- endif %}
|
|
17
|
+
{%- if has_env %}
|
|
15
18
|
"python-dotenv>=1.0.0",
|
|
19
|
+
{%- endif %}
|
|
20
|
+
{%- if is_ml_dl %}
|
|
16
21
|
"kagglehub>=0.2.0",
|
|
17
|
-
{% if project_type == 'ml' %}
|
|
18
22
|
"numpy>=1.24.0",
|
|
19
23
|
"pandas>=2.0.0",
|
|
20
24
|
"scikit-learn>=1.3.0",
|
|
@@ -22,35 +26,32 @@ dependencies = [
|
|
|
22
26
|
"seaborn>=0.12.0",
|
|
23
27
|
"requests>=2.30.0",
|
|
24
28
|
"tqdm>=4.65.0",
|
|
25
|
-
{
|
|
26
|
-
{
|
|
29
|
+
{%- endif %}
|
|
30
|
+
{%- if is_dl %}
|
|
31
|
+
{%- if 'pytorch' in frameworks %}
|
|
27
32
|
"torch>=2.0.0",
|
|
28
33
|
"torchvision>=0.15.0",
|
|
29
|
-
{
|
|
34
|
+
{%- endif %}
|
|
35
|
+
{%- if 'tensorflow' in frameworks %}
|
|
30
36
|
"tensorflow>=2.13.0",
|
|
31
37
|
# "keras>=3.0.0", # Optional, included in tf usually
|
|
32
|
-
{
|
|
33
|
-
|
|
34
|
-
"pandas>=2.0.0",
|
|
35
|
-
"matplotlib>=3.7.0",
|
|
36
|
-
"seaborn>=0.12.0",
|
|
37
|
-
"requests>=2.30.0",
|
|
38
|
-
"tqdm>=4.65.0",
|
|
39
|
-
{% endif %}
|
|
38
|
+
{%- endif %}
|
|
39
|
+
{%- endif %}
|
|
40
40
|
]
|
|
41
41
|
|
|
42
|
+
[project.scripts]
|
|
43
|
+
{%- for name, entry in scripts.items() %}
|
|
44
|
+
{{ name }} = "{{ entry }}"
|
|
45
|
+
{%- endfor %}
|
|
46
|
+
|
|
42
47
|
[build-system]
|
|
43
|
-
{
|
|
48
|
+
{%- if builder == 'hatch' %}
|
|
44
49
|
requires = ["hatchling"]
|
|
45
50
|
build-backend = "hatchling.build"
|
|
46
|
-
{
|
|
51
|
+
{%- else %}
|
|
47
52
|
# Default: uv native build backend
|
|
48
53
|
requires = ["uv_build>=0.9.21,<0.10.0"]
|
|
49
54
|
build-backend = "uv_build"
|
|
50
|
-
{
|
|
55
|
+
{%- endif %}
|
|
51
56
|
|
|
52
|
-
{% if use_uv %}
|
|
53
|
-
[tool.uv]
|
|
54
|
-
managed = true
|
|
55
|
-
{% endif %}
|
|
56
57
|
|
viperx/utils.py
CHANGED
|
@@ -31,11 +31,27 @@ def validate_project_name(ctx, param, value):
|
|
|
31
31
|
raise BadParameter("Project name must contain only letters, numbers, underscores, and hyphens.")
|
|
32
32
|
return value
|
|
33
33
|
|
|
34
|
+
from viperx.constants import SUPPORTED_BUILDERS
|
|
35
|
+
|
|
36
|
+
def validate_choice(value: str, choices: list[str], name: str):
|
|
37
|
+
"""
|
|
38
|
+
Validate that a value is within the allowed choices.
|
|
39
|
+
Raises ValueError with a friendly message if invalid.
|
|
40
|
+
"""
|
|
41
|
+
if value not in choices:
|
|
42
|
+
raise ValueError(f"Invalid {name} '{value}'. Allowed: {', '.join(choices)}")
|
|
43
|
+
return value
|
|
44
|
+
|
|
34
45
|
def check_builder_installed(builder: str) -> bool:
|
|
35
46
|
"""
|
|
36
|
-
Check if the specified builder is installed.
|
|
47
|
+
Check if the specified builder is valid AND installed.
|
|
37
48
|
Uses shutil.which() for robust path detection.
|
|
38
49
|
"""
|
|
50
|
+
# 1. Validate against supported list
|
|
51
|
+
if builder not in SUPPORTED_BUILDERS:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
# 2. Check existence
|
|
39
55
|
return shutil.which(builder) is not None
|
|
40
56
|
|
|
41
57
|
def get_author_from_git() -> tuple[str, str]:
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
viperx/__init__.py,sha256=mRrD-SK9PtwBhqgLzJrvefdViNsXCyBGknzFNsIou1I,51
|
|
2
|
+
viperx/config_engine.py,sha256=zeCV5toxk4mfafbw61UafBK_WGBNgU1Qf93-dTzwyP8,10034
|
|
3
|
+
viperx/constants.py,sha256=Mv5tFjFxe3tD4VDCEsXiwfiV7biTlzl45ALkHmiK33M,887
|
|
4
|
+
viperx/core.py,sha256=QDgSsYGzphUmzUa3PV-nY6Dl6unfhzeywIMZsAnDzjM,19722
|
|
5
|
+
viperx/licenses.py,sha256=TPsG1aqNAjtOsvuD6i3SiC5rUK9f3DslPHQkKiV7vxs,12482
|
|
6
|
+
viperx/main.py,sha256=-k1_vGk-3DCayF1_gGdFthFR_FPWWb2VZz-VBB8y6qc,10996
|
|
7
|
+
viperx/templates/Base.ipynb.j2,sha256=rlGCw7jIds3RiXFrG8D8oAjzXzzj1cayKjBI91k-1kA,3140
|
|
8
|
+
viperx/templates/Base_General.ipynb.j2,sha256=f5ZUu65khtvBeCofs3Lvzccn5Krl0nRGu0dv7SzLvzg,2716
|
|
9
|
+
viperx/templates/Base_Kaggle.ipynb.j2,sha256=gPkmFt4cA5UzhRU_gLZ1UzSjEa7Mgel001Eqiw94-fc,2698
|
|
10
|
+
viperx/templates/README.md.j2,sha256=s5HBkx6PEZTv9gsBlmYRJnH4jAuiWJ6qSyaoFVCR_PU,3792
|
|
11
|
+
viperx/templates/__init__.py.j2,sha256=NNzSYZTYzbohNf6zL-u3_WgmiUspuejoS7nLCafB7w8,159
|
|
12
|
+
viperx/templates/config.py.j2,sha256=ExMbopPQPntmkNgbv9uKooGDjv82gh_XhyQmjJ8MiQg,1456
|
|
13
|
+
viperx/templates/config.yaml.j2,sha256=EyeUfIIGvNIyMSHayh4i_0XVxIeu4Wdf2I6uAgfmSf4,552
|
|
14
|
+
viperx/templates/data_loader.py.j2,sha256=hehewwvBAYJM-acKn6_T4cU5EvUPX7wHAurAQ3z3ET4,3696
|
|
15
|
+
viperx/templates/main.py.j2,sha256=v4rukrrLXYgyMt5We02_QU3eNSrKFh-uafVReh-2RKw,263
|
|
16
|
+
viperx/templates/pyproject.toml.j2,sha256=NGrgIJM_pzjbQ3OAgsT_y3mX2n8UzdAg_mo9AERhGmU,1297
|
|
17
|
+
viperx/templates/viperx_config.yaml.j2,sha256=z-teOb1hTYmoVdXBQqgr6kK5fQGkMpI6KsTzbeF0m4Q,1408
|
|
18
|
+
viperx/utils.py,sha256=c9FKL9eRsszNvinQd3zjvA2bRMVhEQI5F26aLN_PPZE,2578
|
|
19
|
+
viperx-0.9.50.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
|
|
20
|
+
viperx-0.9.50.dist-info/entry_points.txt,sha256=Is38BrTuf6unQCLgDE990Rjd9HyGldBSw4Uzy_nMePY,44
|
|
21
|
+
viperx-0.9.50.dist-info/METADATA,sha256=t-3Q_mNDXVdcZCLJVSF3E4JPc5oETU9INZwgTfP6pBc,6782
|
|
22
|
+
viperx-0.9.50.dist-info/RECORD,,
|
viperx-0.9.26.dist-info/RECORD
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
viperx/__init__.py,sha256=mRrD-SK9PtwBhqgLzJrvefdViNsXCyBGknzFNsIou1I,51
|
|
2
|
-
viperx/config_engine.py,sha256=9P9xxWIYWhL_QFRyvi601D3_4HBHn4RA20dreGwdSX4,7341
|
|
3
|
-
viperx/constants.py,sha256=Mv5tFjFxe3tD4VDCEsXiwfiV7biTlzl45ALkHmiK33M,887
|
|
4
|
-
viperx/core.py,sha256=R0IzxNTL_Coyz1bCTc2QAnQM3FK36ADV86NVQXuXmBM,20438
|
|
5
|
-
viperx/licenses.py,sha256=TPsG1aqNAjtOsvuD6i3SiC5rUK9f3DslPHQkKiV7vxs,12482
|
|
6
|
-
viperx/main.py,sha256=N0pqLnZoWr00XjH1kS5dxbDKEqXvazsVspqLQ_-fF8k,10663
|
|
7
|
-
viperx/templates/Base.ipynb.j2,sha256=rlGCw7jIds3RiXFrG8D8oAjzXzzj1cayKjBI91k-1kA,3140
|
|
8
|
-
viperx/templates/Base_General.ipynb.j2,sha256=f5ZUu65khtvBeCofs3Lvzccn5Krl0nRGu0dv7SzLvzg,2716
|
|
9
|
-
viperx/templates/Base_Kaggle.ipynb.j2,sha256=gPkmFt4cA5UzhRU_gLZ1UzSjEa7Mgel001Eqiw94-fc,2698
|
|
10
|
-
viperx/templates/README.md.j2,sha256=TJNUwTcKnjALrP0MOSs8UBx8zNhI8erRHrc2-8nuMgQ,3445
|
|
11
|
-
viperx/templates/__init__.py.j2,sha256=uq8IYd0SGYX2-cpGb9q5197eYs-ode-g5y2NCGWU5YM,196
|
|
12
|
-
viperx/templates/config.py.j2,sha256=ExMbopPQPntmkNgbv9uKooGDjv82gh_XhyQmjJ8MiQg,1456
|
|
13
|
-
viperx/templates/config.yaml.j2,sha256=IYvjQUioZ9lyzVHVDgwrdjgM6s4pUnCFXwtnydoKe2k,485
|
|
14
|
-
viperx/templates/data_loader.py.j2,sha256=hehewwvBAYJM-acKn6_T4cU5EvUPX7wHAurAQ3z3ET4,3696
|
|
15
|
-
viperx/templates/main.py.j2,sha256=caGN54Hck_EcwT3Aqju52CBuGujSnzU2Hehw8mowzjI,277
|
|
16
|
-
viperx/templates/pyproject.toml.j2,sha256=9s3wbXZjoA_643feZAlS5-7YMQLxVUd6XLPhDusyuJY,1287
|
|
17
|
-
viperx/templates/viperx_config.yaml.j2,sha256=a2H0N_W8sq0Zrjio4El7kFjOYQUATf_VdD9zK3NzG3U,1405
|
|
18
|
-
viperx/utils.py,sha256=yCJMclJT10_Vc8A1dx_lP0OSs7crCnVNLP46SCpxmog,2054
|
|
19
|
-
viperx-0.9.26.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
|
|
20
|
-
viperx-0.9.26.dist-info/entry_points.txt,sha256=Is38BrTuf6unQCLgDE990Rjd9HyGldBSw4Uzy_nMePY,44
|
|
21
|
-
viperx-0.9.26.dist-info/METADATA,sha256=GuapQRE5fpVYvo6_5i54EI8uA1AplAIAAsEaSiBTty4,6782
|
|
22
|
-
viperx-0.9.26.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|