viperx 0.9.14__py3-none-any.whl → 0.9.40__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 +53 -4
- viperx/constants.py +5 -0
- viperx/core.py +105 -69
- viperx/main.py +54 -32
- viperx/templates/__init__.py.j2 +0 -3
- viperx/templates/config.py.j2 +2 -0
- viperx/templates/config.yaml.j2 +3 -0
- viperx/templates/main.py.j2 +3 -3
- viperx/templates/pyproject.toml.j2 +23 -25
- viperx/templates/viperx_config.yaml.j2 +2 -1
- viperx/utils.py +37 -6
- {viperx-0.9.14.dist-info → viperx-0.9.40.dist-info}/METADATA +1 -1
- viperx-0.9.40.dist-info/RECORD +22 -0
- viperx-0.9.14.dist-info/RECORD +0 -22
- {viperx-0.9.14.dist-info → viperx-0.9.40.dist-info}/WHEEL +0 -0
- {viperx-0.9.14.dist-info → viperx-0.9.40.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
|
|
|
@@ -69,6 +69,7 @@ class ConfigEngine:
|
|
|
69
69
|
use_config=settings_conf.get("use_config", True),
|
|
70
70
|
use_tests=settings_conf.get("use_tests", True),
|
|
71
71
|
framework=settings_conf.get("framework", FRAMEWORK_PYTORCH),
|
|
72
|
+
scripts={project_name: f"{project_name}.main:main"}, # Simple default for hydration
|
|
72
73
|
verbose=self.verbose
|
|
73
74
|
)
|
|
74
75
|
# generate() expects parent dir, and will operate on parent/name (which is self.root_path)
|
|
@@ -84,9 +85,55 @@ class ConfigEngine:
|
|
|
84
85
|
else:
|
|
85
86
|
if target_dir.exists():
|
|
86
87
|
console.print(Panel(f"⚠️ [bold yellow]Directory exists but not initialized. Hydrating:[/bold yellow] {project_name}", border_style="yellow"))
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
# Prepare Scripts & Dependency Context
|
|
89
|
+
packages = workspace_conf.get("packages", [])
|
|
89
90
|
|
|
91
|
+
# --- Aggregate Global Dependencies ---
|
|
92
|
+
# Start with Root Settings
|
|
93
|
+
# Root is always present (ProjectGenerator uses these)
|
|
94
|
+
root_use_config = settings_conf.get("use_config", True)
|
|
95
|
+
root_use_env = settings_conf.get("use_env", False)
|
|
96
|
+
root_type = settings_conf.get("type", TYPE_CLASSIC)
|
|
97
|
+
root_framework = settings_conf.get("framework", FRAMEWORK_PYTORCH)
|
|
98
|
+
|
|
99
|
+
glob_has_config = root_use_config
|
|
100
|
+
glob_has_env = root_use_env
|
|
101
|
+
glob_is_ml_dl = root_type in [TYPE_ML, TYPE_DL]
|
|
102
|
+
glob_is_dl = root_type == TYPE_DL
|
|
103
|
+
glob_frameworks = {root_framework} if glob_is_dl else set()
|
|
104
|
+
|
|
105
|
+
project_scripts = {project_name: f"{project_name}.main:main"}
|
|
106
|
+
|
|
107
|
+
for pkg in packages:
|
|
108
|
+
# Scripts
|
|
109
|
+
pkg_name = pkg.get("name")
|
|
110
|
+
from viperx.utils import sanitize_project_name
|
|
111
|
+
pkg_name_clean = sanitize_project_name(pkg_name)
|
|
112
|
+
# CLI Command = Raw Name (e.g. test-classic) -> sanitized module path (test_classic.main:main)
|
|
113
|
+
project_scripts[pkg_name] = f"{pkg_name_clean}.main:main"
|
|
114
|
+
|
|
115
|
+
# Dependency Aggregation
|
|
116
|
+
# Inherit defaults if not defined in pkg
|
|
117
|
+
p_config = pkg.get("use_config", settings_conf.get("use_config", True))
|
|
118
|
+
p_env = pkg.get("use_env", settings_conf.get("use_env", False))
|
|
119
|
+
p_type = pkg.get("type", TYPE_CLASSIC)
|
|
120
|
+
p_framework = pkg.get("framework", FRAMEWORK_PYTORCH) # Defaults to pytorch if implicit
|
|
121
|
+
|
|
122
|
+
if p_config: glob_has_config = True
|
|
123
|
+
if p_env: glob_has_env = True
|
|
124
|
+
if p_type in [TYPE_ML, TYPE_DL]: glob_is_ml_dl = True
|
|
125
|
+
if p_type == TYPE_DL:
|
|
126
|
+
glob_is_dl = True
|
|
127
|
+
glob_frameworks.add(p_framework)
|
|
128
|
+
|
|
129
|
+
dep_context = {
|
|
130
|
+
"has_config": glob_has_config,
|
|
131
|
+
"has_env": glob_has_env,
|
|
132
|
+
"is_ml_dl": glob_is_ml_dl,
|
|
133
|
+
"is_dl": glob_is_dl,
|
|
134
|
+
"frameworks": list(glob_frameworks)
|
|
135
|
+
}
|
|
136
|
+
|
|
90
137
|
# Create Root (or Hydrate)
|
|
91
138
|
gen = ProjectGenerator(
|
|
92
139
|
name=project_name,
|
|
@@ -99,6 +146,8 @@ class ConfigEngine:
|
|
|
99
146
|
use_config=settings_conf.get("use_config", True),
|
|
100
147
|
use_tests=settings_conf.get("use_tests", True),
|
|
101
148
|
framework=settings_conf.get("framework", FRAMEWORK_PYTORCH),
|
|
149
|
+
scripts=project_scripts,
|
|
150
|
+
dependency_context=dep_context,
|
|
102
151
|
verbose=self.verbose
|
|
103
152
|
)
|
|
104
153
|
gen.generate(self.root_path)
|
|
@@ -129,7 +178,7 @@ class ConfigEngine:
|
|
|
129
178
|
author=project_conf.get("author", "Your Name"), # Inherit author
|
|
130
179
|
use_env=pkg.get("use_env", settings_conf.get("use_env", False)), # Inherit settings or default False
|
|
131
180
|
use_config=pkg.get("use_config", settings_conf.get("use_config", True)), # Inherit or default True
|
|
132
|
-
use_readme=pkg.get("use_readme",
|
|
181
|
+
use_readme=pkg.get("use_readme", False),
|
|
133
182
|
use_tests=pkg.get("use_tests", settings_conf.get("use_tests", True)),
|
|
134
183
|
framework=pkg.get("framework", FRAMEWORK_PYTORCH),
|
|
135
184
|
verbose=self.verbose
|
viperx/constants.py
CHANGED
|
@@ -33,3 +33,8 @@ PROJECT_TYPES = [TYPE_CLASSIC, TYPE_ML, TYPE_DL]
|
|
|
33
33
|
FRAMEWORK_PYTORCH = "pytorch"
|
|
34
34
|
FRAMEWORK_TENSORFLOW = "tensorflow"
|
|
35
35
|
DL_FRAMEWORKS = [FRAMEWORK_PYTORCH, FRAMEWORK_TENSORFLOW]
|
|
36
|
+
|
|
37
|
+
# Builders
|
|
38
|
+
BUILDER_UV = "uv"
|
|
39
|
+
BUILDER_HATCH = "hatch"
|
|
40
|
+
SUPPORTED_BUILDERS = [BUILDER_UV, BUILDER_HATCH]
|
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,9 +74,39 @@ 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"),
|
|
@@ -81,7 +131,8 @@ class ProjectGenerator:
|
|
|
81
131
|
console.print(f"[bold green]Creating project {self.raw_name} ({self.type})...[/bold green]")
|
|
82
132
|
|
|
83
133
|
self.log(f"Target directory: {project_dir}")
|
|
84
|
-
self.log(f"Python
|
|
134
|
+
self.log(f"System Python: {self.system_python_version}")
|
|
135
|
+
self.log(f"Project Python: {self.python_version}")
|
|
85
136
|
|
|
86
137
|
|
|
87
138
|
def generate(self, target_dir: Path, is_subpackage: bool = False):
|
|
@@ -102,8 +153,13 @@ class ProjectGenerator:
|
|
|
102
153
|
)
|
|
103
154
|
else:
|
|
104
155
|
# Create new
|
|
156
|
+
# STRICT DIR NAMING: Use self.project_name (underscores) for directory
|
|
157
|
+
# But use self.raw_name (hyphens) for the package Name metadata if possible?
|
|
158
|
+
# uv init [NAME] creates directory NAME.
|
|
159
|
+
# If we want dir=test_classic but name=test-classic:
|
|
160
|
+
# uv init test_classic --name test-classic
|
|
105
161
|
subprocess.run(
|
|
106
|
-
["uv", "init", "--package", "--no-workspace", self.raw_name],
|
|
162
|
+
["uv", "init", "--package", "--no-workspace", self.project_name, "--name", self.raw_name],
|
|
107
163
|
check=True, cwd=target_dir, capture_output=True
|
|
108
164
|
)
|
|
109
165
|
console.print(" [blue]✓ Scaffolding created with uv init[/blue]")
|
|
@@ -136,10 +192,17 @@ class ProjectGenerator:
|
|
|
136
192
|
# 4. Overwrite/Add Files
|
|
137
193
|
self._generate_files(project_dir, is_subpackage)
|
|
138
194
|
|
|
195
|
+
|
|
196
|
+
# Cleanup extra files for subpackages
|
|
197
|
+
if is_subpackage:
|
|
198
|
+
for f in [".gitignore", ".python-version"]:
|
|
199
|
+
if (project_dir / f).exists():
|
|
200
|
+
(project_dir / f).unlink()
|
|
201
|
+
|
|
139
202
|
# 5. Git & Final Steps
|
|
140
203
|
console.print(f"\n[bold green]✓ Project {self.raw_name} created successfully![/bold green]")
|
|
141
204
|
if not is_subpackage:
|
|
142
|
-
console.print(f" [dim]cd {self.
|
|
205
|
+
console.print(f" [dim]cd {self.project_name} && uv sync[/dim]")
|
|
143
206
|
|
|
144
207
|
def _create_extra_dirs(self, root: Path, is_subpackage: bool = False):
|
|
145
208
|
if is_subpackage:
|
|
@@ -186,10 +249,15 @@ class ProjectGenerator:
|
|
|
186
249
|
"has_config": self.use_config,
|
|
187
250
|
"use_readme": self.use_readme,
|
|
188
251
|
"use_env": self.use_env,
|
|
252
|
+
"use_env": self.use_env,
|
|
189
253
|
"framework": self.framework,
|
|
254
|
+
"scripts": self.scripts,
|
|
190
255
|
}
|
|
256
|
+
# Merge dependency context overrides
|
|
257
|
+
context.update(self.dependency_context)
|
|
191
258
|
|
|
192
259
|
# pyproject.toml (Overwrite uv's basic one to add our specific deps)
|
|
260
|
+
# Even subpackages need this if they are Workspace Members (which they are in our model)
|
|
193
261
|
self._render("pyproject.toml.j2", root / "pyproject.toml", context)
|
|
194
262
|
|
|
195
263
|
# Determine Package Root
|
|
@@ -210,19 +278,33 @@ class ProjectGenerator:
|
|
|
210
278
|
self._render("__init__.py.j2", pkg_root / "__init__.py", context)
|
|
211
279
|
|
|
212
280
|
# README.md
|
|
213
|
-
|
|
214
|
-
|
|
281
|
+
# README.md
|
|
282
|
+
if not is_subpackage:
|
|
283
|
+
# Root Project: Respect use_readme
|
|
284
|
+
if self.use_readme:
|
|
285
|
+
self._render("README.md.j2", root / "README.md", context)
|
|
286
|
+
else:
|
|
287
|
+
if (root / "README.md").exists():
|
|
288
|
+
(root / "README.md").unlink()
|
|
289
|
+
self.log("Removed default README.md (requested --no-readme)")
|
|
215
290
|
else:
|
|
216
|
-
if
|
|
291
|
+
# Subpackage: Default False, but if True, generate it
|
|
292
|
+
if self.use_readme:
|
|
293
|
+
self._render("README.md.j2", root / "README.md", context)
|
|
294
|
+
elif (root / "README.md").exists():
|
|
295
|
+
# Cleanup default README from uv init if we didn't request one
|
|
217
296
|
(root / "README.md").unlink()
|
|
218
|
-
|
|
297
|
+
|
|
219
298
|
|
|
220
299
|
# LICENSE
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
300
|
+
if not is_subpackage:
|
|
301
|
+
license_text = LICENSES.get(self.license, LICENSES["MIT"])
|
|
302
|
+
license_text = license_text.format(year=datetime.now().year, author=self.author)
|
|
303
|
+
with open(root / "LICENSE", "w") as f:
|
|
304
|
+
f.write(license_text)
|
|
305
|
+
self.log(f"Generated LICENSE ({self.license})")
|
|
306
|
+
elif (root / "LICENSE").exists():
|
|
307
|
+
(root / "LICENSE").unlink()
|
|
226
308
|
|
|
227
309
|
# Config files
|
|
228
310
|
if self.use_config:
|
|
@@ -249,10 +331,12 @@ class ProjectGenerator:
|
|
|
249
331
|
self.log(f"Created .env and .env.example in {pkg_root.relative_to(root)}")
|
|
250
332
|
|
|
251
333
|
# .gitignore
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
334
|
+
# Only for Root
|
|
335
|
+
if not is_subpackage:
|
|
336
|
+
with open(root / ".gitignore", "a") as f:
|
|
337
|
+
# Add data/ to gitignore but allow .gitkeep
|
|
338
|
+
f.write("\n# ViperX specific\n.ipynb_checkpoints/\n# Isolated Env\nsrc/**/.env\n# Data (Local)\ndata/*\n!data/.gitkeep\n")
|
|
339
|
+
self.log("Updated .gitignore")
|
|
256
340
|
|
|
257
341
|
def _render(self, template_name: str, target_path: Path, context: dict):
|
|
258
342
|
template = self.env.get_template(template_name)
|
|
@@ -265,57 +349,9 @@ class ProjectGenerator:
|
|
|
265
349
|
"""Add a new package to an existing workspace."""
|
|
266
350
|
console.print(f"[bold green]Adding package {self.raw_name} to workspace...[/bold green]")
|
|
267
351
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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")
|
|
352
|
+
# User Request: Do not modify root pyproject.toml to add workspace members.
|
|
353
|
+
# "uv est assez intelligent"
|
|
354
|
+
pass
|
|
319
355
|
|
|
320
356
|
# Generate the package in the root IF it doesn't exist
|
|
321
357
|
pkg_dir = workspace_root / SRC_DIR / self.raw_name
|
viperx/main.py
CHANGED
|
@@ -14,9 +14,14 @@ from viperx.constants import (
|
|
|
14
14
|
DL_FRAMEWORKS,
|
|
15
15
|
FRAMEWORK_PYTORCH,
|
|
16
16
|
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
import importlib.metadata
|
|
18
|
+
try:
|
|
19
|
+
version = importlib.metadata.version("viperx")
|
|
20
|
+
except importlib.metadata.PackageNotFoundError:
|
|
21
|
+
version = "unknown"
|
|
22
|
+
|
|
23
|
+
HELP_TEXT = f"""
|
|
24
|
+
[bold green]ViperX[/bold green] (v{version}): Professional Python Project Initializer
|
|
20
25
|
.
|
|
21
26
|
|
|
22
27
|
Automates the creation of professional-grade Python projects using `uv`.
|
|
@@ -28,31 +33,15 @@ app = typer.Typer(
|
|
|
28
33
|
add_completion=False,
|
|
29
34
|
no_args_is_help=True,
|
|
30
35
|
rich_markup_mode="markdown",
|
|
31
|
-
epilog="Made with ❤️
|
|
36
|
+
epilog="Made with ❤️ by KpihX"
|
|
32
37
|
)
|
|
33
38
|
|
|
34
39
|
# Global state for verbose flag
|
|
35
40
|
state = {"verbose": False}
|
|
36
41
|
console = Console(force_terminal=True)
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
"""
|
|
40
|
-
**ViperX**: Professional Python Project Initializer.
|
|
41
|
-
|
|
42
|
-
Automates the creation of professional-grade Python projects using `uv`.
|
|
43
|
-
Supports Standard Libraries, Machine Learning, and Deep Learning templates.
|
|
44
|
-
"""
|
|
45
|
-
if verbose:
|
|
46
|
-
state["verbose"] = True
|
|
47
|
-
console.print("[dim]Verbose mode enabled[/dim]")
|
|
48
|
-
|
|
49
43
|
def version_callback(value: bool):
|
|
50
44
|
if value:
|
|
51
|
-
import importlib.metadata
|
|
52
|
-
try:
|
|
53
|
-
version = importlib.metadata.version("viperx")
|
|
54
|
-
except importlib.metadata.PackageNotFoundError:
|
|
55
|
-
version = "unknown"
|
|
56
45
|
console.print(f"ViperX CLI Version: [bold green]{version}[/bold green]")
|
|
57
46
|
raise typer.Exit()
|
|
58
47
|
|
|
@@ -79,9 +68,21 @@ def cli_callback(
|
|
|
79
68
|
if verbose:
|
|
80
69
|
state["verbose"] = True
|
|
81
70
|
console.print("[dim]Verbose mode enabled[/dim]")
|
|
71
|
+
|
|
72
|
+
|
|
82
73
|
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
|
|
75
|
+
# Config Management Group (The Main Entry Point)
|
|
76
|
+
config_app = typer.Typer(
|
|
77
|
+
help="Manage Declarative Configuration (viperx.yaml).",
|
|
78
|
+
no_args_is_help=False, # Allow running without subcommands (acts as apply)
|
|
79
|
+
)
|
|
80
|
+
app.add_typer(config_app, name="config")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@config_app.callback(invoke_without_command=True)
|
|
84
|
+
def config_main(
|
|
85
|
+
ctx: typer.Context,
|
|
85
86
|
# --- Config Driven Mode ---
|
|
86
87
|
config: Path = typer.Option(
|
|
87
88
|
None, "--config", "-c",
|
|
@@ -115,10 +116,17 @@ def init(
|
|
|
115
116
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
|
|
116
117
|
):
|
|
117
118
|
"""
|
|
118
|
-
Initialize
|
|
119
|
+
**Configure & Initialize**: Apply configuration to create or update a project.
|
|
119
120
|
|
|
120
|
-
|
|
121
|
+
usage: [bold]viperx config [OPTIONS][/bold]
|
|
122
|
+
[bold]viperx config get[/bold]
|
|
121
123
|
"""
|
|
124
|
+
# Check if a subcommand (like 'get') is invoked
|
|
125
|
+
if ctx.invoked_subcommand is not None:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
# --- Apply Logic (Former Init) ---
|
|
129
|
+
|
|
122
130
|
# 1. Declarative Mode
|
|
123
131
|
if config:
|
|
124
132
|
if not config.exists():
|
|
@@ -131,6 +139,10 @@ def init(
|
|
|
131
139
|
|
|
132
140
|
# 2. Imperative Mode (Validation)
|
|
133
141
|
if not name:
|
|
142
|
+
# Implicitly show help if no options provided?
|
|
143
|
+
# Or error out. User expects correct run.
|
|
144
|
+
# If user runs `viperx config` with NO args, and NO config, what happens?
|
|
145
|
+
# "Missing option name".
|
|
134
146
|
console.print("[bold red]Error:[/bold red] Missing option '--name' / '-n'. Required in manual mode.")
|
|
135
147
|
raise typer.Exit(code=1)
|
|
136
148
|
|
|
@@ -156,12 +168,8 @@ def init(
|
|
|
156
168
|
# Generate in current directory
|
|
157
169
|
generator.generate(Path.cwd())
|
|
158
170
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
help="Manage Declarative Configuration (viperx.yaml).",
|
|
162
|
-
no_args_is_help=True
|
|
163
|
-
)
|
|
164
|
-
app.add_typer(config_app, name="config")
|
|
171
|
+
|
|
172
|
+
|
|
165
173
|
|
|
166
174
|
@config_app.command("get")
|
|
167
175
|
def config_get(
|
|
@@ -212,7 +220,7 @@ def package_add(
|
|
|
212
220
|
),
|
|
213
221
|
use_env: bool = typer.Option(True, "--env/--no-env", help="Generate .env file"),
|
|
214
222
|
use_config: bool = typer.Option(True, "--embed-config/--no-embed-config", help="Generate embedded config"),
|
|
215
|
-
use_readme: bool = typer.Option(
|
|
223
|
+
use_readme: bool = typer.Option(False, "--readme/--no-readme", help="Generate README.md"),
|
|
216
224
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
|
|
217
225
|
):
|
|
218
226
|
"""
|
|
@@ -275,4 +283,18 @@ def package_update(
|
|
|
275
283
|
generator.update_package(Path.cwd())
|
|
276
284
|
|
|
277
285
|
if __name__ == "__main__":
|
|
278
|
-
|
|
286
|
+
try:
|
|
287
|
+
app()
|
|
288
|
+
except SystemExit as e:
|
|
289
|
+
if e.code != 0:
|
|
290
|
+
# On error (non-zero exit), display help as requested
|
|
291
|
+
from typer.main import get_command
|
|
292
|
+
import click
|
|
293
|
+
cli = get_command(app)
|
|
294
|
+
# Create a dummy context to render help
|
|
295
|
+
# We print it to stderr or stdout? Console prints to stdout usually.
|
|
296
|
+
# User wants it displayed immediately.
|
|
297
|
+
with click.Context(cli) as ctx:
|
|
298
|
+
console.print("\n")
|
|
299
|
+
console.print(cli.get_help(ctx))
|
|
300
|
+
raise
|
viperx/templates/__init__.py.j2
CHANGED
viperx/templates/config.py.j2
CHANGED
|
@@ -28,6 +28,7 @@ def get_config(key: str, default: Any = None) -> Any:
|
|
|
28
28
|
"""Retrieve a value from the globally loaded settings."""
|
|
29
29
|
return SETTINGS.get(key, default)
|
|
30
30
|
|
|
31
|
+
{% if project_type != 'classic' %}
|
|
31
32
|
def get_dataset_path(notebook_name: str, key: str = "datasets", extension: str = ".csv") -> str | None:
|
|
32
33
|
"""
|
|
33
34
|
Helper for notebook data loading.
|
|
@@ -38,3 +39,4 @@ def get_dataset_path(notebook_name: str, key: str = "datasets", extension: str =
|
|
|
38
39
|
if not dataset_name:
|
|
39
40
|
return None
|
|
40
41
|
return f"{dataset_name}{extension}"
|
|
42
|
+
{% endif %}
|
viperx/templates/config.yaml.j2
CHANGED
viperx/templates/main.py.j2
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
from {{ package_name }} import hello
|
|
2
1
|
{% if has_config %}
|
|
3
2
|
from {{ package_name }} import SETTINGS
|
|
4
3
|
{% endif %}
|
|
5
4
|
|
|
6
5
|
def main():
|
|
7
|
-
hello()
|
|
8
6
|
{% if has_config %}
|
|
9
|
-
print(f"
|
|
7
|
+
print(f"Hi from {SETTINGS['project_name']}!")
|
|
8
|
+
{% else %}
|
|
9
|
+
print("Hi from viperx!")
|
|
10
10
|
{% endif %}
|
|
11
11
|
|
|
12
12
|
if __name__ == "__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,38 +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
|
-
{
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
requires = ["setuptools>=61.0"]
|
|
52
|
-
build-backend = "setuptools.build_meta"
|
|
53
|
-
{% endif %}
|
|
51
|
+
{%- else %}
|
|
52
|
+
# Default: uv native build backend
|
|
53
|
+
requires = ["uv_build>=0.9.21,<0.10.0"]
|
|
54
|
+
build-backend = "uv_build"
|
|
55
|
+
{%- endif %}
|
|
54
56
|
|
|
55
|
-
{% if use_uv %}
|
|
56
|
-
[tool.uv]
|
|
57
|
-
managed = true
|
|
58
|
-
{% endif %}
|
|
59
57
|
|
|
@@ -10,7 +10,7 @@ project:
|
|
|
10
10
|
|
|
11
11
|
# [Optional] Defaults (Inferred from git/system if omitted)
|
|
12
12
|
# description: "My robust project"
|
|
13
|
-
# author: "
|
|
13
|
+
# author: "Nameless"
|
|
14
14
|
# license: "MIT"
|
|
15
15
|
# builder: "uv"
|
|
16
16
|
|
|
@@ -42,4 +42,5 @@ workspace:
|
|
|
42
42
|
# use_env: false
|
|
43
43
|
# use_config: true
|
|
44
44
|
# use_tests: true
|
|
45
|
+
# use_readme: false
|
|
45
46
|
|
viperx/utils.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import shutil
|
|
3
|
-
import subprocess
|
|
4
3
|
from rich.console import Console
|
|
5
4
|
|
|
6
5
|
console = Console()
|
|
@@ -32,16 +31,48 @@ def validate_project_name(ctx, param, value):
|
|
|
32
31
|
raise BadParameter("Project name must contain only letters, numbers, underscores, and hyphens.")
|
|
33
32
|
return value
|
|
34
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
|
+
|
|
45
|
+
def check_builder_installed(builder: str) -> bool:
|
|
46
|
+
"""
|
|
47
|
+
Check if the specified builder is valid AND installed.
|
|
48
|
+
Uses shutil.which() for robust path detection.
|
|
49
|
+
"""
|
|
50
|
+
# 1. Validate against supported list
|
|
51
|
+
if builder not in SUPPORTED_BUILDERS:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
# 2. Check existence
|
|
55
|
+
return shutil.which(builder) is not None
|
|
56
|
+
|
|
35
57
|
def get_author_from_git() -> tuple[str, str]:
|
|
36
58
|
"""
|
|
37
59
|
Attempt to get author name and email from git config.
|
|
38
60
|
Returns (name, email) or defaults.
|
|
39
61
|
"""
|
|
40
62
|
try:
|
|
63
|
+
# Check if git is installed first
|
|
64
|
+
if not shutil.which("git"):
|
|
65
|
+
return "Nameless", "nameless@example.com"
|
|
66
|
+
|
|
41
67
|
import git
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
# Robust way using git command wrapper
|
|
69
|
+
reader = git.Git().config
|
|
70
|
+
name = reader("--global", "--get", "user.name")
|
|
71
|
+
email = reader("--global", "--get", "user.email")
|
|
72
|
+
|
|
73
|
+
# strip newlines if any
|
|
74
|
+
return (name.strip() if name else "Nameless",
|
|
75
|
+
email.strip() if email else "nameless@example.com")
|
|
46
76
|
except Exception:
|
|
47
|
-
|
|
77
|
+
# Fallback if git call fails
|
|
78
|
+
return "Nameless", "nameless@example.com"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
viperx/__init__.py,sha256=mRrD-SK9PtwBhqgLzJrvefdViNsXCyBGknzFNsIou1I,51
|
|
2
|
+
viperx/config_engine.py,sha256=A5u1qUaWlgvXQReRHtF05--spPsd90XgT81zZYR2rBM,9908
|
|
3
|
+
viperx/constants.py,sha256=Mv5tFjFxe3tD4VDCEsXiwfiV7biTlzl45ALkHmiK33M,887
|
|
4
|
+
viperx/core.py,sha256=yEACR7U-z5h7_fuCKyy4uljGPMe4S-F1HO5H_XeqHNg,20308
|
|
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=j-i1NRnKdvk-EgjijSyx-rfOsxWOVSXCyHY2-JEwMDM,93
|
|
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=cpXjtc9MS6CnGAZ0RK1jaB3xBqh4r5Q4iwscBVB2pcY,258
|
|
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.40.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
|
|
20
|
+
viperx-0.9.40.dist-info/entry_points.txt,sha256=Is38BrTuf6unQCLgDE990Rjd9HyGldBSw4Uzy_nMePY,44
|
|
21
|
+
viperx-0.9.40.dist-info/METADATA,sha256=h7IGeSpqB1LJ__SFKtWl_PGaJ1tVoZCARLzynArtGEk,6782
|
|
22
|
+
viperx-0.9.40.dist-info/RECORD,,
|
viperx-0.9.14.dist-info/RECORD
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
viperx/__init__.py,sha256=mRrD-SK9PtwBhqgLzJrvefdViNsXCyBGknzFNsIou1I,51
|
|
2
|
-
viperx/config_engine.py,sha256=Fzb5F3rE7BqcnpAdu6QhN28tfQNTuBHI04YuR1B7cHM,7340
|
|
3
|
-
viperx/constants.py,sha256=BMogG1-ik67YeheQdCgjsdS5FNFYiLfdro6utCWwczY,784
|
|
4
|
-
viperx/core.py,sha256=dOLLPxBohYN6A5i1BnT5gx35x6x5n40yYYGm6yEZKHM,18733
|
|
5
|
-
viperx/licenses.py,sha256=TPsG1aqNAjtOsvuD6i3SiC5rUK9f3DslPHQkKiV7vxs,12482
|
|
6
|
-
viperx/main.py,sha256=B8Z6bjESFKFvL80NJcfPGgYycxQP3NwLhKtJDtaj1Zc,9856
|
|
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=sU6bwiA0eW7g0d9Y_rhxSd-6LcMUyYiZKBhcF_OJcxM,1409
|
|
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=RnJm7BinT-p646BiC2gxOTBuIX0nmRM7BUqt0n981io,1434
|
|
17
|
-
viperx/templates/viperx_config.yaml.j2,sha256=dZ5GtiGRWT_FOsMeF1YUjqL5wHO0y0i38gqU-lwrVSQ,1379
|
|
18
|
-
viperx/utils.py,sha256=2kiQLDHsPcCELEKeQYbIkSL8e3HhW4pRzw1pUth6hYA,1583
|
|
19
|
-
viperx-0.9.14.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
|
|
20
|
-
viperx-0.9.14.dist-info/entry_points.txt,sha256=Is38BrTuf6unQCLgDE990Rjd9HyGldBSw4Uzy_nMePY,44
|
|
21
|
-
viperx-0.9.14.dist-info/METADATA,sha256=OC8UDqEikK2-pU1RKt02YGRbKtsLvg0p-7OFGzvzXKA,6782
|
|
22
|
-
viperx-0.9.14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|