viperx 0.9.5__tar.gz → 0.9.49__tar.gz

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.
Files changed (24) hide show
  1. {viperx-0.9.5 → viperx-0.9.49}/PKG-INFO +3 -3
  2. {viperx-0.9.5 → viperx-0.9.49}/README.md +2 -2
  3. {viperx-0.9.5 → viperx-0.9.49}/pyproject.toml +1 -1
  4. {viperx-0.9.5 → viperx-0.9.49}/src/viperx/config_engine.py +78 -20
  5. {viperx-0.9.5 → viperx-0.9.49}/src/viperx/constants.py +5 -0
  6. {viperx-0.9.5 → viperx-0.9.49}/src/viperx/core.py +135 -107
  7. {viperx-0.9.5 → viperx-0.9.49}/src/viperx/main.py +78 -22
  8. viperx-0.9.49/src/viperx/templates/__init__.py.j2 +8 -0
  9. {viperx-0.9.5 → viperx-0.9.49}/src/viperx/templates/config.py.j2 +2 -0
  10. {viperx-0.9.5 → viperx-0.9.49}/src/viperx/templates/config.yaml.j2 +3 -0
  11. viperx-0.9.49/src/viperx/templates/main.py.j2 +13 -0
  12. {viperx-0.9.5 → viperx-0.9.49}/src/viperx/templates/pyproject.toml.j2 +23 -16
  13. {viperx-0.9.5 → viperx-0.9.49}/src/viperx/templates/viperx_config.yaml.j2 +8 -11
  14. viperx-0.9.49/src/viperx/utils.py +78 -0
  15. viperx-0.9.5/src/viperx/templates/__init__.py.j2 +0 -8
  16. viperx-0.9.5/src/viperx/templates/main.py.j2 +0 -13
  17. viperx-0.9.5/src/viperx/utils.py +0 -47
  18. {viperx-0.9.5 → viperx-0.9.49}/src/viperx/__init__.py +0 -0
  19. {viperx-0.9.5 → viperx-0.9.49}/src/viperx/licenses.py +0 -0
  20. {viperx-0.9.5 → viperx-0.9.49}/src/viperx/templates/Base.ipynb.j2 +0 -0
  21. {viperx-0.9.5 → viperx-0.9.49}/src/viperx/templates/Base_General.ipynb.j2 +0 -0
  22. {viperx-0.9.5 → viperx-0.9.49}/src/viperx/templates/Base_Kaggle.ipynb.j2 +0 -0
  23. {viperx-0.9.5 → viperx-0.9.49}/src/viperx/templates/README.md.j2 +0 -0
  24. {viperx-0.9.5 → viperx-0.9.49}/src/viperx/templates/data_loader.py.j2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: viperx
3
- Version: 0.9.5
3
+ Version: 0.9.49
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
@@ -64,7 +64,7 @@ viperx init -n deep-vision -t dl --framework pytorch
64
64
  viperx init -n deep-vision -t dl --framework pytorch
65
65
 
66
66
  # ✨ Declarative Config (Infrastructure as Code)
67
- viperx config init # Generate template
67
+ viperx config get # Generate template
68
68
  viperx init -c viperx.yaml # Apply config
69
69
  ```
70
70
 
@@ -174,7 +174,7 @@ viperx init -c viperx.yaml
174
174
  Manage your project infrastructure using a YAML file.
175
175
 
176
176
  ```bash
177
- viperx config init
177
+ viperx config get
178
178
  ```
179
179
  Generates a `viperx.yaml` template in the current directory.
180
180
 
@@ -45,7 +45,7 @@ viperx init -n deep-vision -t dl --framework pytorch
45
45
  viperx init -n deep-vision -t dl --framework pytorch
46
46
 
47
47
  # ✨ Declarative Config (Infrastructure as Code)
48
- viperx config init # Generate template
48
+ viperx config get # Generate template
49
49
  viperx init -c viperx.yaml # Apply config
50
50
  ```
51
51
 
@@ -155,7 +155,7 @@ viperx init -c viperx.yaml
155
155
  Manage your project infrastructure using a YAML file.
156
156
 
157
157
  ```bash
158
- viperx config init
158
+ viperx config get
159
159
  ```
