gentem 0.1.3__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.
@@ -0,0 +1,191 @@
1
+ """Template engine for Gentem using Jinja2."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any, Dict, Optional
6
+
7
+ from jinja2 import (
8
+ BaseLoader,
9
+ Environment,
10
+ FileSystemLoader,
11
+ TemplateSyntaxError,
12
+ UndefinedError,
13
+ )
14
+ from rich import print
15
+ from rich.panel import Panel
16
+ from rich.tree import Tree
17
+
18
+
19
+ class TemplateEngine:
20
+ """Jinja2 template engine for generating project files."""
21
+
22
+ def __init__(self, template_dir: Optional[str] = None) -> None:
23
+ """Initialize the template engine.
24
+
25
+ Args:
26
+ template_dir: Base directory for templates. If not provided,
27
+ uses the gentem templates directory.
28
+ """
29
+ if template_dir:
30
+ self.template_dir = Path(template_dir)
31
+ else:
32
+ # Find the package directory
33
+ package_dir = Path(__file__).parent.parent
34
+ self.template_dir = package_dir / "templates"
35
+
36
+ self.env = Environment(
37
+ loader=FileSystemLoader(str(self.template_dir)),
38
+ autoescape=True,
39
+ trim_blocks=True,
40
+ lstrip_blocks=True,
41
+ )
42
+
43
+ def get_template(self, template_path: str) -> "jinja2.Template":
44
+ """Get a template by path.
45
+
46
+ Args:
47
+ template_path: Path to the template relative to template_dir.
48
+
49
+ Returns:
50
+ The Jinja2 template.
51
+
52
+ Raises:
53
+ FileNotFoundError: If template not found.
54
+ """
55
+ try:
56
+ return self.env.get_template(template_path)
57
+ except TemplateSyntaxError as e:
58
+ raise TemplateSyntaxError(
59
+ f"Syntax error in template {template_path}: {e.message}",
60
+ lineno=e.lineno,
61
+ ) from e
62
+ except UndefinedError as e:
63
+ raise UndefinedError(
64
+ f"Undefined variable in template {template_path}: {e.message}"
65
+ ) from e
66
+
67
+ def render_template(
68
+ self,
69
+ template_path: str,
70
+ context: Dict[str, Any],
71
+ ) -> str:
72
+ """Render a template with the given context.
73
+
74
+ Args:
75
+ template_path: Path to the template relative to template_dir.
76
+ context: Variables to pass to the template.
77
+
78
+ Returns:
79
+ The rendered template content.
80
+ """
81
+ template = self.get_template(template_path)
82
+ return template.render(**context)
83
+
84
+ def render_file(
85
+ self,
86
+ template_path: str,
87
+ context: Dict[str, Any],
88
+ output_path: Path,
89
+ ) -> None:
90
+ """Render a template to a file.
91
+
92
+ Args:
93
+ template_path: Path to the template relative to template_dir.
94
+ context: Variables to pass to the template.
95
+ output_path: Path to write the rendered file.
96
+ """
97
+ content = self.render_template(template_path, context)
98
+
99
+ # Ensure parent directory exists
100
+ output_path.parent.mkdir(parents=True, exist_ok=True)
101
+
102
+ # Write the file
103
+ output_path.write_text(content, encoding="utf-8")
104
+
105
+ def list_templates(self, subdir: Optional[str] = None) -> list[str]:
106
+ """List all templates in a subdirectory.
107
+
108
+ Args:
109
+ subdir: Subdirectory within template_dir to list.
110
+
111
+ Returns:
112
+ List of template paths.
113
+ """
114
+ search_dir = self.template_dir
115
+ if subdir:
116
+ search_dir = search_dir / subdir
117
+
118
+ if not search_dir.exists():
119
+ return []
120
+
121
+ templates = []
122
+ for root, _, files in os.walk(search_dir):
123
+ for file in files:
124
+ if file.endswith((".j2", ".jinja2")):
125
+ rel_path = Path(root).relative_to(self.template_dir)
126
+ templates.append(str(rel_path / file))
127
+
128
+ return sorted(templates)
129
+
130
+ def preview_tree(
131
+ self,
132
+ template_paths: list[str],
133
+ context: Dict[str, Any],
134
+ ) -> Tree:
135
+ """Preview the file tree that would be generated.
136
+
137
+ Args:
138
+ template_paths: List of template paths to render.
139
+ context: Variables to pass to the templates.
140
+
141
+ Returns:
142
+ A Rich Tree showing the file structure.
143
+ """
144
+ tree = Tree("Project Structure", guide_style="bold.cyan")
145
+
146
+ for template_path in template_paths:
147
+ # Get the output path (remove .j2 extension)
148
+ output_path = template_path
149
+ if output_path.endswith((".j2", ".jinja2")):
150
+ output_path = output_path[:-3] # Remove .j2
151
+
152
+ # Render the path with context
153
+ try:
154
+ rendered_path = self.render_template(
155
+ f"_paths/{output_path}.path.j2", context
156
+ )
157
+ except Exception:
158
+ # Fallback to template path
159
+ rendered_path = output_path
160
+
161
+ # Add to tree
162
+ parts = Path(rendered_path).parts
163
+ current = tree
164
+ for i, part in enumerate(parts):
165
+ is_last = i == len(parts) - 1
166
+ if is_last:
167
+ current.add(f"[cyan]{part}[/]")
168
+ else:
169
+ # Find or create branch
170
+ found = False
171
+ for child in current.children:
172
+ if child.label and str(child.label).strip("[]") == part:
173
+ current = child
174
+ found = True
175
+ break
176
+ if not found:
177
+ current = current.add(f"[bold]{part}/[/]")
178
+
179
+ return tree
180
+
181
+
182
+ # Global template engine instance
183
+ _engine: Optional[TemplateEngine] = None
184
+
185
+
186
+ def get_template_engine() -> TemplateEngine:
187
+ """Get the global template engine instance."""
188
+ global _engine
189
+ if _engine is None:
190
+ _engine = TemplateEngine()
191
+ return _engine
@@ -0,0 +1,13 @@
1
+ """Utilities package for Gentem."""
2
+
3
+ from gentem.utils.validators import (
4
+ validate_project_name,
5
+ validate_python_identifier,
6
+ validate_license_type,
7
+ )
8
+
9
+ __all__ = [
10
+ "validate_project_name",
11
+ "validate_python_identifier",
12
+ "validate_license_type",
13
+ ]
@@ -0,0 +1,158 @@
1
+ """Input validators for Gentem."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+
8
+ class ValidationError(Exception):
9
+ """Raised when validation fails."""
10
+
11
+ pass
12
+
13
+
14
+ def validate_python_identifier(name: str) -> bool:
15
+ """Check if the name is a valid Python identifier."""
16
+ pattern = r"^[a-zA-Z_][a-zA-Z0-9_]*$"
17
+ return bool(re.match(pattern, name))
18
+
19
+
20
+ def validate_project_name(name: str) -> str:
21
+ """Validate and return the project name.
22
+
23
+ Args:
24
+ name: The project name to validate.
25
+
26
+ Returns:
27
+ The validated project name.
28
+
29
+ Raises:
30
+ ValidationError: If the name is invalid.
31
+ """
32
+ if not name:
33
+ raise ValidationError("Project name cannot be empty.")
34
+
35
+ if not validate_python_identifier(name):
36
+ raise ValidationError(
37
+ f"'{name}' is not a valid Python project name. "
38
+ "Use only letters, numbers, and underscores, starting with a letter or underscore."
39
+ )
40
+
41
+ # Check for Python reserved words
42
+ reserved_words = {
43
+ "and", "as", "assert", "async", "await", "break", "class", "continue",
44
+ "def", "del", "elif", "else", "except", "finally", "for", "from",
45
+ "global", "if", "import", "in", "is", "lambda", "nonlocal", "not",
46
+ "or", "pass", "raise", "return", "try", "while", "with", "yield", "True",
47
+ "False", "None",
48
+ }
49
+
50
+ if name.lower() in reserved_words:
51
+ raise ValidationError(
52
+ f"'{name}' is a Python reserved word and cannot be used as a project name."
53
+ )
54
+
55
+ return name
56
+
57
+
58
+ def validate_license_type(license_type: str) -> str:
59
+ """Validate and normalize the license type.
60
+
61
+ Args:
62
+ license_type: The license type to validate.
63
+
64
+ Returns:
65
+ The normalized license type.
66
+
67
+ Raises:
68
+ ValidationError: If the license type is invalid.
69
+ """
70
+ valid_licenses = {"mit", "apache", "gpl", "bsd", "none", ""}
71
+
72
+ normalized = license_type.lower().strip()
73
+ if normalized not in valid_licenses:
74
+ raise ValidationError(
75
+ f"Invalid license type: '{license_type}'. "
76
+ f"Valid options are: {', '.join(sorted(valid_licenses))}"
77
+ )
78
+
79
+ return normalized
80
+
81
+
82
+ def validate_project_type(project_type: str) -> str:
83
+ """Validate and normalize the project type.
84
+
85
+ Args:
86
+ project_type: The project type to validate.
87
+
88
+ Returns:
89
+ The normalized project type.
90
+
91
+ Raises:
92
+ ValidationError: If the project type is invalid.
93
+ """
94
+ valid_types = {"library", "cli", "script"}
95
+
96
+ normalized = project_type.lower().strip()
97
+ if normalized not in valid_types:
98
+ raise ValidationError(
99
+ f"Invalid project type: '{project_type}'. "
100
+ f"Valid options are: {', '.join(sorted(valid_types))}"
101
+ )
102
+
103
+ return normalized
104
+
105
+
106
+ def validate_db_type(db_type: str) -> Optional[str]:
107
+ """Validate the database type.
108
+
109
+ Args:
110
+ db_type: The database type to validate.
111
+
112
+ Returns:
113
+ The normalized database type or None.
114
+
115
+ Raises:
116
+ ValidationError: If the database type is invalid.
117
+ """
118
+ if not db_type:
119
+ return None
120
+
121
+ valid_db_types = {"asyncpg", "sqlite", "postgres", "postgresql"}
122
+
123
+ normalized = db_type.lower().strip()
124
+ if normalized not in valid_db_types:
125
+ raise ValidationError(
126
+ f"Invalid database type: '{db_type}'. "
127
+ f"Valid options are: {', '.join(sorted(valid_db_types))} or empty for none."
128
+ )
129
+
130
+ # Normalize postgres/postgresql to asyncpg for now
131
+ if normalized in {"postgres", "postgresql"}:
132
+ normalized = "asyncpg"
133
+
134
+ return normalized
135
+
136
+
137
+ def validate_output_path(path: str, dry_run: bool = False) -> Path:
138
+ """Validate the output path.
139
+
140
+ Args:
141
+ path: The path to validate.
142
+ dry_run: Whether this is a dry run.
143
+
144
+ Returns:
145
+ The validated Path object.
146
+
147
+ Raises:
148
+ ValidationError: If the path is invalid.
149
+ """
150
+ output_path = Path(path)
151
+
152
+ if output_path.exists() and not dry_run:
153
+ raise ValidationError(
154
+ f"Directory '{output_path}' already exists. "
155
+ "Please choose a different project name or remove the existing directory."
156
+ )
157
+
158
+ return output_path
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: gentem
3
+ Version: 0.1.3
4
+ Summary: A Python CLI template boilerplate generator
5
+ Author-email: Abu Bakr <mabs2406@gmail.com>
6
+ License: MIT
7
+ Keywords: cli,boilerplate,template,generator,scaffolding
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: typer>=0.9.0
21
+ Requires-Dist: jinja2>=3.1.0
22
+ Requires-Dist: rich>=13.0.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.4.0; extra == "dev"
25
+ Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
26
+ Requires-Dist: black>=23.0.0; extra == "dev"
27
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
28
+ Requires-Dist: mypy>=1.5.0; extra == "dev"
29
+ Requires-Dist: types-pyyaml>=6.0.0; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # Gentem
33
+
34
+ ![Python Version](https://img.shields.io/pypi/pyversions/gentem)
35
+ ![License](https://img.shields.io/pypi/l/gentem)
36
+ ![PyPI Version](https://img.shields.io/pypi/v/gentem)
37
+ ![PyPI Status](https://img.shields.io/pypi/status/gentem)
38
+
39
+ ![Downloads](https://img.shields.io/pypi/dm/gentem)
40
+ ![Last Commit](https://img.shields.io/github/last-commit/knightlesssword/gentem)
41
+ ![Contributors](https://img.shields.io/github/contributors/knightlesssword/gentem)
42
+
43
+
44
+ A Python CLI template boilerplate generator for quickly scaffolding Python projects.
45
+
46
+ ## Features
47
+
48
+ - **Project Scaffolding**: Generate Python projects with a single command
49
+ - **Multiple Project Types**: Support for library, CLI tool, and script projects
50
+ - **FastAPI Templates**: Pre-configured FastAPI project templates with optional database support
51
+ - **Opinionated Structure**: Best practices baked into every template
52
+ - **Interactive Preview**: `--dry-run` option to preview before creating files
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ pip install gentem
58
+ ```
59
+
60
+ Or install from source:
61
+
62
+ ```bash
63
+ git clone https://github.com/knightlesssword/gentem.git
64
+ cd gentem
65
+ pip install -e .
66
+ ```
67
+
68
+ ## Usage
69
+
70
+ ### Creating a New Project
71
+
72
+ ```bash
73
+ # Create a library project
74
+ gentem new mylib --type library
75
+
76
+ # Create a CLI tool project
77
+ gentem new mycli --type cli
78
+
79
+ # Create a simple script project
80
+ gentem new myscript --type script
81
+
82
+ # With author and description
83
+ gentem new mylib --type library --author "John Doe" --description "My library"
84
+
85
+ # With license
86
+ gentem new mylib --type library --license mit
87
+ gentem new mylib --type library --license apache
88
+ gentem new mylib --type library --license gpl
89
+
90
+ # Preview without creating files
91
+ gentem new mylib --type library --dry-run
92
+ ```
93
+
94
+ ### Creating a FastAPI Project
95
+
96
+ ```bash
97
+ # Create a basic FastAPI project
98
+ gentem fastapi myapi
99
+
100
+ # Create with async mode and lifespan
101
+ gentem fastapi myapi --async
102
+
103
+ # Create with database support (asyncpg)
104
+ gentem fastapi myapi --db asyncpg
105
+
106
+ # Combine options
107
+ gentem fastapi myapi --async --db asyncpg --author "John Doe"
108
+ ```
109
+
110
+ ## Project Structure
111
+
112
+ ### Library Template
113
+ ```
114
+ mylib/
115
+ ├── src/
116
+ │ └── mylib/
117
+ │ ├── __init__.py
118
+ │ └── core.py
119
+ ├── tests/
120
+ │ ├── __init__.py
121
+ │ └── test_core.py
122
+ ├── pyproject.toml
123
+ ├── README.md
124
+ ├── LICENSE
125
+ └── .gitignore
126
+ ```
127
+
128
+ ### FastAPI Template
129
+ ```
130
+ myapi/
131
+ ├── src/
132
+ │ └── myapi/
133
+ │ ├── __init__.py
134
+ │ ├── main.py # FastAPI application
135
+ │ ├── core/
136
+ │ │ ├── __init__.py
137
+ │ │ ├── config.py # Settings
138
+ │ │ └── exceptions.py # Custom exceptions
139
+ │ ├── deps/
140
+ │ │ └── __init__.py # Dependencies
141
+ │ ├── utils/
142
+ │ │ └── __init__.py # Utility functions
143
+ │ ├── v1/
144
+ │ │ ├── __init__.py
145
+ │ │ └── apis/
146
+ │ │ ├── __init__.py
147
+ │ │ └── routes.py # API routes
148
+ │ ├── services/
149
+ │ │ └── __init__.py # Business logic
150
+ │ ├── schemas/
151
+ │ │ └── __init__.py # Pydantic schemas
152
+ │ └── models/
153
+ │ └── __init__.py # SQLAlchemy models
154
+ ├── .env
155
+ ├── requirements.txt
156
+ ├── .gitignore
157
+ └── README.md
158
+ ```
159
+
160
+ ## Development
161
+
162
+ ```bash
163
+ # Install development dependencies
164
+ pip install -e ".[dev]"
165
+
166
+ # Run tests
167
+ pytest
168
+
169
+ # Format code
170
+ black .
171
+ ruff check --fix .
172
+
173
+ # Type checking
174
+ mypy .
175
+ ```
176
+
177
+ ## Contributing
178
+
179
+ Contributions are welcome! Please feel free to submit a Pull Request.
180
+
181
+ ## License
182
+
183
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,15 @@
1
+ gentem/__init__.py,sha256=gW0VRbNnpFuyVzLPzO7A0vp0O0bs5oIV-cQx7RBX30E,86
2
+ gentem/__main__.py,sha256=1j3yOSbbv3Yfrr9HMuQD2zes6SpiiqwIksPSFcntlps,114
3
+ gentem/cli.py,sha256=tTgEUTS78nZBsZ0DyJb9vMLWu1Ow8L7KVetRRe8BCNU,4007
4
+ gentem/template_engine.py,sha256=WWu5m1h63lD0pxVHUSftEnFFWNf1OYqHQ01pGvb9qq0,6019
5
+ gentem/commands/__init__.py,sha256=_dNABeV14JFMeOB1htS-bzqj87EA99X1VSXw-37QIhM,36
6
+ gentem/commands/fastapi.py,sha256=BUH4Y-KkOA56XJNO01zyFlfbMClNlNT9KOWE0vMiolI,20267
7
+ gentem/commands/new.py,sha256=OYTTSBz9X6MObYKpIKPFhyzQJ3PyU1owcfxwL1pH6XQ,22912
8
+ gentem/utils/__init__.py,sha256=TJuXgn7gV-zF8HZuQv3uU-BlwlU1mJpiBDYoeliCFWo,283
9
+ gentem/utils/validators.py,sha256=lFDd1Qn-F2Npyq0illN4vI1SehKxf2qm2bIeWd3mwHM,4389
10
+ gentem-0.1.3.dist-info/licenses/LICENSE,sha256=ng7Oliq_MR4vJnb76Dg2QccUXSozR4-bIRzNTOvtGUc,1086
11
+ gentem-0.1.3.dist-info/METADATA,sha256=EE6QeyVV8gQgEX-0jFBF3XW-sTz_04HCeMU_bqmSAEE,5095
12
+ gentem-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
+ gentem-0.1.3.dist-info/entry_points.txt,sha256=M2X3uoUyPS1rcKCwy2hpT3PT1KyAhoOXCfvGoxj0NLM,43
14
+ gentem-0.1.3.dist-info/top_level.txt,sha256=kGBfHc-pTzlx-A68GISTSLXtBd3__eBad_zXoBW4-wA,7
15
+ gentem-0.1.3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gentem = gentem.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abu Bakr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ gentem