pymelos 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.
- pymelos/__init__.py +63 -0
- pymelos/__main__.py +6 -0
- pymelos/cli/__init__.py +5 -0
- pymelos/cli/__main__.py +6 -0
- pymelos/cli/app.py +527 -0
- pymelos/cli/commands/__init__.py +1 -0
- pymelos/cli/commands/init.py +151 -0
- pymelos/commands/__init__.py +84 -0
- pymelos/commands/add.py +77 -0
- pymelos/commands/base.py +108 -0
- pymelos/commands/bootstrap.py +154 -0
- pymelos/commands/changed.py +161 -0
- pymelos/commands/clean.py +142 -0
- pymelos/commands/exec.py +116 -0
- pymelos/commands/list.py +128 -0
- pymelos/commands/release.py +258 -0
- pymelos/commands/run.py +160 -0
- pymelos/compat.py +14 -0
- pymelos/config/__init__.py +47 -0
- pymelos/config/loader.py +132 -0
- pymelos/config/schema.py +236 -0
- pymelos/errors.py +139 -0
- pymelos/execution/__init__.py +32 -0
- pymelos/execution/parallel.py +249 -0
- pymelos/execution/results.py +172 -0
- pymelos/execution/runner.py +171 -0
- pymelos/filters/__init__.py +27 -0
- pymelos/filters/chain.py +101 -0
- pymelos/filters/ignore.py +60 -0
- pymelos/filters/scope.py +90 -0
- pymelos/filters/since.py +98 -0
- pymelos/git/__init__.py +69 -0
- pymelos/git/changes.py +153 -0
- pymelos/git/commits.py +174 -0
- pymelos/git/repo.py +210 -0
- pymelos/git/tags.py +242 -0
- pymelos/py.typed +0 -0
- pymelos/types.py +16 -0
- pymelos/uv/__init__.py +44 -0
- pymelos/uv/client.py +167 -0
- pymelos/uv/publish.py +162 -0
- pymelos/uv/sync.py +168 -0
- pymelos/versioning/__init__.py +57 -0
- pymelos/versioning/changelog.py +189 -0
- pymelos/versioning/conventional.py +216 -0
- pymelos/versioning/semver.py +249 -0
- pymelos/versioning/updater.py +146 -0
- pymelos/workspace/__init__.py +33 -0
- pymelos/workspace/discovery.py +138 -0
- pymelos/workspace/graph.py +238 -0
- pymelos/workspace/package.py +191 -0
- pymelos/workspace/workspace.py +218 -0
- pymelos-0.1.3.dist-info/METADATA +106 -0
- pymelos-0.1.3.dist-info/RECORD +57 -0
- pymelos-0.1.3.dist-info/WHEEL +4 -0
- pymelos-0.1.3.dist-info/entry_points.txt +2 -0
- pymelos-0.1.3.dist-info/licenses/LICENSE +21 -0
pymelos/__init__.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""pymelos - Python monorepo manager.
|
|
2
|
+
|
|
3
|
+
A Melos-like monorepo management tool for Python, providing:
|
|
4
|
+
- Workspace discovery and package management
|
|
5
|
+
- Parallel command execution with dependency ordering
|
|
6
|
+
- Git-based change detection
|
|
7
|
+
- Semantic versioning and release management
|
|
8
|
+
- VS Code integration
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pymelos.config import PyMelosConfig, load_config
|
|
12
|
+
from pymelos.errors import (
|
|
13
|
+
BootstrapError,
|
|
14
|
+
ConfigurationError,
|
|
15
|
+
CyclicDependencyError,
|
|
16
|
+
ExecutionError,
|
|
17
|
+
GitError,
|
|
18
|
+
PackageNotFoundError,
|
|
19
|
+
PublishError,
|
|
20
|
+
PyMelosError,
|
|
21
|
+
ReleaseError,
|
|
22
|
+
ScriptNotFoundError,
|
|
23
|
+
ValidationError,
|
|
24
|
+
WorkspaceNotFoundError,
|
|
25
|
+
)
|
|
26
|
+
from pymelos.execution import (
|
|
27
|
+
BatchResult,
|
|
28
|
+
ExecutionResult,
|
|
29
|
+
ExecutionStatus,
|
|
30
|
+
ParallelExecutor,
|
|
31
|
+
)
|
|
32
|
+
from pymelos.workspace import DependencyGraph, Package, Workspace
|
|
33
|
+
|
|
34
|
+
__version__ = "0.1.3"
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
# Version
|
|
38
|
+
"__version__",
|
|
39
|
+
# Core
|
|
40
|
+
"Workspace",
|
|
41
|
+
"Package",
|
|
42
|
+
"DependencyGraph",
|
|
43
|
+
"PyMelosConfig",
|
|
44
|
+
"load_config",
|
|
45
|
+
# Execution
|
|
46
|
+
"ExecutionResult",
|
|
47
|
+
"ExecutionStatus",
|
|
48
|
+
"BatchResult",
|
|
49
|
+
"ParallelExecutor",
|
|
50
|
+
# Errors
|
|
51
|
+
"PyMelosError",
|
|
52
|
+
"ConfigurationError",
|
|
53
|
+
"WorkspaceNotFoundError",
|
|
54
|
+
"PackageNotFoundError",
|
|
55
|
+
"CyclicDependencyError",
|
|
56
|
+
"ScriptNotFoundError",
|
|
57
|
+
"ExecutionError",
|
|
58
|
+
"BootstrapError",
|
|
59
|
+
"GitError",
|
|
60
|
+
"ReleaseError",
|
|
61
|
+
"PublishError",
|
|
62
|
+
"ValidationError",
|
|
63
|
+
]
|
pymelos/__main__.py
ADDED
pymelos/cli/__init__.py
ADDED
pymelos/cli/__main__.py
ADDED
pymelos/cli/app.py
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
"""pymelos CLI application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated, Literal
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.markup import escape
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from pymelos.errors import PyMelosError
|
|
15
|
+
from pymelos.workspace import Workspace
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def version_callback(value: bool) -> None:
|
|
19
|
+
"""Print version and exit."""
|
|
20
|
+
if value:
|
|
21
|
+
from pymelos import __version__
|
|
22
|
+
|
|
23
|
+
print(f"pymelos {__version__}")
|
|
24
|
+
raise typer.Exit()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
app = typer.Typer(
|
|
28
|
+
name="pymelos",
|
|
29
|
+
help="Python monorepo manager powered",
|
|
30
|
+
no_args_is_help=True,
|
|
31
|
+
add_completion=False,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.callback()
|
|
36
|
+
def _app_callback(
|
|
37
|
+
version: Annotated[ # noqa: ARG001
|
|
38
|
+
bool,
|
|
39
|
+
typer.Option("--version", "-V", help="Show version and exit", callback=version_callback),
|
|
40
|
+
] = False,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Python monorepo manager ."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
console = Console()
|
|
47
|
+
error_console = Console(stderr=True)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_comma_list(value: str | None) -> list[str] | None:
|
|
51
|
+
"""Parse comma-separated string into list."""
|
|
52
|
+
return value.split(",") if value else None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_workspace(path: Path | None = None) -> Workspace:
|
|
56
|
+
"""Load workspace from current directory or specified path."""
|
|
57
|
+
try:
|
|
58
|
+
return Workspace.discover(path)
|
|
59
|
+
except PyMelosError as e:
|
|
60
|
+
error_console.print(f"[red]Error:[/red] {e.message}")
|
|
61
|
+
raise typer.Exit(1) from e
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.command()
|
|
65
|
+
def init(
|
|
66
|
+
path: Annotated[
|
|
67
|
+
Path | None,
|
|
68
|
+
typer.Argument(help="Directory to initialize"),
|
|
69
|
+
] = None,
|
|
70
|
+
name: Annotated[
|
|
71
|
+
str | None,
|
|
72
|
+
typer.Option("--name", "-n", help="Workspace name"),
|
|
73
|
+
] = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Initialize a new pymelos workspace."""
|
|
76
|
+
from pymelos.cli.commands.init import init_workspace
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
init_workspace(path or Path.cwd(), name)
|
|
80
|
+
console.print("[green]Workspace initialized![/green]")
|
|
81
|
+
console.print("Run [bold]pymelos bootstrap[/bold] to install dependencies.")
|
|
82
|
+
except PyMelosError as e:
|
|
83
|
+
error_console.print(f"[red]Error:[/red] {e.message}")
|
|
84
|
+
raise typer.Exit(1) from e
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command()
|
|
88
|
+
def bootstrap(
|
|
89
|
+
clean: Annotated[
|
|
90
|
+
bool,
|
|
91
|
+
typer.Option("--clean", help="Clean before bootstrap"),
|
|
92
|
+
] = False,
|
|
93
|
+
frozen: Annotated[
|
|
94
|
+
bool,
|
|
95
|
+
typer.Option("--frozen", help="Use frozen dependencies"),
|
|
96
|
+
] = False,
|
|
97
|
+
skip_hooks: Annotated[
|
|
98
|
+
bool,
|
|
99
|
+
typer.Option("--skip-hooks", help="Skip bootstrap hooks"),
|
|
100
|
+
] = False,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Install dependencies and link packages."""
|
|
103
|
+
from pymelos.commands import bootstrap as do_bootstrap
|
|
104
|
+
|
|
105
|
+
workspace = get_workspace()
|
|
106
|
+
|
|
107
|
+
async def run() -> None:
|
|
108
|
+
result = await do_bootstrap(
|
|
109
|
+
workspace,
|
|
110
|
+
clean_first=clean,
|
|
111
|
+
frozen=frozen,
|
|
112
|
+
skip_hooks=skip_hooks,
|
|
113
|
+
)
|
|
114
|
+
if result.success:
|
|
115
|
+
console.print(f"[green]Bootstrapped {result.packages_installed} packages[/green]")
|
|
116
|
+
else:
|
|
117
|
+
error_console.print(f"[red]Bootstrap failed:[/red] {result.uv_output}")
|
|
118
|
+
raise typer.Exit(1)
|
|
119
|
+
|
|
120
|
+
asyncio.run(run())
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@app.command(name="add")
|
|
124
|
+
def run_add_project(
|
|
125
|
+
name: Annotated[
|
|
126
|
+
str,
|
|
127
|
+
typer.Argument(help="Project name"),
|
|
128
|
+
],
|
|
129
|
+
project_type: Annotated[
|
|
130
|
+
Literal["lib", "app"],
|
|
131
|
+
typer.Option("--project-type", "-t", help="Project type"),
|
|
132
|
+
] = "lib",
|
|
133
|
+
folder: Annotated[
|
|
134
|
+
str | None,
|
|
135
|
+
typer.Option("--folder", "-f", help="Target folder"),
|
|
136
|
+
] = None,
|
|
137
|
+
editable: Annotated[
|
|
138
|
+
bool,
|
|
139
|
+
typer.Option("--editable", help="Install project as editable"),
|
|
140
|
+
] = True,
|
|
141
|
+
) -> None:
|
|
142
|
+
from pymelos.commands import add_project
|
|
143
|
+
|
|
144
|
+
workspace = get_workspace()
|
|
145
|
+
|
|
146
|
+
async def run() -> None:
|
|
147
|
+
result = await add_project(workspace, name, project_type, folder, editable)
|
|
148
|
+
if result.success:
|
|
149
|
+
console.print(f"[green]Added project {name}[/green]")
|
|
150
|
+
else:
|
|
151
|
+
error_console.print(f"[red]Failed to add project {name}:[/red] {result.message}")
|
|
152
|
+
raise typer.Exit(1)
|
|
153
|
+
|
|
154
|
+
asyncio.run(run())
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@app.command("run")
|
|
158
|
+
def run_cmd(
|
|
159
|
+
script: Annotated[str, typer.Argument(help="Script name to run")],
|
|
160
|
+
scope: Annotated[
|
|
161
|
+
str | None,
|
|
162
|
+
typer.Option("--scope", "-s", help="Package scope filter"),
|
|
163
|
+
] = None,
|
|
164
|
+
since: Annotated[
|
|
165
|
+
str | None,
|
|
166
|
+
typer.Option("--since", help="Only packages changed since git ref"),
|
|
167
|
+
] = None,
|
|
168
|
+
ignore: Annotated[
|
|
169
|
+
str | None,
|
|
170
|
+
typer.Option("--ignore", "-i", help="Patterns to ignore (comma-separated)"),
|
|
171
|
+
] = None,
|
|
172
|
+
concurrency: Annotated[
|
|
173
|
+
int,
|
|
174
|
+
typer.Option("--concurrency", "-c", help="Parallel jobs"),
|
|
175
|
+
] = 4,
|
|
176
|
+
fail_fast: Annotated[
|
|
177
|
+
bool,
|
|
178
|
+
typer.Option("--fail-fast", help="Stop on first failure"),
|
|
179
|
+
] = False,
|
|
180
|
+
no_topological: Annotated[
|
|
181
|
+
bool,
|
|
182
|
+
typer.Option("--no-topological", help="Ignore dependency order"),
|
|
183
|
+
] = False,
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Run a defined script across packages."""
|
|
186
|
+
from pymelos.commands import run_script
|
|
187
|
+
|
|
188
|
+
workspace = get_workspace()
|
|
189
|
+
ignore_list = parse_comma_list(ignore)
|
|
190
|
+
|
|
191
|
+
async def run() -> None:
|
|
192
|
+
result = await run_script(
|
|
193
|
+
workspace,
|
|
194
|
+
script,
|
|
195
|
+
scope=scope,
|
|
196
|
+
since=since,
|
|
197
|
+
ignore=ignore_list,
|
|
198
|
+
concurrency=concurrency,
|
|
199
|
+
fail_fast=fail_fast,
|
|
200
|
+
topological=not no_topological,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
for r in result:
|
|
204
|
+
package_name = escape(f"[{r.package_name}]")
|
|
205
|
+
if r.success:
|
|
206
|
+
console.print(f"[green]✓[/green] {package_name} ({r.duration_ms}ms)")
|
|
207
|
+
if r.stdout:
|
|
208
|
+
console.print(r.stdout)
|
|
209
|
+
else:
|
|
210
|
+
console.print(f"[red]✗[/red] {package_name} (exit {r.exit_code})")
|
|
211
|
+
if r.stderr:
|
|
212
|
+
console.print(r.stderr)
|
|
213
|
+
|
|
214
|
+
if result.all_success:
|
|
215
|
+
console.print(f"\n[green]All {len(result)} packages passed[/green]")
|
|
216
|
+
else:
|
|
217
|
+
console.print(
|
|
218
|
+
f"\n[red]{result.failure_count} failed, {result.success_count} passed[/red]"
|
|
219
|
+
)
|
|
220
|
+
raise typer.Exit(1)
|
|
221
|
+
|
|
222
|
+
asyncio.run(run())
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@app.command("exec")
|
|
226
|
+
def exec_cmd(
|
|
227
|
+
command: Annotated[str, typer.Argument(help="Command to execute")],
|
|
228
|
+
scope: Annotated[
|
|
229
|
+
str | None,
|
|
230
|
+
typer.Option("--scope", "-s", help="Package scope filter"),
|
|
231
|
+
] = None,
|
|
232
|
+
since: Annotated[
|
|
233
|
+
str | None,
|
|
234
|
+
typer.Option("--since", help="Only packages changed since git ref"),
|
|
235
|
+
] = None,
|
|
236
|
+
ignore: Annotated[
|
|
237
|
+
str | None,
|
|
238
|
+
typer.Option("--ignore", "-i", help="Patterns to ignore"),
|
|
239
|
+
] = None,
|
|
240
|
+
concurrency: Annotated[
|
|
241
|
+
int,
|
|
242
|
+
typer.Option("--concurrency", "-c", help="Parallel jobs"),
|
|
243
|
+
] = 4,
|
|
244
|
+
fail_fast: Annotated[
|
|
245
|
+
bool,
|
|
246
|
+
typer.Option("--fail-fast", help="Stop on first failure"),
|
|
247
|
+
] = False,
|
|
248
|
+
) -> None:
|
|
249
|
+
"""Execute an arbitrary command across packages."""
|
|
250
|
+
from pymelos.commands import exec_command
|
|
251
|
+
|
|
252
|
+
workspace = get_workspace()
|
|
253
|
+
ignore_list = parse_comma_list(ignore)
|
|
254
|
+
|
|
255
|
+
async def run() -> None:
|
|
256
|
+
result = await exec_command(
|
|
257
|
+
workspace,
|
|
258
|
+
command,
|
|
259
|
+
scope=scope,
|
|
260
|
+
since=since,
|
|
261
|
+
ignore=ignore_list,
|
|
262
|
+
concurrency=concurrency,
|
|
263
|
+
fail_fast=fail_fast,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
for r in result:
|
|
267
|
+
console.print(f"\n[bold][{r.package_name}][/bold]")
|
|
268
|
+
if r.stdout:
|
|
269
|
+
console.print(r.stdout)
|
|
270
|
+
if r.stderr:
|
|
271
|
+
error_console.print(r.stderr)
|
|
272
|
+
|
|
273
|
+
if not result.all_success:
|
|
274
|
+
raise typer.Exit(1)
|
|
275
|
+
|
|
276
|
+
asyncio.run(run())
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@app.command("list")
|
|
280
|
+
def list_cmd(
|
|
281
|
+
scope: Annotated[
|
|
282
|
+
str | None,
|
|
283
|
+
typer.Option("--scope", "-s", help="Package scope filter"),
|
|
284
|
+
] = None,
|
|
285
|
+
since: Annotated[
|
|
286
|
+
str | None,
|
|
287
|
+
typer.Option("--since", help="Only packages changed since git ref"),
|
|
288
|
+
] = None,
|
|
289
|
+
json_output: Annotated[
|
|
290
|
+
bool,
|
|
291
|
+
typer.Option("--json", help="Output as JSON"),
|
|
292
|
+
] = False,
|
|
293
|
+
graph: Annotated[
|
|
294
|
+
bool,
|
|
295
|
+
typer.Option("--graph", help="Show dependency graph"),
|
|
296
|
+
] = False,
|
|
297
|
+
) -> None:
|
|
298
|
+
"""List workspace packages."""
|
|
299
|
+
from pymelos.commands import ListFormat, list_packages
|
|
300
|
+
|
|
301
|
+
workspace = get_workspace()
|
|
302
|
+
|
|
303
|
+
fmt = ListFormat.TABLE
|
|
304
|
+
if json_output:
|
|
305
|
+
fmt = ListFormat.JSON
|
|
306
|
+
elif graph:
|
|
307
|
+
fmt = ListFormat.GRAPH
|
|
308
|
+
|
|
309
|
+
result = list_packages(workspace, scope=scope, since=since, format=fmt)
|
|
310
|
+
|
|
311
|
+
if json_output:
|
|
312
|
+
import json
|
|
313
|
+
|
|
314
|
+
data = [
|
|
315
|
+
{
|
|
316
|
+
"name": p.name,
|
|
317
|
+
"version": p.version,
|
|
318
|
+
"path": p.path,
|
|
319
|
+
"description": p.description,
|
|
320
|
+
"dependencies": p.dependencies,
|
|
321
|
+
}
|
|
322
|
+
for p in result.packages
|
|
323
|
+
]
|
|
324
|
+
console.print(json.dumps(data, indent=2))
|
|
325
|
+
elif graph:
|
|
326
|
+
# Simple tree output
|
|
327
|
+
for pkg in result.packages:
|
|
328
|
+
if not pkg.dependencies:
|
|
329
|
+
console.print(f"[bold]{pkg.name}[/bold] v{pkg.version}")
|
|
330
|
+
else:
|
|
331
|
+
deps_str = ", ".join(pkg.dependencies)
|
|
332
|
+
console.print(f"[bold]{pkg.name}[/bold] v{pkg.version} -> {deps_str}")
|
|
333
|
+
else:
|
|
334
|
+
table = Table(title="Packages")
|
|
335
|
+
table.add_column("Name", style="bold")
|
|
336
|
+
table.add_column("Version")
|
|
337
|
+
table.add_column("Path")
|
|
338
|
+
table.add_column("Dependencies")
|
|
339
|
+
|
|
340
|
+
for pkg in result.packages:
|
|
341
|
+
deps = ", ".join(pkg.dependencies) if pkg.dependencies else "-"
|
|
342
|
+
table.add_row(pkg.name, pkg.version, pkg.path, deps)
|
|
343
|
+
|
|
344
|
+
console.print(table)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@app.command()
|
|
348
|
+
def clean(
|
|
349
|
+
scope: Annotated[
|
|
350
|
+
str | None,
|
|
351
|
+
typer.Option("--scope", "-s", help="Package scope filter"),
|
|
352
|
+
] = None,
|
|
353
|
+
dry_run: Annotated[
|
|
354
|
+
bool,
|
|
355
|
+
typer.Option("--dry-run", help="Show what would be cleaned"),
|
|
356
|
+
] = False,
|
|
357
|
+
) -> None:
|
|
358
|
+
"""Clean build artifacts."""
|
|
359
|
+
from pymelos.commands import clean as do_clean
|
|
360
|
+
|
|
361
|
+
workspace = get_workspace()
|
|
362
|
+
|
|
363
|
+
async def run() -> None:
|
|
364
|
+
result = await do_clean(workspace, scope=scope, dry_run=dry_run)
|
|
365
|
+
|
|
366
|
+
if dry_run:
|
|
367
|
+
console.print("[yellow]Dry run - no files removed[/yellow]")
|
|
368
|
+
|
|
369
|
+
console.print(
|
|
370
|
+
f"{'Would clean' if dry_run else 'Cleaned'} "
|
|
371
|
+
f"{result.files_removed} files, {result.dirs_removed} directories "
|
|
372
|
+
f"({result.bytes_freed / 1024:.1f} KB)"
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
asyncio.run(run())
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@app.command()
|
|
379
|
+
def changed(
|
|
380
|
+
since: Annotated[str, typer.Argument(help="Git reference (branch, tag, commit)")],
|
|
381
|
+
no_dependents: Annotated[
|
|
382
|
+
bool,
|
|
383
|
+
typer.Option("--no-dependents", help="Exclude dependent packages"),
|
|
384
|
+
] = False,
|
|
385
|
+
json_output: Annotated[
|
|
386
|
+
bool,
|
|
387
|
+
typer.Option("--json", help="Output as JSON"),
|
|
388
|
+
] = False,
|
|
389
|
+
) -> None:
|
|
390
|
+
"""List packages changed since a git reference."""
|
|
391
|
+
from pymelos.commands import get_changed_packages
|
|
392
|
+
|
|
393
|
+
workspace = get_workspace()
|
|
394
|
+
result = get_changed_packages(
|
|
395
|
+
workspace,
|
|
396
|
+
since,
|
|
397
|
+
include_dependents=not no_dependents,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
if json_output:
|
|
401
|
+
import json
|
|
402
|
+
|
|
403
|
+
data = [
|
|
404
|
+
{
|
|
405
|
+
"name": p.name,
|
|
406
|
+
"path": p.path,
|
|
407
|
+
"files_changed": p.files_changed,
|
|
408
|
+
"is_dependent": p.is_dependent,
|
|
409
|
+
}
|
|
410
|
+
for p in result.changed
|
|
411
|
+
]
|
|
412
|
+
console.print(json.dumps(data, indent=2))
|
|
413
|
+
else:
|
|
414
|
+
console.print(f"Packages changed since [bold]{since}[/bold]:")
|
|
415
|
+
for pkg in result.changed:
|
|
416
|
+
suffix = " [dim](dependent)[/dim]" if pkg.is_dependent else ""
|
|
417
|
+
console.print(f" - {pkg.name} ({pkg.files_changed} files){suffix}")
|
|
418
|
+
|
|
419
|
+
if not result.changed:
|
|
420
|
+
console.print(" [dim]No packages changed[/dim]")
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@app.command()
|
|
424
|
+
def release(
|
|
425
|
+
scope: Annotated[
|
|
426
|
+
str | None,
|
|
427
|
+
typer.Option("--scope", "-s", help="Package scope filter"),
|
|
428
|
+
] = None,
|
|
429
|
+
bump: Annotated[
|
|
430
|
+
str | None,
|
|
431
|
+
typer.Option("--bump", "-b", help="Force bump type (major, minor, patch)"),
|
|
432
|
+
] = None,
|
|
433
|
+
prerelease: Annotated[
|
|
434
|
+
str | None,
|
|
435
|
+
typer.Option("--prerelease", help="Prerelease tag (alpha, beta, rc)"),
|
|
436
|
+
] = None,
|
|
437
|
+
dry_run: Annotated[
|
|
438
|
+
bool,
|
|
439
|
+
typer.Option("--dry-run", help="Show what would be released"),
|
|
440
|
+
] = False,
|
|
441
|
+
publish: Annotated[
|
|
442
|
+
bool,
|
|
443
|
+
typer.Option("--publish", help="Publish to PyPI"),
|
|
444
|
+
] = False,
|
|
445
|
+
no_git_tag: Annotated[
|
|
446
|
+
bool,
|
|
447
|
+
typer.Option("--no-git-tag", help="Skip creating git tags"),
|
|
448
|
+
] = False,
|
|
449
|
+
no_changelog: Annotated[
|
|
450
|
+
bool,
|
|
451
|
+
typer.Option("--no-changelog", help="Skip changelog generation"),
|
|
452
|
+
] = False,
|
|
453
|
+
no_commit: Annotated[
|
|
454
|
+
bool,
|
|
455
|
+
typer.Option("--no-commit", help="Skip git commit"),
|
|
456
|
+
] = False,
|
|
457
|
+
) -> None:
|
|
458
|
+
"""Version and publish packages."""
|
|
459
|
+
from pymelos.commands import release as do_release
|
|
460
|
+
from pymelos.versioning import BumpType
|
|
461
|
+
|
|
462
|
+
workspace = get_workspace()
|
|
463
|
+
|
|
464
|
+
bump_type = None
|
|
465
|
+
if bump:
|
|
466
|
+
try:
|
|
467
|
+
bump_type = BumpType[bump.upper()]
|
|
468
|
+
except KeyError:
|
|
469
|
+
error_console.print(f"[red]Invalid bump type:[/red] {bump}")
|
|
470
|
+
raise typer.Exit(1) from None
|
|
471
|
+
|
|
472
|
+
async def run() -> None:
|
|
473
|
+
result = await do_release(
|
|
474
|
+
workspace,
|
|
475
|
+
scope=scope,
|
|
476
|
+
bump=bump_type,
|
|
477
|
+
prerelease=prerelease,
|
|
478
|
+
dry_run=dry_run,
|
|
479
|
+
publish=publish,
|
|
480
|
+
no_git_tag=no_git_tag,
|
|
481
|
+
no_changelog=no_changelog,
|
|
482
|
+
no_commit=no_commit,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if not result.releases:
|
|
486
|
+
console.print("[yellow]No packages to release[/yellow]")
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
if dry_run:
|
|
490
|
+
console.print("[yellow]Dry run - no changes made[/yellow]\n")
|
|
491
|
+
console.print("Pending releases:")
|
|
492
|
+
|
|
493
|
+
table = Table()
|
|
494
|
+
table.add_column("Package")
|
|
495
|
+
table.add_column("Current")
|
|
496
|
+
table.add_column("Next")
|
|
497
|
+
table.add_column("Bump")
|
|
498
|
+
|
|
499
|
+
for r in result.releases:
|
|
500
|
+
table.add_row(
|
|
501
|
+
r.name,
|
|
502
|
+
r.old_version,
|
|
503
|
+
r.new_version,
|
|
504
|
+
r.bump_type.name.lower(),
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
console.print(table)
|
|
508
|
+
|
|
509
|
+
if not dry_run:
|
|
510
|
+
if result.success:
|
|
511
|
+
console.print(f"\n[green]Released {len(result.releases)} packages[/green]")
|
|
512
|
+
if result.commit_sha:
|
|
513
|
+
console.print(f"Commit: {result.commit_sha[:8]}")
|
|
514
|
+
else:
|
|
515
|
+
error_console.print(f"\n[red]Release failed:[/red] {result.error}")
|
|
516
|
+
raise typer.Exit(1)
|
|
517
|
+
|
|
518
|
+
asyncio.run(run())
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def main() -> None:
|
|
522
|
+
"""Main entry point."""
|
|
523
|
+
app()
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
if __name__ == "__main__":
|
|
527
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command implementations."""
|