160
160
  Generates a `viperx.yaml` template in the current directory.
161
161
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "viperx"
3
- version = "0.9.5"
3
+ version = "0.9.49"
4
4
  description = "Professional Python Project Initializer with uv, ml/dl support, and embedded config."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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")
@@ -129,7 +187,7 @@ class ConfigEngine:
129
187
  author=project_conf.get("author", "Your Name"), # Inherit author
130
188
  use_env=pkg.get("use_env", settings_conf.get("use_env", False)), # Inherit settings or default False
131
189
  use_config=pkg.get("use_config", settings_conf.get("use_config", True)), # Inherit or default True
132
- use_readme=pkg.get("use_readme", True),
190
+ use_readme=pkg.get("use_readme", False),
133
191
  use_tests=pkg.get("use_tests", settings_conf.get("use_tests", True)),
134
192
  framework=pkg.get("framework", FRAMEWORK_PYTORCH),
135
193
  verbose=self.verbose
@@ -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"))
@@ -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]
@@ -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
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
59
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"),
@@ -67,26 +117,10 @@ class ProjectGenerator:
67
117
  if self.verbose:
68
118
  console.print(f" [{style}]{message}[/{style}]")
69
119
 
70
- def generate(self, target_dir: Optional[Path] = None):
71
- """Main generation flow using uv init."""
72
- if target_dir is None:
73
- target_dir = Path.cwd()
74
-
75
- project_dir = target_dir / self.raw_name
76
-
77
- if project_dir.exists():
78
- console.print(f"[bold red]Error:[/bold red] Directory {project_dir} already exists.")
79
- return
80
-
81
- console.print(f"[bold green]Creating project {self.raw_name} ({self.type})...[/bold green]")
82
-
83
- self.log(f"Target directory: {project_dir}")
84
- self.log(f"Python version: {self.python_version}")
85
-
86
-
87
120
  def generate(self, target_dir: Path, is_subpackage: bool = False):
88
121
  """Main generation flow using uv init."""
89
- project_dir = target_dir / self.raw_name
122
+ # STRICT DIRECTORY NAMING: Always use sanitized name
123
+ project_dir = target_dir / self.project_name
90
124
 
91
125
  # 1. Scaffolding with uv init
92
126
  try:
@@ -102,8 +136,13 @@ class ProjectGenerator:
102
136
  )
103
137
  else:
104
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
105
144
  subprocess.run(
106
- ["uv", "init", "--package", "--no-workspace", self.raw_name],
145
+ ["uv", "init", "--package", "--no-workspace", self.project_name, "--name", self.raw_name],
107
146
  check=True, cwd=target_dir, capture_output=True
108
147
  )
109
148
  console.print(" [blue]✓ Scaffolding created with uv init[/blue]")
@@ -112,20 +151,23 @@ class ProjectGenerator:
112
151
  return
113
152
 
114
153
  # 2. Restructure / Clean up
115
- # If is_subpackage, convert to Flat Layout
154
+ # If is_subpackage, convert to Ultra-Flat Layout (Code at Root)
155
+ # Target: root/__init__.py (and siblings)
156
+ # Source: root/src/pkg/__init__.py (from uv init --lib)
116
157
  if is_subpackage:
117
- src_path = project_dir / "src" / self.project_name
118
- flat_path = project_dir / self.project_name
119
- if src_path.exists():
120
- if flat_path.exists():
121
- import shutil
122
- shutil.rmtree(flat_path)
123
- src_path.rename(flat_path)
124
- # Remove empty src
125
- import shutil
126
- if (project_dir / "src").exists():
127
- shutil.rmtree(project_dir / "src")
128
- self.log("Converted to Flat Layout (Subpackage)")
158
+ src_pkg_path = project_dir / "src" / self.project_name
159
+ import shutil
160
+
161
+ if src_pkg_path.exists():
162
+ # Move children of src/pkg to root
163
+ for item in src_pkg_path.iterdir():
164
+ shutil.move(str(item), str(project_dir))
165
+
166
+ # Cleanup src/pkg and src
167
+ shutil.rmtree(src_pkg_path)
168
+ if (project_dir / "src").exists() and not any((project_dir / "src").iterdir()):
169
+ shutil.rmtree(project_dir / "src")
170
+ self.log("Converted to Ultra-Flat Layout (Code at Root)")
129
171
 
