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 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
- # 1. Root Project Handling
53
- # If we are NOT already in the project dir (checking name), we might need to create it
54
- # Or if we are running in the root of where `viperx init` is called.
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
- # Heuristic: Are we already in a folder named 'project_name'?
57
- if self.root_path.name == project_name:
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
- current_root = self.root_path
85
+
79
86
  else:
80
- # We are outside, check if it exists
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
- # Create Root (or Hydrate)
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
- current_root = target_dir
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"\n📦 [bold]Processing {len(packages)} workspace packages...[/bold]")
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]\nProject is up to date with {self.config_path.name}", border_style="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.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"),
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
- project_dir = target_dir / self.raw_name
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.raw_name} && uv sync[/dim]")
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
- if not is_subpackage:
207
- # pyproject.toml (Overwrite uv's basic one to add our specific deps)
208
- self._render("pyproject.toml.j2", root / "pyproject.toml", context)
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
- pyproject_path = workspace_root / "pyproject.toml"
305
- if not pyproject_path.exists():
306
- console.print("[red]Error: Not in a valid project root (pyproject.toml missing).[/red]")
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,
@@ -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
- Edit `config.yaml` to change parameters. The project uses a **Config-in-Package** architecture:
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
- # Clone the repo
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 {{ package_name }}
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 GitHub**
90
+ **Step 1: Install directly from Git**
87
91
  ```python
88
- !pip install git+https://github.com/{{ author_name | lower | replace(" ", "") }}/{{ project_name }}.git
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['project_name']}")
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
- path = kh.dataset_download(SETTINGS['datasets']['titanic'])
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
- │ └── {{ package_name }}/
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
+
@@ -1,8 +1,8 @@
1
1
  {% if has_config %}
2
- from .config import SETTINGS, get_config, get_dataset_path
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__}!")
@@ -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,13 +1,13 @@
1
- from {{ package_name }} import hello
2
- {% if has_config %}
1
+ {%- if has_config %}
3
2
  from {{ package_name }} import SETTINGS
4
- {% endif %}
3
+ {%- endif %}
5
4
 
6
5
  def main():
7
- hello()
8
- {% if has_config %}
9
- print(f"Project config loaded: {SETTINGS['project_name']}")
10
- {% endif %}
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
- {% 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
- {% else %}
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
- {% endif %}
55
+ {%- endif %}
51
56
 
52
- {% if use_uv %}
53
- [tool.uv]
54
- managed = true
55
- {% endif %}
56
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
 
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]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: viperx
3
- Version: 0.9.26
3
+ Version: 0.9.50
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=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,,
@@ -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,,