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 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
- else:
88
- console.print(Panel(f"🚀 [bold green]Creating New Project:[/bold green] {project_name}", border_style="green"))
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", True),
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.python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
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 version: {self.python_version}")
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.raw_name} && uv sync[/dim]")
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
- if self.use_readme:
214
- self._render("README.md.j2", root / "README.md", context)
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 (root / "README.md").exists():
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
- self.log("Removed default README.md (requested --no-readme)")
297
+
219
298
 
220
299
  # 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})")
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
- 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")
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
- 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")
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
- HELP_TEXT = """
19
- [bold green]ViperX[/bold green]: Professional Python Project Initializer
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 ❤️ by KpihX"
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
- @app.command()
84
- def init(
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 a new Python project.
119
+ **Configure & Initialize**: Apply configuration to create or update a project.
119
120
 
120
- Can stem from a config file (Declarative) or CLI arguments (Imperative).
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
- # Config Management Group
160
- config_app = typer.Typer(
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(True, "--readme/--no-readme", help="Generate README.md"),
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
- app()
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
@@ -2,7 +2,4 @@
2
2
  from .config import SETTINGS, get_config, get_dataset_path
3
3
  {% endif %}
4
4
 
5
- __version__ = "{{ version }}"
6
5
 
7
- def hello():
8
- print(f"Hello from {{ project_name }} v{__version__}!")
@@ -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 %}
@@ -1,3 +1,6 @@
1
+ # Global Project Configuration
2
+ project_name: "{{ project_name }}"
3
+
1
4
  {% if project_type in ['ml', 'dl'] %}
2
5
  data_urls:
3
6
  iris: "https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv"
@@ -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"Project config loaded: {SETTINGS['project_name']}")
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
- {% elif project_type == 'dl' %}
26
- {% if framework == 'pytorch' %}
29
+ {%- endif %}
30
+ {%- if is_dl %}
31
+ {%- if 'pytorch' in frameworks %}
27
32
  "torch>=2.0.0",
28
33
  "torchvision>=0.15.0",
29
- {% elif framework == 'tensorflow' %}
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
- {% endif %}
33
- "numpy>=1.24.0",
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
- {% if builder == 'hatch' %}
48
+ {%- if builder == 'hatch' %}
44
49
  requires = ["hatchling"]
45
50
  build-backend = "hatchling.build"
46
- {% elif builder == 'flit' %}
47
- requires = ["flit_core>=3.2,<4"]
48
- build-backend = "flit_core.buildapi"
49
- {% else %}
50
- # Default to Setuptools (Standard/Robust) to avoid Hatchling unless requested
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: "KpihX"
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
- config = git.GitConfigParser(git.GitConfigParser.get_global_config(), read_only=True)
43
- name = config.get("user", "name", fallback="Your Name")
44
- email = config.get("user", "email", fallback="your.email@example.com")
45
- return name, email
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
- return "Your Name", "your.email@example.com"
77
+ # Fallback if git call fails
78
+ return "Nameless", "nameless@example.com"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: viperx
3
- Version: 0.9.14
3
+ Version: 0.9.40
4
4
  Summary: Professional Python Project Initializer with uv, ml/dl support, and embedded config.
5
5
  Keywords: python,project-template,uv,data-science,machine-learning
6
6
  Author: Ivann KAMDEM
@@ -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,,
@@ -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,,