130
172
  # 3. Create extra directories (First, so templates have target dirs)
131
173
  self._create_extra_dirs(project_dir, is_subpackage)
@@ -133,12 +175,26 @@ class ProjectGenerator:
133
175
  # 4. Overwrite/Add Files
134
176
  self._generate_files(project_dir, is_subpackage)
135
177
 
178
+
179
+ # Cleanup extra files for subpackages
180
+ if is_subpackage:
181
+ for f in [".gitignore", ".python-version"]:
182
+ if (project_dir / f).exists():
183
+ (project_dir / f).unlink()
184
+
136
185
  # 5. Git & Final Steps
137
- 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]")
138
187
  if not is_subpackage:
139
- console.print(f" [dim]cd {self.raw_name} && uv sync[/dim]")
188
+ console.print(f" [dim]cd {self.project_name} && uv sync[/dim]")
140
189
 
141
190
  def _create_extra_dirs(self, root: Path, is_subpackage: bool = False):
191
+ if is_subpackage:
192
+ # Ultra-Flat Layout: root IS the package root
193
+ pkg_root = root
194
+ else:
195
+ # Standard Layout: root / src / package_name
196
+ pkg_root = root / SRC_DIR / self.project_name
197
+
142
198
  # Notebooks for ML/DL (Only for Root Project usually)
143
199
  if not is_subpackage and self.type in [TYPE_ML, TYPE_DL]:
144
200
  (root / NOTEBOOKS_DIR).mkdir(exist_ok=True)
@@ -149,11 +205,10 @@ class ProjectGenerator:
149
205
  # For Flat Layout (Subpackage), tests usually go to `tests/` at root
150
206
  # For Src Layout (Root), inside `src/pkg/tests`?
151
207
  # User request: "create dossier tests ... que ce soit au init general ou pour sous package"
152
- # Standard practice: `tests/` at project root.
153
- # ViperX old behavior: `src/pkg/tests` (Line 137).
154
- # Changing to standard `tests/` at project root for BOTH.
155
- tests_dir = root / TESTS_DIR
156
- tests_dir.mkdir(exist_ok=True)
208
+ # User request: "tests/ pour le package principal est dans src/name_package/ tout est isolé"
209
+ # So tests are INSIDE the package.
210
+ tests_dir = pkg_root / TESTS_DIR
211
+ tests_dir.mkdir(parents=True, exist_ok=True)
157
212
 
158
213
  with open(tests_dir / "__init__.py", "w") as f:
159
214
  pass
@@ -177,16 +232,21 @@ class ProjectGenerator:
177
232
  "has_config": self.use_config,
178
233
  "use_readme": self.use_readme,
179
234
  "use_env": self.use_env,
235
+ "use_env": self.use_env,
180
236
  "framework": self.framework,
237
+ "scripts": self.scripts,
181
238
  }
239
+ # Merge dependency context overrides
240
+ context.update(self.dependency_context)
182
241
 
183
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)
184
244
  self._render("pyproject.toml.j2", root / "pyproject.toml", context)
185
245
 
186
246
  # Determine Package Root
187
247
  if is_subpackage:
188
- # Flat Layout: root / package_name
189
- pkg_root = root / self.project_name
248
+ # Ultra-Flat Layout: root IS the package root
249
+ pkg_root = root
190
250
  else:
191
251
  # Standard Layout: root / src / package_name
192
252
  pkg_root = root / SRC_DIR / self.project_name
@@ -201,19 +261,33 @@ class ProjectGenerator:
201
261
  self._render("__init__.py.j2", pkg_root / "__init__.py", context)
202
262
 
203
263
  # README.md
204
- if self.use_readme:
205
- self._render("README.md.j2", root / "README.md", context)
264
+ # README.md
265
+ if not is_subpackage:
266
+ # Root Project: Respect use_readme
267
+ if self.use_readme:
268
+ self._render("README.md.j2", root / "README.md", context)
269
+ else:
270
+ if (root / "README.md").exists():
271
+ (root / "README.md").unlink()
272
+ self.log("Removed default README.md (requested --no-readme)")
206
273
  else:
