uvpg 1.0.1__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.

Potentially problematic release.


This version of uvpg might be problematic. Click here for more details.

uvpg/__init__.py ADDED
File without changes
uvpg/main.py ADDED
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ uvpg - UV Project generator based on monorepo template with uv workspaces.
4
+
5
+ Usage:
6
+ uvpg my-project
7
+ uvpg my-project --package my-lib
8
+ uvpg my-project -p lib1 -p lib2
9
+ """
10
+
11
+ import argparse
12
+ import platform
13
+ import re
14
+ import subprocess
15
+ import sys
16
+ from datetime import UTC, datetime
17
+
18
+ # ============================================================================
19
+ # Configuration
20
+ # ============================================================================
21
+ from importlib.metadata import version
22
+ from pathlib import Path
23
+
24
+ from uvpg.templates import (
25
+ TEMPLATE_CLEAN_SCRIPT,
26
+ TEMPLATE_COMPOSE,
27
+ TEMPLATE_DOCKERFILE,
28
+ TEMPLATE_DOCKERIGNORE,
29
+ TEMPLATE_GITIGNORE,
30
+ TEMPLATE_LICENSE_MIT,
31
+ TEMPLATE_MAIN_APP,
32
+ TEMPLATE_PACKAGE_MAIN,
33
+ TEMPLATE_PYPROJECT_PACKAGE,
34
+ TEMPLATE_PYPROJECT_ROOT,
35
+ TEMPLATE_README,
36
+ TEMPLATE_VSCODE_EXTENSIONS,
37
+ TEMPLATE_VSCODE_SETTINGS,
38
+ )
39
+
40
+ VERSION: str = version(distribution_name="uvpg")
41
+ AUTHORS_NAME = "John Doe"
42
+ AUTHORS_EMAIL = "john@example.com"
43
+ PYTHON_VERSION_DEFAULT = f"{sys.version_info.major}.{sys.version_info.minor}"
44
+
45
+
46
+ # ============================================================================
47
+ # Functions
48
+ # ============================================================================
49
+
50
+
51
+ def run_command(cmd: list[str], cwd: Path | None = None) -> bool:
52
+ """Execute command and return success status."""
53
+ try:
54
+ subprocess.run(cmd, cwd=cwd, check=True, capture_output=True) # noqa: S603
55
+ except subprocess.CalledProcessError as e:
56
+ print(f"Error running {' '.join(cmd)}: {e.stderr.decode()}")
57
+ return False
58
+ else:
59
+ return True
60
+
61
+
62
+ def create_package(root: Path, name: str, python_version: str = PYTHON_VERSION_DEFAULT) -> bool:
63
+ """Create a package inside packages/."""
64
+ package_name = name.replace("-", "_")
65
+ package_dir = root / "packages" / package_name
66
+
67
+ if package_dir.exists():
68
+ print(f"Package '{name}' already exists!")
69
+ return False
70
+
71
+ print(f"Creating package: {name}")
72
+
73
+ # Package structure
74
+ src_dir = package_dir / "src" / package_name
75
+ src_dir.mkdir(parents=True)
76
+ (package_dir / "tests").mkdir()
77
+
78
+ # Files
79
+ (package_dir / "pyproject.toml").write_text(
80
+ TEMPLATE_PYPROJECT_PACKAGE.format(
81
+ name=name,
82
+ package_name=package_name,
83
+ python_version=python_version,
84
+ )
85
+ )
86
+ (src_dir / "__init__.py").touch()
87
+ (src_dir / "main.py").write_text(TEMPLATE_PACKAGE_MAIN.format(name=name))
88
+ (package_dir / "tests" / "__init__.py").touch()
89
+
90
+ # Register package in root pyproject.toml
91
+ register_package_in_root(root, package_name)
92
+
93
+ return True
94
+
95
+
96
+ def register_package_in_root(root: Path, package_name: str) -> None:
97
+ """Register a package in root pyproject.toml dependencies and sources."""
98
+ pyproject_path = root / "pyproject.toml"
99
+ if not pyproject_path.exists():
100
+ return
101
+
102
+ content = pyproject_path.read_text()
103
+
104
+ # Add to dependencies = []
105
+ if "dependencies = []" in content:
106
+ content = content.replace("dependencies = []", f'dependencies = ["{package_name}"]')
107
+ elif "dependencies = [" in content:
108
+ # Find the dependencies line and add the package
109
+ pattern = r"dependencies = \[([^\]]*)\]"
110
+ match = re.search(pattern, content)
111
+ if match:
112
+ current_deps = match.group(1).strip()
113
+ if current_deps and f'"{package_name}"' not in current_deps:
114
+ new_deps = f'{current_deps}, "{package_name}"'
115
+ content = re.sub(pattern, f"dependencies = [{new_deps}]", content)
116
+ elif not current_deps:
117
+ content = re.sub(pattern, f'dependencies = ["{package_name}"]', content)
118
+
119
+ # Add to [tool.uv.sources]
120
+ source_entry = f"{package_name} = {{ workspace = true }}"
121
+ if "[tool.uv.sources]" in content and f"{package_name} = " not in content:
122
+ content = content.replace("[tool.uv.sources]", f"[tool.uv.sources]\n{source_entry}")
123
+
124
+ pyproject_path.write_text(content)
125
+
126
+
127
+ def create_project(
128
+ name: str,
129
+ packages: list[str] | None = None,
130
+ python_version: str = PYTHON_VERSION_DEFAULT,
131
+ authors_name: str = AUTHORS_NAME,
132
+ authors_email: str = AUTHORS_EMAIL,
133
+ ) -> bool:
134
+ """Create complete project with monorepo structure."""
135
+ root = Path(name).resolve()
136
+
137
+ # If directory exists and has pyproject.toml, only add packages
138
+ if root.exists() and (root / "pyproject.toml").exists():
139
+ if packages:
140
+ print(f"Adding packages to existing project: {root}")
141
+ for pkg in packages:
142
+ create_package(root, pkg, python_version)
143
+ print("\nRun 'uv sync' to update dependencies.")
144
+ return True
145
+ print(f"Project already exists at {root}")
146
+ return False
147
+
148
+ print(f"Creating project: {name} (Python {python_version})")
149
+
150
+ # Create directory structure
151
+ root.mkdir(exist_ok=True)
152
+ (root / "src" / "app").mkdir(parents=True)
153
+ (root / "packages").mkdir()
154
+ (root / "scripts").mkdir()
155
+ (root / "tests").mkdir()
156
+ (root / ".vscode").mkdir()
157
+
158
+ # Create root files
159
+ py_target = f"py{python_version.replace('.', '')}"
160
+ (root / "pyproject.toml").write_text(
161
+ TEMPLATE_PYPROJECT_ROOT.format(
162
+ name=name,
163
+ python_version=python_version,
164
+ py_target=py_target,
165
+ authors_name=authors_name,
166
+ authors_email=authors_email,
167
+ )
168
+ )
169
+ (root / "README.md").write_text(TEMPLATE_README.format(name=name))
170
+ (root / "LICENSE").write_text(
171
+ TEMPLATE_LICENSE_MIT.format(year=datetime.now(tz=UTC).year, authors_name=authors_name)
172
+ )
173
+ (root / ".gitignore").write_text(TEMPLATE_GITIGNORE)
174
+ (root / ".python-version").write_text(f"{python_version}\n")
175
+ (root / "Dockerfile").write_text(TEMPLATE_DOCKERFILE.format(python_version=python_version))
176
+ (root / ".dockerignore").write_text(TEMPLATE_DOCKERIGNORE)
177
+ (root / "compose.yaml").write_text(TEMPLATE_COMPOSE)
178
+
179
+ # Create VSCode config (detect OS for python path)
180
+ if platform.system() == "Windows":
181
+ python_interpreter_path = "${workspaceFolder}\\.venv\\Scripts\\python.exe"
182
+ else:
183
+ python_interpreter_path = "${workspaceFolder}/.venv/bin/python"
184
+
185
+ (root / ".vscode" / "settings.json").write_text(
186
+ TEMPLATE_VSCODE_SETTINGS.format(python_interpreter_path=python_interpreter_path)
187
+ )
188
+ (root / ".vscode" / "extensions.json").write_text(TEMPLATE_VSCODE_EXTENSIONS)
189
+
190
+ # Create main app
191
+ (root / "src" / "app" / "__init__.py").touch()
192
+ (root / "src" / "app" / "main.py").write_text(TEMPLATE_MAIN_APP)
193
+ (root / "tests" / "__init__.py").touch()
194
+
195
+ # Cleanup script
196
+ clean_script = root / "scripts" / "project_clean.sh"
197
+ clean_script.write_text(TEMPLATE_CLEAN_SCRIPT)
198
+ clean_script.chmod(0o755)
199
+
200
+ # Create initial packages
201
+ if packages:
202
+ for pkg in packages:
203
+ create_package(root, pkg, python_version)
204
+
205
+ # Initialize with uv
206
+ print("Initializing with uv...")
207
+ if not run_command(["uv", "sync"], cwd=root):
208
+ print("Warning: failed to run 'uv sync'")
209
+
210
+ # Initialize git
211
+ print("Initializing git...")
212
+ run_command(["git", "init"], cwd=root)
213
+
214
+ print(f"\n✓ Project created at: {root}")
215
+ print("\nNext steps:")
216
+ print(f" cd {name}")
217
+ print(" uv sync")
218
+ print(" uv run python src/app/main.py")
219
+
220
+ return True
221
+
222
+
223
+ def main() -> int:
224
+ parser = argparse.ArgumentParser(
225
+ prog="uvpg",
226
+ description="uvpg - UV Project generator for Python monorepos with uv workspaces.",
227
+ formatter_class=argparse.RawDescriptionHelpFormatter,
228
+ epilog=f"""\
229
+ Examples:
230
+ %(prog)s my-project # Create new project (Python {PYTHON_VERSION_DEFAULT})
231
+ %(prog)s my-project --python {PYTHON_VERSION_DEFAULT}
232
+ %(prog)s my-project -p utils -p core # With initial packages
233
+ %(prog)s my-project --author "John Doe" --email "john@example.com"
234
+ %(prog)s . -p new-lib # Add package to current project
235
+ """,
236
+ )
237
+ parser.add_argument(
238
+ "-v",
239
+ "--version",
240
+ action="version",
241
+ version=f"%(prog)s {VERSION}",
242
+ )
243
+ parser.add_argument("name", help="Project name or '.' for current directory")
244
+ parser.add_argument(
245
+ "-p",
246
+ "--package",
247
+ action="append",
248
+ dest="packages",
249
+ metavar="NAME",
250
+ help="Package name to create (can be repeated)",
251
+ )
252
+ parser.add_argument(
253
+ "--python",
254
+ default=PYTHON_VERSION_DEFAULT,
255
+ metavar="VERSION",
256
+ help=f"Python version (default: {PYTHON_VERSION_DEFAULT})",
257
+ )
258
+ parser.add_argument(
259
+ "--author",
260
+ default=AUTHORS_NAME,
261
+ metavar="NAME",
262
+ help=f"Author name (default: {AUTHORS_NAME})",
263
+ )
264
+ parser.add_argument(
265
+ "--email",
266
+ default=AUTHORS_EMAIL,
267
+ metavar="EMAIL",
268
+ help=f"Author email (default: {AUTHORS_EMAIL})",
269
+ )
270
+
271
+ args = parser.parse_args()
272
+
273
+ # Use current directory if '.'
274
+ name = args.name
275
+ if name == ".":
276
+ name = str(Path.cwd())
277
+
278
+ success = create_project(name, args.packages, args.python, args.author, args.email)
279
+ return 0 if success else 1
280
+
281
+
282
+ if __name__ == "__main__":
283
+ sys.exit(main())
uvpg/templates.py ADDED
@@ -0,0 +1,687 @@
1
+ """Templates for uvpg project generator."""
2
+
3
+ TEMPLATE_PYPROJECT_ROOT = """\
4
+ # ============================
5
+ # UV Instructions
6
+ # ============================
7
+ # # On macOS and Linux.
8
+ # curl -LsSf https://astral.sh/uv/install.sh | sh
9
+ # # On Windows (PowerShell).
10
+ # powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
11
+ # uv --version
12
+ # # Initialized project
13
+ # uv init
14
+ # # Initialized project with python version
15
+ # uv init --python {python_version}
16
+ # # Update uv to the latest version
17
+ # uv self update
18
+ # # Resolve and install dependencies
19
+ # uv sync
20
+ # # Upgrade all packages in the workspace
21
+ # uv sync --upgrade
22
+ # # Cache directory
23
+ # uv cache dir
24
+ # # Install python versions
25
+ # uv python install {python_version}
26
+ # # Set project python version
27
+ # uv python pin {python_version}
28
+
29
+ # ============================
30
+ # Project Metadata
31
+ # ============================
32
+ [project]
33
+ name = "{name}"
34
+ version = "1.0.0"
35
+ description = ""
36
+ readme = "README.md"
37
+ license = "MIT"
38
+ authors = [{{ name = "{authors_name}", email = "{authors_email}" }}]
39
+ requires-python = ">={python_version}"
40
+ dependencies = ["uvicorn", "fastapi"]
41
+
42
+ # ============================
43
+ # Dependency Groups
44
+ # dependencies: "ruff", "ty", "watchfiles", "pytest", "pytest-cov"
45
+ # ============================
46
+ [dependency-groups]
47
+ dev = ["ruff", "ty", "watchfiles"]
48
+
49
+ # # ============================
50
+ # # UV Configuration
51
+ # # ============================
52
+ # [tool.uv]
53
+ # link-mode = "copy"
54
+
55
+ # ============================
56
+ # Workspace Configuration
57
+ # https://docs.astral.sh/uv/concepts/projects/workspaces/#workspace-layouts
58
+ # ============================
59
+ [tool.uv.workspace]
60
+ members = ["packages/*"]
61
+
62
+ [tool.uv.sources]
63
+
64
+ # ============================
65
+ # Scripts Entry Points
66
+ # ============================
67
+ [project.scripts]
68
+ app = "app.main:main"
69
+
70
+ # ============================
71
+ # Build System Configuration
72
+ # ============================
73
+ [build-system]
74
+ requires = ["hatchling"]
75
+ build-backend = "hatchling.build"
76
+
77
+ [tool.hatch.build.targets.wheel]
78
+ packages = ["src/app"]
79
+
80
+ # ============================
81
+ # Linting Configuration (Ruff)
82
+ # uv add ruff --dev
83
+ # ruff --version
84
+ # uv sync --upgrade-package ruff
85
+ # ============================
86
+ [tool.ruff]
87
+ line-length = 100
88
+ target-version = "{py_target}"
89
+ fix = true
90
+ show-fixes = true
91
+ indent-width = 4
92
+ exclude = ["venv", ".venv", "env", ".env", "node_modules", "__pycache__"]
93
+
94
+ [tool.ruff.lint]
95
+ select = ["ALL"]
96
+ ignore = [
97
+ "T201", # Checks for print statements,
98
+ "COM812", # Checks for the absence of trailing commas.
99
+ "INP001", # Checks for packages that are missing an __init__.py file.
100
+ "D", # All pydocstyle (D)
101
+ "ANN401", # Checks that function arguments are annotated with a more specific type than Any.
102
+ "ERA001", # Checks for commented-out Python code.
103
+ "A004", # Shadowing Python Builtin
104
+ "EXE001", # Shebang is present but file is not executable.
105
+ ]
106
+
107
+ [tool.ruff.lint.per-file-ignores]
108
+ "tests/**/*.py" = ["ANN201", "S101", "ANN001"]
109
+ "packages/**/tests/*.py" = ["ANN201", "S101", "ANN001"]
110
+
111
+ # ============================
112
+ # Typing Configuration (Ty)
113
+ # uv add ty --dev
114
+ # ty --version
115
+ # uv sync --upgrade-package ty
116
+ # ============================
117
+ [tool.ty.rules]
118
+ possibly-unresolved-reference = "warn"
119
+ # division-by-zero = "ignore"
120
+
121
+ # # ============================
122
+ # # Testing Configuration (pytest)
123
+ # # uv add pytest --dev
124
+ # # pytest --version
125
+ # # uv sync --upgrade-package pytest
126
+ # # ============================
127
+ # [tool.pytest.ini_options]
128
+ # addopts = "-vs --color=yes --tb=short --cov=packages --cov=src --cov-report=term-missing"
129
+ # testpaths = ["packages", "tests"]
130
+
131
+ # # ============================
132
+ # # Coverage Configuration (pytest-cov)
133
+ # # uv add pytest-cov --dev
134
+ # # pytest --version
135
+ # # uv sync --upgrade-package pytest-cov
136
+ # # ============================
137
+ # [tool.coverage.run]
138
+ # source = ["packages", "src"]
139
+ # omit = ["*/__pycache__/*", "*/tests/*"]
140
+
141
+ # [tool.coverage.report]
142
+ # exclude_lines = [
143
+ # "pragma: no cover", # Ignore pragma no cover ex.: def foo(): # pragma: no cover
144
+ # "if __name__ == .__main__.:", # Ignore if __name__ == "__main__" blocks
145
+ # ]
146
+ """
147
+
148
+ TEMPLATE_PYPROJECT_PACKAGE = """\
149
+ # ============================
150
+ # Project Metadata
151
+ # ============================
152
+ [project]
153
+ name = "{name}"
154
+ version = "1.0.0"
155
+ description = ""
156
+ requires-python = ">={python_version}"
157
+ dependencies = []
158
+
159
+ # # ============================
160
+ # # Scripts Entry Points
161
+ # # ============================
162
+ # [project.scripts]
163
+ # {package_name} = "{package_name}.main:hello"
164
+
165
+ # ============================
166
+ # Build System Configuration
167
+ # ============================
168
+ [build-system]
169
+ requires = ["hatchling"]
170
+ build-backend = "hatchling.build"
171
+
172
+ [tool.hatch.build.targets.wheel]
173
+ packages = ["src/{package_name}"]
174
+ """
175
+
176
+ TEMPLATE_README = """\
177
+ # {name}
178
+
179
+ ## Structure
180
+
181
+ ```
182
+ {name}/
183
+ ├── src/app/ # FastAPI application
184
+ ├── packages/ # Internal libraries (uv workspace)
185
+ ├── scripts/ # Utility scripts
186
+ ├── tests/ # Test files
187
+ ├── Dockerfile # Multi-stage Docker build
188
+ └── compose.yaml # Docker Compose config
189
+ ```
190
+
191
+ ## Getting Started
192
+
193
+ ### Local Development
194
+
195
+ ```bash
196
+ # Install dependencies
197
+ uv sync
198
+
199
+ # Run application
200
+ uv run app
201
+
202
+ # Or run directly
203
+ uv run python src/app/main.py
204
+
205
+ # Run with auto-reload
206
+ uv run watchfiles 'python src/app/main.py'
207
+ ```
208
+
209
+ ### Docker
210
+
211
+ ```bash
212
+ # Build and run
213
+ docker compose up --build
214
+
215
+ # Run with watch mode (auto-reload on file changes)
216
+ docker compose up --watch
217
+
218
+ # Stop
219
+ docker compose down
220
+ ```
221
+
222
+ ## API
223
+
224
+ Once running, access:
225
+
226
+ - **API:** http://localhost:8000
227
+ - **Docs:** http://localhost:8000/docs
228
+ - **ReDoc:** http://localhost:8000/redoc
229
+
230
+ ## Development Commands
231
+
232
+ ```bash
233
+ # Run tests
234
+ uv run pytest
235
+
236
+ # Lint
237
+ uv run ruff check .
238
+
239
+ # Type check
240
+ uv run ty check
241
+
242
+ # Format
243
+ uv run ruff format .
244
+
245
+ # Clean project
246
+ bash scripts/project_clean.sh
247
+ ```
248
+
249
+ ## Add new package
250
+
251
+ ```bash
252
+ uvpg . --package package-name
253
+ ```
254
+
255
+ ## Build
256
+
257
+ ```bash
258
+ uv build
259
+ ```
260
+ """
261
+
262
+ TEMPLATE_MAIN_APP = """\
263
+ \"\"\"Main application.\"\"\"
264
+
265
+ from datetime import UTC, datetime
266
+
267
+ import uvicorn
268
+ from fastapi import FastAPI
269
+
270
+ app = FastAPI()
271
+
272
+
273
+ @app.get("/")
274
+ async def home() -> dict[str, str | int]:
275
+ return {"message": "Hello, world!", "now": datetime.now(tz=UTC).isoformat()}
276
+
277
+
278
+ def main() -> None:
279
+ uvicorn.run(app, host="0.0.0.0", port=8000) # noqa: S104
280
+
281
+
282
+ if __name__ == "__main__":
283
+ main()
284
+ """
285
+
286
+ TEMPLATE_PACKAGE_MAIN = """\
287
+ \"\"\"{name} main module.\"\"\"
288
+
289
+
290
+ def hello() -> str:
291
+ return "Hello from {name}!"
292
+ """
293
+
294
+ TEMPLATE_CLEAN_SCRIPT = """\
295
+ #!/bin/bash
296
+ # Clean temporary files and caches
297
+
298
+ set -e
299
+
300
+ echo "Cleaning project..."
301
+
302
+ # Python caches
303
+ find . -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true
304
+ find . -type d -name '*.egg-info' -exec rm -rf {} + 2>/dev/null || true
305
+ find . -type f -name '*.pyc' -delete 2>/dev/null || true
306
+ find . -type d -name '.pytest_cache' -exec rm -rf {} + 2>/dev/null || true
307
+ find . -type d -name '.ruff_cache' -exec rm -rf {} + 2>/dev/null || true
308
+ find . -type f -name '*.Identifier' -delete 2>/dev/null || true
309
+
310
+ # Build artifacts
311
+ find . -type d -name "dist" -exec rm -rf {} + 2>/dev/null || true
312
+ find . -type d -name "build" -exec rm -rf {} + 2>/dev/null || true
313
+
314
+ echo "Cleanup complete!"
315
+ """
316
+
317
+ TEMPLATE_LICENSE_MIT = """\
318
+ MIT License
319
+
320
+ Copyright (c) {year} {authors_name}
321
+
322
+ Permission is hereby granted, free of charge, to any person obtaining a copy
323
+ of this software and associated documentation files (the "Software"), to deal
324
+ in the Software without restriction, including without limitation the rights
325
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
326
+ copies of the Software, and to permit persons to whom the Software is
327
+ furnished to do so, subject to the following conditions:
328
+
329
+ The above copyright notice and this permission notice shall be included in all
330
+ copies or substantial portions of the Software.
331
+
332
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
333
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
334
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
335
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
336
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
337
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
338
+ SOFTWARE.
339
+ """
340
+
341
+ TEMPLATE_GITIGNORE = """\
342
+ # Python
343
+ __pycache__/
344
+ *.py[cod]
345
+ *$py.class
346
+ *.egg-info/
347
+ dist/
348
+ build/
349
+ .eggs/
350
+
351
+ # Virtual environments
352
+ .env
353
+ .envrc
354
+ .venv
355
+ env/
356
+ venv/
357
+ ENV/
358
+ env.bak/
359
+ venv.bak/
360
+
361
+ # uv
362
+ .python-version
363
+
364
+ # Testing
365
+ htmlcov/
366
+ .tox/
367
+ .nox/
368
+ .coverage
369
+ .coverage.*
370
+ .cache
371
+ nosetests.xml
372
+ coverage.xml
373
+ *.cover
374
+ *.py.cover
375
+ .hypothesis/
376
+ .pytest_cache/
377
+ cover
378
+
379
+ # Ruff
380
+ .ruff_cache/
381
+
382
+ # IDEs
383
+ .idea/
384
+ *.swp
385
+ *.swo
386
+
387
+ # OS
388
+ .DS_Store
389
+ Thumbs.db
390
+ *.Identifier
391
+ """
392
+
393
+ TEMPLATE_VSCODE_SETTINGS = """\
394
+ {{
395
+ // VsCode general settings
396
+ "editor.fontSize": 14,
397
+ "window.zoomLevel": 0,
398
+ "explorer.compactFolders": false,
399
+ // Action Buttons settings
400
+ "actionButtons": {{
401
+ "defaultColor": "Lime",
402
+ "reloadButton": "$(refresh)",
403
+ "commands": [
404
+ {{
405
+ "name": "$(sync)",
406
+ "tooltip": "Reload Windows",
407
+ "useVsCodeApi": true,
408
+ "command": "workbench.action.reloadWindow"
409
+ }},
410
+ {{
411
+ "name": "$(trash)",
412
+ "tooltip": "Clean Project",
413
+ "singleInstance": true,
414
+ "cwd": "${{workspaceFolder}}",
415
+ "command": "bash scripts/project_clean.sh"
416
+ }},
417
+ {{
418
+ "name": "$(beaker)",
419
+ "tooltip": "Run Tests",
420
+ "singleInstance": true,
421
+ "cwd": "${{workspaceFolder}}",
422
+ "command": "uv run pytest"
423
+ }},
424
+ {{
425
+ "name": "$(terminal)",
426
+ "tooltip": "Open New Terminal",
427
+ "useVsCodeApi": true,
428
+ "command": "workbench.action.terminal.new"
429
+ }},
430
+ {{
431
+ "name": "$(versions)",
432
+ "tooltip": "Python Version",
433
+ "singleInstance": true,
434
+ "cwd": "${{workspaceFolder}}",
435
+ "command": "uv run python --version"
436
+ }},
437
+ {{
438
+ "name": "$(package)",
439
+ "tooltip": "UV Sync",
440
+ "singleInstance": true,
441
+ "cwd": "${{workspaceFolder}}",
442
+ "command": "uv sync"
443
+ }},
444
+ {{
445
+ "name": "$(build)",
446
+ "tooltip": "UV Build",
447
+ "singleInstance": true,
448
+ "cwd": "${{workspaceFolder}}",
449
+ "command": "uv build"
450
+ }},
451
+ {{
452
+ "name": "$(run)",
453
+ "tooltip": "UV Run",
454
+ "singleInstance": true,
455
+ "cwd": "${{workspaceFolder}}",
456
+ "command": "uv run src/app/main.py"
457
+ }},
458
+ {{
459
+ "name": "$(debug-alt)",
460
+ "tooltip": "Run Watch Files",
461
+ "singleInstance": true,
462
+ "cwd": "${{workspaceFolder}}",
463
+ "command": "uv run watchfiles 'python src/app/main.py'"
464
+ }}
465
+ ]
466
+ }},
467
+ // Code Runner settings
468
+ "code-runner.clearPreviousOutput": true,
469
+ "code-runner.ignoreSelection": true,
470
+ "code-runner.saveFileBeforeRun": true,
471
+ "code-runner.runInTerminal": true,
472
+ "code-runner.preserveFocus": false,
473
+ "code-runner.executorMap": {{
474
+ "python": "clear ; python -u"
475
+ }},
476
+ // Python settings
477
+ "[python]": {{
478
+ "editor.formatOnSave": true,
479
+ "editor.tabSize": 4,
480
+ "editor.insertSpaces": true,
481
+ "editor.codeActionsOnSave": {{
482
+ "source.fixAll": "explicit",
483
+ "source.organizeImports": "explicit"
484
+ }},
485
+ "editor.defaultFormatter": "charliermarsh.ruff"
486
+ }},
487
+ "python.defaultInterpreterPath": "{python_interpreter_path}",
488
+ "python.analysis.autoImportCompletions": true,
489
+ "python.terminal.activateEnvInCurrentTerminal": true,
490
+ "python.terminal.activateEnvironment": true,
491
+ "python.languageServer": "None",
492
+ "python.venvPath": ".venv",
493
+ // Ty settings (https://docs.astral.sh/ty/reference/editor-settings)
494
+ "ty.disableLanguageServices": false,
495
+ "ty.diagnosticMode": "workspace"
496
+ }}
497
+ """
498
+
499
+ TEMPLATE_VSCODE_EXTENSIONS = """\
500
+ {
501
+ // Search for extensions in the Marketplace: @recommended
502
+ "recommendations": [
503
+ "dracula-theme.theme-dracula",
504
+ "pkief.material-icon-theme",
505
+ "seunlanlege.action-buttons",
506
+ "mhutchie.git-graph",
507
+ "ms-python.python",
508
+ "formulahendry.code-runner",
509
+ "tamasfe.even-better-toml",
510
+ "charliermarsh.ruff",
511
+ "astral-sh.ty",
512
+ "tal7aouy.rainbow-bracket",
513
+ "ms-vscode-remote.remote-wsl"
514
+ ]
515
+ }
516
+ """
517
+
518
+ TEMPLATE_DOCKERFILE = """\
519
+ FROM debian:trixie-slim AS builder
520
+
521
+ RUN apt-get update \\
522
+ && apt-get install -y --no-install-recommends build-essential \\
523
+ && apt-get clean \\
524
+ && rm -rf /var/lib/apt/lists/*
525
+
526
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
527
+
528
+ ENV UV_COMPILE_BYTECODE=1 \\
529
+ UV_LINK_MODE=copy \\
530
+ UV_PYTHON_PREFERENCE=only-managed \\
531
+ UV_NO_DEV=1 \\
532
+ UV_PYTHON_INSTALL_DIR=/python
533
+
534
+ RUN uv python install {python_version}
535
+
536
+ WORKDIR /app
537
+
538
+ RUN --mount=type=cache,target=/root/.cache/uv \\
539
+ --mount=type=bind,source=uv.lock,target=uv.lock \\
540
+ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \\
541
+ --mount=type=bind,source=packages,target=packages \\
542
+ uv sync --frozen --no-install-project
543
+
544
+ COPY . /app
545
+
546
+ RUN --mount=type=cache,target=/root/.cache/uv \\
547
+ uv sync --frozen
548
+
549
+ ################################################################################
550
+ FROM debian:trixie-slim AS runtime
551
+
552
+ ARG UID=1000
553
+ ARG GID=1000
554
+ ARG USERNAME=python
555
+
556
+ ENV PYTHONUNBUFFERED=1 \\
557
+ PATH="/app/.venv/bin:${{PATH}}"
558
+
559
+ RUN groupadd --gid ${{GID}} ${{USERNAME}} \\
560
+ && useradd --uid ${{UID}} --gid ${{USERNAME}} --shell /bin/bash --create-home ${{USERNAME}}
561
+
562
+ COPY --from=builder --chown=${{UID}}:${{GID}} /python /python
563
+ COPY --from=builder --chown=${{UID}}:${{GID}} /app /app
564
+
565
+ USER ${{USERNAME}}
566
+
567
+ WORKDIR /app
568
+
569
+ EXPOSE 8000
570
+
571
+ CMD ["app"]
572
+ """
573
+
574
+ TEMPLATE_DOCKERIGNORE = """\
575
+ # ADDED
576
+ tests/
577
+ scripts/
578
+ docs/
579
+ Dockerfile*
580
+ *compose*.yaml
581
+ *compose*.yml
582
+ .python-version
583
+ model_cache/
584
+ TEMP.md
585
+ TMP.md
586
+ temp/
587
+ LICENSE
588
+ .ruff_cache/
589
+ # Git
590
+ .git
591
+ .gitignore
592
+ .gitattributes
593
+ # CI
594
+ .codeclimate.yml
595
+ .travis.yml
596
+ .taskcluster.yml
597
+ # Docker
598
+ docker-compose.yml
599
+ Dockerfile
600
+ .docker
601
+ .dockerignore
602
+ # Byte-compiled / optimized / DLL files
603
+ **/__pycache__/
604
+ **/*.py[cod]
605
+ # C extensions
606
+ *.so
607
+ # Distribution / packaging
608
+ .Python
609
+ env/
610
+ build/
611
+ develop-eggs/
612
+ dist/
613
+ downloads/
614
+ eggs/
615
+ lib/
616
+ lib64/
617
+ parts/
618
+ sdist/
619
+ var/
620
+ *.egg-info/
621
+ .installed.cfg
622
+ *.egg
623
+ # PyInstaller
624
+ # Usually these files are written by a python script from a template
625
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
626
+ *.manifest
627
+ *.spec
628
+ # Installer logs
629
+ pip-log.txt
630
+ pip-delete-this-directory.txt
631
+ # Unit test / coverage reports
632
+ htmlcov/
633
+ .tox/
634
+ .coverage
635
+ .cache
636
+ nosetests.xml
637
+ coverage.xml
638
+ # Translations
639
+ *.mo
640
+ *.pot
641
+ # Django stuff:
642
+ *.log
643
+ # Sphinx documentation
644
+ docs/_build/
645
+ # PyBuilder
646
+ target/
647
+ # Virtual environment
648
+ .env
649
+ .venv/
650
+ venv/
651
+ # PyCharm
652
+ .idea
653
+ # Python mode for VIM
654
+ .ropeproject
655
+ **/.ropeproject
656
+ # Vim swap files
657
+ **/*.swp
658
+ # VS Code
659
+ .vscode/
660
+ """
661
+
662
+ TEMPLATE_COMPOSE = """\
663
+ services:
664
+ app:
665
+ pull_policy: never
666
+ image: app
667
+ container_name: app
668
+ restart: unless-stopped
669
+ build:
670
+ context: .
671
+ dockerfile: Dockerfile
672
+ target: runtime
673
+ ports:
674
+ - 8000:8000
675
+ develop:
676
+ watch:
677
+ - action: sync+restart # this depends on the project
678
+ path: .
679
+ target: /app
680
+
681
+ ignore:
682
+ - .venv/
683
+ - __pycache__/
684
+
685
+ - action: rebuild
686
+ path: ./uv.lock
687
+ """
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.4
2
+ Name: uvpg
3
+ Version: 1.0.1
4
+ Summary: UV Project Generator - Create Python monorepos with workspaces.
5
+ Author-email: Lucas Maziero <lucas.mazie.ro@hotmail.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+
11
+ # uvpg - UV Project Generator
12
+
13
+ [![PyPI version](https://img.shields.io/pypi/v/uvpg)](https://pypi.org/project/uvpg/)
14
+ [![License](https://img.shields.io/pypi/l/uvpg)](https://github.com/lucasmaziero/uvpg/blob/main/LICENSE)
15
+ [![Release and Publish](https://github.com/lucasmaziero/uvpg/actions/workflows/release.yml/badge.svg)](https://github.com/lucasmaziero/uvpg/actions/workflows/release.yml)
16
+
17
+ A CLI tool to scaffold Python monorepo projects using [uv](https://docs.astral.sh/uv/) workspaces.
18
+
19
+ ## Features
20
+
21
+ - 🚀 Creates monorepo structure with uv workspaces
22
+ - 📦 Adds internal packages with automatic dependency registration
23
+ - 🐍 Configurable Python version
24
+ - 👤 Configurable author name and email
25
+ - 📄 MIT License auto-generated
26
+ - 🔧 Pre-configured tools: Ruff, Ty, pytest, coverage
27
+ - 💻 VSCode settings with recommended extensions
28
+ - 🖥️ Auto-detects OS for correct Python interpreter path
29
+ - 🐳 Docker support with multi-stage build and compose
30
+ - ⚡ FastAPI + Uvicorn ready
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ # Install from PyPI
36
+ pip install uvpg
37
+
38
+ # Or install from PyPI with uv
39
+ uv tool install uvpg
40
+
41
+ # Or clone the repository
42
+ git clone https://github.com/lucasmaziero/uvpg.git
43
+ cd uvpg
44
+
45
+ # Install with uv
46
+ uv sync
47
+ uv build
48
+ uv tool install dist/uvpg-*.whl
49
+
50
+ # Or install with pip
51
+ pip install dist/uvpg-*.whl --break-system-packages
52
+ ```
53
+
54
+ ## Uninstall
55
+
56
+ ```bash
57
+ # Via uv
58
+ uv tool uninstall uvpg
59
+
60
+ # Via pip
61
+ pip uninstall uvpg
62
+ ```
63
+
64
+ ## Usage
65
+
66
+ ### Create a new project
67
+
68
+ ```bash
69
+ # Basic project
70
+ uvpg my-project
71
+
72
+ # With specific Python version
73
+ uvpg my-project --python 3.14
74
+
75
+ # With author information
76
+ uvpg my-project --author "John Doe" --email "john@example.com"
77
+
78
+ # With initial packages
79
+ uvpg my-project -p core -p utils -p api
80
+
81
+ # Combining options
82
+ uvpg my-project --python 3.14 --author "John Doe" --email "john@example.com" -p core
83
+ ```
84
+
85
+ ### Add packages to existing project
86
+
87
+ ```bash
88
+ cd my-project
89
+ uvpg . -p new-package
90
+ ```
91
+
92
+ ### Show version
93
+
94
+ ```bash
95
+ uvpg --version
96
+ ```
97
+
98
+ ### Show help
99
+
100
+ ```bash
101
+ uvpg --help
102
+ ```
103
+
104
+ ## Generated Structure
105
+
106
+ ```
107
+ my-project/
108
+ ├── .vscode/
109
+ │ ├── settings.json # VSCode settings (Ruff, Ty, Action Buttons)
110
+ │ └── extensions.json # Recommended extensions
111
+ ├── packages/
112
+ │ └── core/
113
+ │ ├── pyproject.toml
114
+ │ ├── src/
115
+ │ │ └── core/
116
+ │ │ ├── __init__.py
117
+ │ │ └── main.py
118
+ │ └── tests/
119
+ ├── scripts/
120
+ │ └── project_clean.sh # Cleanup script
121
+ ├── src/
122
+ │ └── app/
123
+ │ ├── __init__.py
124
+ │ └── main.py # FastAPI application
125
+ ├── tests/
126
+ ├── .dockerignore
127
+ ├── .gitignore
128
+ ├── .python-version
129
+ ├── compose.yaml # Docker Compose config
130
+ ├── Dockerfile # Multi-stage Docker build
131
+ ├── LICENSE # MIT License
132
+ ├── pyproject.toml # Root config with workspace
133
+ ├── README.md
134
+ └── uv.lock
135
+ ```
136
+
137
+ ## Configuration
138
+
139
+ The generated `pyproject.toml` includes:
140
+
141
+ - **FastAPI + Uvicorn** - Web framework and ASGI server
142
+ - **Ruff** - Linting with `select = ["ALL"]`
143
+ - **Ty** - Type checking
144
+ - **pytest** - Testing (commented, ready to enable)
145
+ - **coverage** - Code coverage (commented, ready to enable)
146
+ - **uv workspaces** - Monorepo package management
147
+ - **hatchling** - Build system
148
+
149
+ ## Docker
150
+
151
+ Run with Docker Compose:
152
+
153
+ ```bash
154
+ # Build and run
155
+ docker compose up --build
156
+
157
+ # Run with watch mode (auto-reload)
158
+ docker compose up --watch
159
+ ```
160
+
161
+ ## Requirements
162
+
163
+ - Python >= 3.12
164
+ - [uv](https://docs.astral.sh/uv/) package manager
165
+
166
+ ## Development
167
+
168
+ ```bash
169
+ # Clone
170
+ git clone https://github.com/lucasmaziero/uvpg.git
171
+ cd uvpg
172
+
173
+ # Install dependencies
174
+ uv sync
175
+
176
+ # Run locally
177
+ uv run uvpg --help
178
+
179
+ # Run tests
180
+ uv run pytest
181
+
182
+ # Build
183
+ uv build
184
+ ```
185
+
186
+ ## License
187
+
188
+ MIT License - See [LICENSE](LICENSE) for details.
@@ -0,0 +1,8 @@
1
+ uvpg/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ uvpg/main.py,sha256=doSBwNONLCQNItximr0MrKkkgTTBMEGm8OO92R_LJjY,9325
3
+ uvpg/templates.py,sha256=7oOcalfQAwIpFTg_RBQkWd9PwI9xZH4GdZ2hNw6-LPg,15887
4
+ uvpg-1.0.1.dist-info/METADATA,sha256=BhTk_NEOJkoeC0vMUDzD4-ftRhGQJZZKlX9fDy_umwE,4191
5
+ uvpg-1.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
+ uvpg-1.0.1.dist-info/entry_points.txt,sha256=A2qYLAP0vHidOtba62Y8i665RHYt8sC9xMd2aeSwoB0,40
7
+ uvpg-1.0.1.dist-info/licenses/LICENSE,sha256=m-3xgK4dm033H8KWdXDGwqZb3nutJAR3MhDj6hX6WzE,1070
8
+ uvpg-1.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ uvpg = uvpg.main:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lucas Maziero
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.