207
- if (root / "README.md").exists():
274
+ # Subpackage: Default False, but if True, generate it
275
+ if self.use_readme:
276
+ self._render("README.md.j2", root / "README.md", context)
277
+ elif (root / "README.md").exists():
278
+ # Cleanup default README from uv init if we didn't request one
208
279
  (root / "README.md").unlink()
209
- self.log("Removed default README.md (requested --no-readme)")
280
+
210
281
 
211
282
  # LICENSE
212
- license_text = LICENSES.get(self.license, LICENSES["MIT"])
213
- license_text = license_text.format(year=datetime.now().year, author=self.author)
214
- with open(root / "LICENSE", "w") as f:
215
- f.write(license_text)
216
- self.log(f"Generated LICENSE ({self.license})")
283
+ if not is_subpackage:
284
+ license_text = LICENSES.get(self.license, LICENSES["MIT"])
285
+ license_text = license_text.format(year=datetime.now().year, author=self.author)
286
+ with open(root / "LICENSE", "w") as f:
287
+ f.write(license_text)
288
+ self.log(f"Generated LICENSE ({self.license})")
289
+ elif (root / "LICENSE").exists():
290
+ (root / "LICENSE").unlink()
217
291
 
218
292
  # Config files
219
293
  if self.use_config:
@@ -240,10 +314,12 @@ class ProjectGenerator:
240
314
  self.log(f"Created .env and .env.example in {pkg_root.relative_to(root)}")
241
315
 
242
316
  # .gitignore
243
- with open(root / ".gitignore", "a") as f:
244
- # Add data/ to gitignore but allow .gitkeep
245
- f.write("\n# ViperX specific\n.ipynb_checkpoints/\n# Isolated Env\nsrc/**/.env\n# Data (Local)\ndata/*\n!data/.gitkeep\n")
246
- self.log("Updated .gitignore")
317
+ # Only for Root
318
+ if not is_subpackage:
319
+ with open(root / ".gitignore", "a") as f:
320
+ # Add data/ to gitignore but allow .gitkeep
321
+ f.write("\n# ViperX specific\n.ipynb_checkpoints/\n# Isolated Env\nsrc/**/.env\n# Data (Local)\ndata/*\n!data/.gitkeep\n")
322
+ self.log("Updated .gitignore")
247
323
 
248
324
  def _render(self, template_name: str, target_path: Path, context: dict):
249
325
  template = self.env.get_template(template_name)
@@ -256,57 +332,9 @@ class ProjectGenerator:
256
332
  """Add a new package to an existing workspace."""
257
333
  console.print(f"[bold green]Adding package {self.raw_name} to workspace...[/bold green]")
258
334
 
259
- pyproject_path = workspace_root / "pyproject.toml"
260
- if not pyproject_path.exists():
261
- console.print("[red]Error: Not in a valid project root (pyproject.toml missing).[/red]")
262
- return
263
-
264
- # Read pyproject.toml
265
- with open(pyproject_path, "r") as f:
266
- content = f.read()
267
-
268
- # Check if it's already a workspace
269
- is_workspace = "[tool.uv.workspace]" in content
270
-
271
- if not is_workspace:
272
- console.print("[yellow]Upgrading project to Workspace...[/yellow]")
273
- # Append workspace definition
274
- with open(pyproject_path, "a") as f:
275
- f.write(f"\n[tool.uv.workspace]\nmembers = [\"src/{self.raw_name}\"]\n")
276
- self.log("Added [tool.uv.workspace] section")
277
- else:
278
- # Add member to specific list if it exists
279
- # We use a simple regex approach to find 'members = [...]'
280
- import re
281
- members_pattern = r'members\s*=\s*\[(.*?)\]'
282
- match = re.search(members_pattern, content, re.DOTALL)
283
-
284
- if match:
285
- current_members = match.group(1)
286
- # Check if already present
287
- if f'"{self.raw_name}"' in current_members or f"'{self.raw_name}'" in current_members:
288
- self.log(f"Package {self.raw_name} is already in workspace members.")
289
- else:
290
- # Append new member
291
- # We inject it into the list
292
- self.log("Adding member to existing workspace list")
293
- # Naively replace the closing bracket
294
- # Better: parse, but for now robust string insertion
295
- # Cleanest way without breaking formatting involves finding the last element
296
- # Cleanest way without breaking formatting involves finding the last element
297
- new_member = f', "src/{self.raw_name}"'
298
- # Warning: This regex replace is basic. `uv` handles toml well, maybe we should just edit safely.
299
- # Let's try to append to the end of the content of the list
300
- new_content = re.sub(members_pattern, lambda m: f'members = [{m.group(1)}{new_member}]', content, flags=re.DOTALL)
301
- with open(pyproject_path, "w") as f:
302
- f.write(new_content)
303
- else:
304
- # Section exists but members key might be missing? Or weird formatting.
305
- # Append to section?
306
- # Safe fallback
307
- console.print("[yellow]Warning: Could not parse members list. Adding manually at end.[/yellow]")
308
- with open(pyproject_path, "a") as f:
309
- 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
310
338
 
311
339
  # Generate the package in the root IF it doesn't exist
312
340
  pkg_dir = workspace_root / SRC_DIR / self.raw_name
@@ -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,18 +33,30 @@ 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
- @app.callback()
43
+ def version_callback(value: bool):
44
+ if value:
45
+ console.print(f"ViperX CLI Version: [bold green]{version}[/bold green]")
46
+ raise typer.Exit()
47
+
48
+ @app.callback(invoke_without_command=True)
39
49
  def cli_callback(
50
+ ctx: typer.Context,
40
51
  verbose: bool = typer.Option(
41
52
  False, "-v", "--verbose",
42
53
  help="Enable verbose logging."
54
+ ),
55
+ version: bool = typer.Option(
56
+ None, "--version", "-V",
57
+ callback=version_callback,
58
+ is_eager=True,
59
+ help="Show version and exit."
43
60
  )
44
61
  ):
45
62
  """
@@ -51,10 +68,21 @@ def cli_callback(
51
68
  if verbose:
52
69
  state["verbose"] = True
53
70
  console.print("[dim]Verbose mode enabled[/dim]")
71
+
72
+
73
+
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")
54
81
 
55
82
 
56
- @app.command()
57
- def init(
83
+ @config_app.callback(invoke_without_command=True)
84
+ def config_main(
85
+ ctx: typer.Context,
58
86
  # --- Config Driven Mode ---
59
87
  config: Path = typer.Option(
60
88
  None, "--config", "-c",
@@ -84,14 +112,21 @@ def init(
84
112
  help=f"DL Framework ({'|'.join(DL_FRAMEWORKS)}). Defaults to pytorch."
85
113
  ),
86
114
  use_env: bool = typer.Option(True, "--env/--no-env", help="Generate .env file"),
87
- use_config: bool = typer.Option(True, "--config/--no-config", help="Generate embedded config"),
115
+ use_config: bool = typer.Option(True, "--embed-config/--no-embed-config", help="Generate embedded config"),
88
116
  verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
89
117
  ):
90
118
  """
91
- Initialize a new Python project.
119
+ **Configure & Initialize**: Apply configuration to create or update a project.
92
120
 
93
- Can stem from a config file (Declarative) or CLI arguments (Imperative).
121
+ usage: [bold]viperx config [OPTIONS][/bold]
122
+ [bold]viperx config get[/bold]
94
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
+
95
130
  # 1. Declarative Mode
96
131
  if config:
97
132
  if not config.exists():
@@ -104,6 +139,10 @@ def init(
104
139
 
105
140
  # 2. Imperative Mode (Validation)
106
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".
107
146
  console.print("[bold red]Error:[/bold red] Missing option '--name' / '-n'. Required in manual mode.")
108
147
  raise typer.Exit(code=1)
109
148
 
@@ -113,6 +152,13 @@ def init(
113
152
 
114
153
  console.print(Panel(f"Initializing [bold blue]{name}[/bold blue]", border_style="blue"))
115
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
+
116
162
  generator = ProjectGenerator(
117
163
  name=name,
118
164
  description=description,
@@ -129,19 +175,15 @@ def init(
129
175
  # Generate in current directory
130
176
  generator.generate(Path.cwd())
131
177
 
132
- # Config Management Group
133
- config_app = typer.Typer(
134
- help="Manage Declarative Configuration (viperx.yaml).",
135
- no_args_is_help=True
136
- )
137
- app.add_typer(config_app, name="config")
138
178
 
139
- @config_app.command("init")
140
- def config_init(
179
+
180
+
181
+ @config_app.command("get")
182
+ def config_get(
141
183
  filename: Path = typer.Option("viperx.yaml", "--output", "-o", help="Output filename")
142
184
  ):
143
185
  """
144
- Generate a template viperx.yaml configuration file.
186
+ Get the default configuration template (viperx.yaml).
145
187
  Use this to start a 'Project as Code' workflow.
146
188
  """
147
189
  from viperx.constants import TEMPLATES_DIR
@@ -184,8 +226,8 @@ def package_add(
184
226
  help=f"DL Framework ({'|'.join(DL_FRAMEWORKS)}). Defaults to pytorch."
185
227
  ),
186
228
  use_env: bool = typer.Option(True, "--env/--no-env", help="Generate .env file"),
187
- use_config: bool = typer.Option(True, "--config/--no-config", help="Generate embedded config"),
188
- use_readme: bool = typer.Option(True, "--readme/--no-readme", help="Generate README.md"),
229
+ use_config: bool = typer.Option(True, "--embed-config/--no-embed-config", help="Generate embedded config"),
230
+ use_readme: bool = typer.Option(False, "--readme/--no-readme", help="Generate README.md"),
189
231
  verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
190
232
  ):
191
233
  """
@@ -248,4 +290,18 @@ def package_update(
248
290
  generator.update_package(Path.cwd())
249
291
 
250
292
  if __name__ == "__main__":
251
- app()
293
+ try:
294
+ app()
295
+ except SystemExit as e:
296
+ if e.code != 0:
297
+ # On error (non-zero exit), display help as requested
298
+ from typer.main import get_command
299
+ import click
300
+ cli = get_command(app)
301
+ # Create a dummy context to render help
302
+ # We print it to stderr or stdout? Console prints to stdout usually.
303
+ # User wants it displayed immediately.
304
+ with click.Context(cli) as ctx:
305
+ console.print("\n")
306
+ console.print(cli.get_help(ctx))
307
+ raise
@@ -0,0 +1,8 @@
1
+ {% if has_config %}
2
+ from .config import SETTINGS, get_config
3
+ {% if project_type != 'classic' %}
4
+ from .config import get_dataset_path
5
+ {% endif %}
6
+ {% endif %}
7
+
8
+
@@ -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"
@@ -0,0 +1,13 @@
1
+ {%- if has_config %}
2
+ from {{ package_name }} import SETTINGS
3
+ {%- endif %}
4
+
5
+ def main():
6
+ {%- if has_config %}
7
+ print(f"Hi from {SETTINGS['project_name']}!")
8
+ {%- else %}
9
+ print("Hi from viperx!")
10
+ {%- endif %}
11
+
12
+ if __name__ == "__main__":
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,29 +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]
48
+ {%- if builder == 'hatch' %}
43
49
  requires = ["hatchling"]
44
50
  build-backend = "hatchling.build"
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 %}
45
56
 
46
- {% if use_uv %}
47
- [tool.uv]
48
- managed = true
49
- {% endif %}
50
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
 
@@ -36,14 +36,11 @@ workspace:
36
36
  # Define workspace members (Monorepo / Utility Packages).
37
37
  # These are minimal packages created at the workspace root (Flat Layout).
38
38
  packages:
39
- # --- Example 1: Shared Utilities ---
40
- # - name: "shared-utils"
41
- # description: "Common tools"
42
- # # Defaults: use_env=false, use_config=true, use_tests=true
43
-
44
- # --- Example 2: AI Submodule ---
45
- # - name: "vision-core"
46
- # type: "dl"
47
- # framework: "pytorch"
48
- # use_env: true # Override default
39
+ # --- Example 1: Preprocess Package ---
40
+ # - name: "preprocess"
41
+ # description: "reprocessing utilities"
42
+ # use_env: false
43
+ # use_config: true
44
+ # use_tests: true
45
+ # use_readme: false
49
46
 
@@ -0,0 +1,78 @@
1
+ import re
2
+ import shutil
3
+ from rich.console import Console
4
+
5
+ console = Console()
6
+
7
+ def check_uv_installed() -> bool:
8
+ """Check if 'uv' is installed and accessible."""
9
+ return shutil.which("uv") is not None
10
+
11
+ def sanitize_project_name(name: str) -> str:
12
+ """
13
+ Sanitize the project name to be a valid Python package name.
14
+ Replaces hyphens with underscores and removes invalid characters.
15
+ """
16
+ # Replace - with _
17
+ name = name.replace("-", "_")
18
+ # Remove any characters that aren't alphanumerics or underscores
19
+ name = re.sub(r"[^a-zA-Z0-9_]", "", name)
20
+ # Ensure it starts with a letter or underscore
21
+ if not re.match(r"^[a-zA-Z_]", name):
22
+ name = f"_{name}"
23
+ return name.lower()
24
+
25
+ def validate_project_name(ctx, param, value):
26
+ """
27
+ Typer callback to validate project name.
28
+ """
29
+ if not re.match(r"^[a-zA-Z0-9_-]+$", value):
30
+ from typer import BadParameter
31
+ raise BadParameter("Project name must contain only letters, numbers, underscores, and hyphens.")
32
+ return value
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
+
57
+ def get_author_from_git() -> tuple[str, str]:
58
+ """
59
+ Attempt to get author name and email from git config.
60
+ Returns (name, email) or defaults.
61
+ """
62
+ try:
63
+ # Check if git is installed first
64
+ if not shutil.which("git"):
65
+ return "Nameless", "nameless@example.com"
66
+
67
+ import git
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")
76
+ except Exception:
77
+ # Fallback if git call fails
78
+ return "Nameless", "nameless@example.com"
@@ -1,8 +0,0 @@
1
- {% if has_config %}
2
- from .config import SETTINGS, get_config, get_dataset_path
3
- {% endif %}
4
-
5
- __version__ = "{{ version }}"
6
-
7
- def hello():
8
- print(f"Hello from {{ project_name }} v{__version__}!")
@@ -1,13 +0,0 @@
1
- from {{ package_name }} import hello
2
- {% if has_config %}
3
- from {{ package_name }} import SETTINGS
4
- {% endif %}
5
-
6
- def main():
7
- hello()
8
- {% if has_config %}
9
- print(f"Project config loaded: {SETTINGS['project_name']}")
10
- {% endif %}
11
-
12
- if __name__ == "__main__":
13
- main()
@@ -1,47 +0,0 @@
1
- import re
2
- import shutil
3
- import subprocess
4
- from rich.console import Console
5
-
6
- console = Console()
7
-
8
- def check_uv_installed() -> bool:
9
- """Check if 'uv' is installed and accessible."""
10
- return shutil.which("uv") is not None
11
-
12
- def sanitize_project_name(name: str) -> str:
13
- """
14
- Sanitize the project name to be a valid Python package name.
15
- Replaces hyphens with underscores and removes invalid characters.
16
- """
17
- # Replace - with _
18
- name = name.replace("-", "_")
19
- # Remove any characters that aren't alphanumerics or underscores
20
- name = re.sub(r"[^a-zA-Z0-9_]", "", name)
21
- # Ensure it starts with a letter or underscore
22
- if not re.match(r"^[a-zA-Z_]", name):
23
- name = f"_{name}"
24
- return name.lower()
25
-
26
- def validate_project_name(ctx, param, value):
27
- """
28
- Typer callback to validate project name.
29
- """
30
- if not re.match(r"^[a-zA-Z0-9_-]+$", value):
31
- from typer import BadParameter
32
- raise BadParameter("Project name must contain only letters, numbers, underscores, and hyphens.")
33
- return value
34
-
35
- def get_author_from_git() -> tuple[str, str]:
36
- """
37
- Attempt to get author name and email from git config.
38
- Returns (name, email) or defaults.
39
- """
40
- try:
41
- 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
46
- except Exception:
47
- return "Your Name", "your.email@example.com"
File without changes
File without changes