ps-plugin-module-delivery 0.2.8__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.
- ps/plugin/module/delivery/__init__.py +5 -0
- ps/plugin/module/delivery/_delivery_module.py +179 -0
- ps/plugin/module/delivery/_delivery_settings.py +9 -0
- ps/plugin/module/delivery/_parallelization.py +105 -0
- ps/plugin/module/delivery/commands/__init__.py +5 -0
- ps/plugin/module/delivery/commands/_delivery_command.py +65 -0
- ps/plugin/module/delivery/output/__init__.py +23 -0
- ps/plugin/module/delivery/output/_formatted.py +112 -0
- ps/plugin/module/delivery/output/_json.py +76 -0
- ps/plugin/module/delivery/output/_models.py +53 -0
- ps/plugin/module/delivery/output/_protocol.py +22 -0
- ps/plugin/module/delivery/stages/__init__.py +26 -0
- ps/plugin/module/delivery/stages/_build.py +53 -0
- ps/plugin/module/delivery/stages/_logging.py +108 -0
- ps/plugin/module/delivery/stages/_metadata.py +243 -0
- ps/plugin/module/delivery/stages/_patch.py +102 -0
- ps/plugin/module/delivery/stages/_publish.py +76 -0
- ps/plugin/module/delivery/token_resolvers/__init__.py +14 -0
- ps/plugin/module/delivery/token_resolvers/_date_resolver.py +135 -0
- ps/plugin/module/delivery/token_resolvers/_env_resolver.py +12 -0
- ps/plugin/module/delivery/token_resolvers/_git_resolver.py +99 -0
- ps/plugin/module/delivery/token_resolvers/_rand_resolver.py +43 -0
- ps/plugin/module/delivery/token_resolvers/_version_resolver.py +21 -0
- ps_plugin_module_delivery-0.2.8.dist-info/METADATA +11 -0
- ps_plugin_module_delivery-0.2.8.dist-info/RECORD +27 -0
- ps_plugin_module_delivery-0.2.8.dist-info/WHEEL +4 -0
- ps_plugin_module_delivery-0.2.8.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import ClassVar, Optional
|
|
4
|
+
|
|
5
|
+
from cleo.events.console_command_event import ConsoleCommandEvent
|
|
6
|
+
from cleo.events.console_terminate_event import ConsoleTerminateEvent
|
|
7
|
+
from cleo.io.inputs.argument import Argument
|
|
8
|
+
from cleo.io.inputs.input import Input
|
|
9
|
+
from cleo.io.inputs.option import Option
|
|
10
|
+
from cleo.io.io import IO
|
|
11
|
+
from poetry.console.application import Application
|
|
12
|
+
from poetry.console.commands.build import BuildCommand
|
|
13
|
+
from poetry.console.commands.publish import PublishCommand
|
|
14
|
+
|
|
15
|
+
from ps.di import DI
|
|
16
|
+
from ps.plugin.sdk.events import ensure_argument, ensure_option
|
|
17
|
+
from ps.plugin.sdk.project import Environment, filter_projects
|
|
18
|
+
from ps.version import Version
|
|
19
|
+
from ps.token_expressions import TokenResolverEntry
|
|
20
|
+
|
|
21
|
+
from .token_resolvers import DateResolver, EnvResolver, RandResolver, VersionResolver, collect_git_info
|
|
22
|
+
from .stages import (
|
|
23
|
+
DeliverableType,
|
|
24
|
+
ResolvedProjectMetadata,
|
|
25
|
+
build_projects,
|
|
26
|
+
log_resolution,
|
|
27
|
+
patch_projects,
|
|
28
|
+
publish_projects,
|
|
29
|
+
resolve_environment_metadata,
|
|
30
|
+
)
|
|
31
|
+
from .commands import DeliveryCommand
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
BUILD_VERSION_OPTION = "build-version"
|
|
35
|
+
BUILD_VERSION_OPTION_SHORT = "b"
|
|
36
|
+
INPUTS_ARGUMENT = "inputs"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_inputs(input: Input) -> list[str]:
|
|
40
|
+
return input.arguments.get(INPUTS_ARGUMENT, [])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _get_version_option(input: Input) -> Optional[str]:
|
|
44
|
+
return input.options.get(BUILD_VERSION_OPTION, None)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DeliveryModule:
|
|
48
|
+
name: ClassVar[str] = "ps-delivery"
|
|
49
|
+
|
|
50
|
+
def __init__(self) -> None:
|
|
51
|
+
self._exit_code: Optional[int] = None
|
|
52
|
+
|
|
53
|
+
def poetry_activate(
|
|
54
|
+
self,
|
|
55
|
+
application: Application,
|
|
56
|
+
environment: Environment,
|
|
57
|
+
di: DI,
|
|
58
|
+
io: IO
|
|
59
|
+
) -> bool:
|
|
60
|
+
# Extend the BuildCommand with an optional "inputs" argument
|
|
61
|
+
ensure_argument(BuildCommand, Argument(
|
|
62
|
+
name=INPUTS_ARGUMENT,
|
|
63
|
+
description="Optional inputs pointers to build. It could be project names or paths. If not provided, all discovered projects will be built.",
|
|
64
|
+
is_list=True,
|
|
65
|
+
required=False)
|
|
66
|
+
)
|
|
67
|
+
ensure_option(BuildCommand, Option(
|
|
68
|
+
name=BUILD_VERSION_OPTION,
|
|
69
|
+
description="Specify the version to build the project with.",
|
|
70
|
+
shortcut=BUILD_VERSION_OPTION_SHORT,
|
|
71
|
+
flag=False)
|
|
72
|
+
)
|
|
73
|
+
ensure_argument(PublishCommand, Argument(
|
|
74
|
+
name=INPUTS_ARGUMENT,
|
|
75
|
+
description="Optional inputs pointers to publish. It could be project names or paths. If not provided, all discovered projects will be published.",
|
|
76
|
+
is_list=True,
|
|
77
|
+
required=False)
|
|
78
|
+
)
|
|
79
|
+
ensure_option(PublishCommand, Option(
|
|
80
|
+
name=BUILD_VERSION_OPTION,
|
|
81
|
+
description="Specify the version to publish the project with.",
|
|
82
|
+
shortcut=BUILD_VERSION_OPTION_SHORT,
|
|
83
|
+
flag=False)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
application.add(di.spawn(DeliveryCommand))
|
|
87
|
+
|
|
88
|
+
di.register(TokenResolverEntry).factory(lambda: ("env", EnvResolver()))
|
|
89
|
+
di.register(TokenResolverEntry).factory(lambda: ("rand", RandResolver()))
|
|
90
|
+
di.register(TokenResolverEntry).factory(lambda: ("v", VersionResolver()))
|
|
91
|
+
di.register(TokenResolverEntry).factory(lambda: ("date", DateResolver(datetime.now())))
|
|
92
|
+
di.register(TokenResolverEntry).factory(lambda env: ("git", collect_git_info(env)), env=environment.host_project.path)
|
|
93
|
+
di.register(TokenResolverEntry).factory(lambda ver: ("in", ver), ver=Version.parse(_get_version_option(io.input)))
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
def poetry_command(self, event: ConsoleCommandEvent, di: DI) -> None:
|
|
97
|
+
if not isinstance(event.command, (BuildCommand, PublishCommand)):
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
# Disable the original command execution
|
|
101
|
+
event.disable_command()
|
|
102
|
+
|
|
103
|
+
environment = di.resolve(Environment)
|
|
104
|
+
assert environment is not None
|
|
105
|
+
|
|
106
|
+
# Filter projects based on inputs
|
|
107
|
+
inputs = _get_inputs(event.io.input)
|
|
108
|
+
# In case no inputs are provided, and the entry project is different from the host project, add the entry project path as input
|
|
109
|
+
if not inputs and environment.host_project != environment.entry_project:
|
|
110
|
+
inputs.append(str(environment.entry_project.path))
|
|
111
|
+
environment_metadata = di.satisfy(resolve_environment_metadata)()
|
|
112
|
+
|
|
113
|
+
log_resolution(event.io, environment_metadata)
|
|
114
|
+
|
|
115
|
+
filtered_projects = filter_projects(inputs, environment.projects)
|
|
116
|
+
excluded = {id(p) for p in filtered_projects if (environment_metadata.projects.get(p.path) or ResolvedProjectMetadata()).deliver != DeliverableType.ENABLED}
|
|
117
|
+
event.io.write_line("<fg=magenta>Delivery scope:</>")
|
|
118
|
+
for p in filtered_projects:
|
|
119
|
+
name = p.name.value or p.path.name
|
|
120
|
+
if id(p) in excluded:
|
|
121
|
+
event.io.write_line(f" - <fg=dark_gray>{name} (not marked for delivery)</>")
|
|
122
|
+
else:
|
|
123
|
+
event.io.write_line(f" - <fg=blue>{name}</>")
|
|
124
|
+
filtered_projects = [p for p in filtered_projects if id(p) not in excluded]
|
|
125
|
+
if not filtered_projects:
|
|
126
|
+
event.io.write_line("<comment>No projects found to process.</comment>")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
environment.backup_projects(filtered_projects)
|
|
131
|
+
|
|
132
|
+
# Patch all projects
|
|
133
|
+
patch_exit_code = patch_projects(event.io, filtered_projects, environment_metadata)
|
|
134
|
+
if patch_exit_code != 0:
|
|
135
|
+
self._exit_code = patch_exit_code
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# Execute build or publish command
|
|
139
|
+
is_publish = isinstance(event.command, PublishCommand)
|
|
140
|
+
if is_publish:
|
|
141
|
+
opts = event.io.input.options
|
|
142
|
+
cert = opts.get("cert")
|
|
143
|
+
client_cert = opts.get("client-cert")
|
|
144
|
+
dist_dir = opts.get("dist-dir")
|
|
145
|
+
self._exit_code = publish_projects(
|
|
146
|
+
event.io,
|
|
147
|
+
filtered_projects,
|
|
148
|
+
environment,
|
|
149
|
+
environment_metadata,
|
|
150
|
+
repository=opts.get("repository"),
|
|
151
|
+
username=opts.get("username"),
|
|
152
|
+
password=opts.get("password"),
|
|
153
|
+
cert=Path(cert) if cert else None,
|
|
154
|
+
client_cert=Path(client_cert) if client_cert else None,
|
|
155
|
+
dist_dir=Path(dist_dir) if dist_dir else None,
|
|
156
|
+
dry_run=bool(opts.get("dry-run")),
|
|
157
|
+
skip_existing=bool(opts.get("skip-existing")),
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
opts = event.io.input.options
|
|
161
|
+
self._exit_code = build_projects(
|
|
162
|
+
event.io,
|
|
163
|
+
filtered_projects,
|
|
164
|
+
formats=BuildCommand._prepare_formats(opts.get("format")),
|
|
165
|
+
clean=bool(opts.get("clean")),
|
|
166
|
+
output=opts.get("output") or "dist",
|
|
167
|
+
config_settings=BuildCommand._prepare_config_settings(
|
|
168
|
+
local_version=opts.get("local-version"),
|
|
169
|
+
config_settings=opts.get("config-settings"),
|
|
170
|
+
io=event.io,
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
finally:
|
|
174
|
+
environment.restore_projects(environment.projects)
|
|
175
|
+
|
|
176
|
+
def poetry_terminate(self, event: ConsoleTerminateEvent) -> None:
|
|
177
|
+
if self._exit_code is None:
|
|
178
|
+
return
|
|
179
|
+
event.set_exit_code(self._exit_code)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
from ps.version import VersionConstraint
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DeliverySettings(BaseModel):
|
|
7
|
+
version_patterns: Optional[list[str]] = Field(default_factory=list, alias="version-patterns")
|
|
8
|
+
version_pinning: Optional[VersionConstraint] = Field(default=VersionConstraint.COMPATIBLE, alias="version-pinning")
|
|
9
|
+
deliver: Optional[bool] = Field(default=None, alias="deliver")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
+
from typing import Callable, Optional, TypeVar
|
|
5
|
+
|
|
6
|
+
from cleo.formatters.formatter import Formatter
|
|
7
|
+
from cleo.io.buffered_io import BufferedIO
|
|
8
|
+
from cleo.io.io import IO
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
_thread_io: threading.local = threading.local()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ThreadLocalIOHandler(logging.Handler):
|
|
16
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
17
|
+
bio: Optional[BufferedIO] = getattr(_thread_io, "io", None)
|
|
18
|
+
if bio is None:
|
|
19
|
+
return
|
|
20
|
+
try:
|
|
21
|
+
msg = self.format(record)
|
|
22
|
+
if record.levelname.lower() in ("warning", "error", "exception", "critical"):
|
|
23
|
+
bio.write_error_line(msg)
|
|
24
|
+
else:
|
|
25
|
+
bio.write_line(msg)
|
|
26
|
+
except Exception:
|
|
27
|
+
self.handleError(record)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _run_buffered(io: IO, item: T, fn: Callable[[BufferedIO, T], int]) -> tuple[int, str, str]:
|
|
31
|
+
buffered_io = BufferedIO(decorated=io.output.is_decorated())
|
|
32
|
+
buffered_io.set_verbosity(io.output.verbosity)
|
|
33
|
+
_thread_io.io = buffered_io
|
|
34
|
+
try:
|
|
35
|
+
exit_code = fn(buffered_io, item)
|
|
36
|
+
except Exception as e:
|
|
37
|
+
buffered_io.write_error_line(f"\n<error>{Formatter.escape(str(e))}</error>")
|
|
38
|
+
exit_code = 1
|
|
39
|
+
finally:
|
|
40
|
+
_thread_io.io = None
|
|
41
|
+
return exit_code, buffered_io.fetch_output(), buffered_io.fetch_error()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _flush(io: IO, out: str, err: str) -> None:
|
|
45
|
+
if out:
|
|
46
|
+
io.write(out)
|
|
47
|
+
if err:
|
|
48
|
+
io.write_error(err)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _normalize(text: str) -> str:
|
|
52
|
+
text = text.replace("\\n", "\n").replace("\r\n", "\n").replace("\r", "\n").strip()
|
|
53
|
+
lines = [line for line in text.splitlines() if line.strip()]
|
|
54
|
+
if not lines:
|
|
55
|
+
return ""
|
|
56
|
+
return "\n".join(lines) + "\n"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def run_parallel(io: IO, items: list[T], fn: Callable[[BufferedIO, T], int]) -> int:
|
|
60
|
+
lock = threading.Lock()
|
|
61
|
+
exit_code = 0
|
|
62
|
+
|
|
63
|
+
def _run_and_flush(item: T) -> int:
|
|
64
|
+
result, out, err = _run_buffered(io, item, fn)
|
|
65
|
+
with lock:
|
|
66
|
+
_flush(io, out, err)
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
with ThreadPoolExecutor() as executor:
|
|
70
|
+
results = list(executor.map(_run_and_flush, items))
|
|
71
|
+
|
|
72
|
+
for result in results:
|
|
73
|
+
if result != 0:
|
|
74
|
+
exit_code = result
|
|
75
|
+
|
|
76
|
+
return exit_code
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def run_topological(
|
|
80
|
+
io: IO,
|
|
81
|
+
items: list[T],
|
|
82
|
+
fn: Callable[[BufferedIO, T], int],
|
|
83
|
+
get_deps: Callable[[T], list[T]],
|
|
84
|
+
) -> int:
|
|
85
|
+
lock = threading.Lock()
|
|
86
|
+
done_events: dict[int, threading.Event] = {id(item): threading.Event() for item in items}
|
|
87
|
+
|
|
88
|
+
def _run_and_flush(item: T) -> int:
|
|
89
|
+
for dep in get_deps(item):
|
|
90
|
+
done_events[id(dep)].wait()
|
|
91
|
+
result, out, err = _run_buffered(io, item, fn)
|
|
92
|
+
with lock:
|
|
93
|
+
_flush(io, _normalize(out), _normalize(err))
|
|
94
|
+
done_events[id(item)].set()
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
with ThreadPoolExecutor() as executor:
|
|
98
|
+
futures = [executor.submit(_run_and_flush, item) for item in items]
|
|
99
|
+
|
|
100
|
+
exit_code = 0
|
|
101
|
+
for future in futures:
|
|
102
|
+
r = future.result()
|
|
103
|
+
if r != 0:
|
|
104
|
+
exit_code = r
|
|
105
|
+
return exit_code
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from cleo.commands.command import Command
|
|
2
|
+
from cleo.io.inputs.option import Option
|
|
3
|
+
|
|
4
|
+
from ps.di import DI
|
|
5
|
+
from ..output import DeliveryRenderer, FormattedDeliveryRenderer, JsonDeliveryRenderer
|
|
6
|
+
from ps.plugin.sdk.project import Environment
|
|
7
|
+
|
|
8
|
+
from ..stages import (
|
|
9
|
+
DeliverableType,
|
|
10
|
+
ResolvedProjectMetadata,
|
|
11
|
+
build_dependency_tree,
|
|
12
|
+
build_publish_waves,
|
|
13
|
+
resolve_environment_metadata,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DeliveryCommand(Command):
|
|
18
|
+
name = "delivery"
|
|
19
|
+
description = "Display the workspace delivery plan."
|
|
20
|
+
arguments = []
|
|
21
|
+
options = [
|
|
22
|
+
Option("--json", flag=True, description="Output as JSON"),
|
|
23
|
+
Option("--publish-order", flag=True, description="Show publish order"),
|
|
24
|
+
Option("--dependency-tree", flag=True, description="Show dependency tree"),
|
|
25
|
+
Option("--projects", flag=True, description="Show projects"),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
def __init__(self, di: DI) -> None:
|
|
29
|
+
super().__init__()
|
|
30
|
+
self._di = di
|
|
31
|
+
|
|
32
|
+
def handle(self) -> int:
|
|
33
|
+
io = self.io
|
|
34
|
+
environment = self._di.resolve(Environment)
|
|
35
|
+
assert environment is not None
|
|
36
|
+
|
|
37
|
+
environment_metadata = self._di.satisfy(resolve_environment_metadata)()
|
|
38
|
+
all_projects = list(environment.projects)
|
|
39
|
+
filtered = [
|
|
40
|
+
p for p in all_projects
|
|
41
|
+
if (environment_metadata.projects.get(p.path) or ResolvedProjectMetadata()).deliver == DeliverableType.ENABLED
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
renderer: DeliveryRenderer = JsonDeliveryRenderer(io) if self.option("json") else FormattedDeliveryRenderer(io)
|
|
45
|
+
show_publish_order = self.option("publish-order")
|
|
46
|
+
show_dependency_tree = self.option("dependency-tree")
|
|
47
|
+
show_projects = self.option("projects")
|
|
48
|
+
has_filter = show_publish_order or show_dependency_tree or show_projects
|
|
49
|
+
|
|
50
|
+
if not has_filter or show_projects:
|
|
51
|
+
renderer.render_resolution("Projects", environment_metadata.resolutions)
|
|
52
|
+
|
|
53
|
+
if not has_filter or show_dependency_tree:
|
|
54
|
+
nodes = build_dependency_tree(all_projects, environment, environment_metadata)
|
|
55
|
+
renderer.render_dependency_tree("Dependency tree", nodes)
|
|
56
|
+
|
|
57
|
+
if not has_filter or show_publish_order:
|
|
58
|
+
if not filtered:
|
|
59
|
+
renderer.render_message("<comment>No deliverable projects.</comment>")
|
|
60
|
+
else:
|
|
61
|
+
waves = build_publish_waves(filtered, environment, environment_metadata)
|
|
62
|
+
renderer.render_publish_waves("Publish order", waves)
|
|
63
|
+
|
|
64
|
+
renderer.flush()
|
|
65
|
+
return 0
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from ._formatted import FormattedDeliveryRenderer
|
|
2
|
+
from ._json import JsonDeliveryRenderer
|
|
3
|
+
from ._models import (
|
|
4
|
+
DependencyNode,
|
|
5
|
+
DependencyResolution,
|
|
6
|
+
ProjectResolution,
|
|
7
|
+
ProjectSummary,
|
|
8
|
+
PublishWave,
|
|
9
|
+
VersionPatternResult,
|
|
10
|
+
)
|
|
11
|
+
from ._protocol import DeliveryRenderer
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"DeliveryRenderer",
|
|
15
|
+
"DependencyNode",
|
|
16
|
+
"DependencyResolution",
|
|
17
|
+
"FormattedDeliveryRenderer",
|
|
18
|
+
"JsonDeliveryRenderer",
|
|
19
|
+
"ProjectResolution",
|
|
20
|
+
"ProjectSummary",
|
|
21
|
+
"PublishWave",
|
|
22
|
+
"VersionPatternResult",
|
|
23
|
+
]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from cleo.io.io import IO
|
|
2
|
+
|
|
3
|
+
from ._models import DependencyNode, DependencyResolution, ProjectResolution, PublishWave
|
|
4
|
+
from ._protocol import DeliveryRenderer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FormattedDeliveryRenderer(DeliveryRenderer):
|
|
8
|
+
def __init__(self, io: IO) -> None:
|
|
9
|
+
self._io = io
|
|
10
|
+
|
|
11
|
+
def render_resolution(self, title: str, resolutions: list[ProjectResolution]) -> None:
|
|
12
|
+
self._io.write_line("")
|
|
13
|
+
self._io.write_line(f"<fg=magenta>{title}:</>")
|
|
14
|
+
for r in resolutions:
|
|
15
|
+
self._io.write_line(f"<fg=magenta>Resolving project:</> <fg=blue>{r.name}</> [<fg=dark_gray>{r.path}</>]")
|
|
16
|
+
|
|
17
|
+
if r.deliver == "Enabled":
|
|
18
|
+
deliver_label = "<fg=green>Enabled</>"
|
|
19
|
+
elif r.deliver == "DisabledByPackageMode":
|
|
20
|
+
deliver_label = "<fg=red>Disabled (package-mode)</>"
|
|
21
|
+
else:
|
|
22
|
+
deliver_label = "<fg=red>Disabled (deliver option)</>"
|
|
23
|
+
self._io.write_line(f" - Deliverable: {deliver_label}")
|
|
24
|
+
|
|
25
|
+
if self._io.is_verbose():
|
|
26
|
+
for i, pr in enumerate(r.pattern_results, 1):
|
|
27
|
+
if pr.matched:
|
|
28
|
+
self._io.write_line(f" <fg=dark_gray>[{i}]</> <fg=green>matched</> '<fg=cyan>{pr.pattern}</>' -> <fg=green>{pr.resolved_raw}</>")
|
|
29
|
+
elif pr.condition and pr.errors:
|
|
30
|
+
self._io.write_line(f" <fg=dark_gray>[{i}]</> <fg=red>error</> '<fg=cyan>{pr.pattern}</>' condition invalid: {'; '.join(pr.errors)}")
|
|
31
|
+
elif pr.condition and not pr.condition_matched:
|
|
32
|
+
self._io.write_line(f" <fg=dark_gray>[{i}]</> <fg=yellow>skipped</> '<fg=cyan>{pr.pattern}</>' condition '<fg=dark_gray>{pr.condition}</>'")
|
|
33
|
+
elif pr.errors and not pr.condition:
|
|
34
|
+
self._io.write_line(f" <fg=dark_gray>[{i}]</> <fg=red>error</> '<fg=cyan>{pr.pattern}</>' invalid: {'; '.join(pr.errors)}")
|
|
35
|
+
elif pr.resolved_raw:
|
|
36
|
+
self._io.write_line(f" <fg=dark_gray>[{i}]</> <fg=yellow>failed</> '<fg=cyan>{pr.pattern}</>' -> '<fg=yellow>{pr.resolved_raw}</>' not a valid version")
|
|
37
|
+
else:
|
|
38
|
+
self._io.write_line(f" <fg=dark_gray>[{i}]</> <fg=dark_gray>ignored</> '<fg=cyan>{pr.pattern}</>'")
|
|
39
|
+
|
|
40
|
+
if r.version:
|
|
41
|
+
if self._io.is_verbose():
|
|
42
|
+
self._io.write_line(f" - Version: <fg=green>{r.version}</> (Pattern: '<fg=cyan>{r.matched_pattern}</>', Pinning rule: <fg=cyan>{r.pinning}</>)")
|
|
43
|
+
else:
|
|
44
|
+
self._io.write_line(f" - Version: <fg=green>{r.version}</>")
|
|
45
|
+
|
|
46
|
+
self._render_dependencies(r.dependencies)
|
|
47
|
+
|
|
48
|
+
def render_dependency_tree(self, title: str, roots: list[DependencyNode]) -> None:
|
|
49
|
+
self._io.write_line("")
|
|
50
|
+
self._io.write_line(f"<fg=magenta>{title}:</>")
|
|
51
|
+
for i, root in enumerate(roots):
|
|
52
|
+
is_last = i == len(roots) - 1
|
|
53
|
+
self._print_node(
|
|
54
|
+
root,
|
|
55
|
+
" " + ("└── " if is_last else "├── "),
|
|
56
|
+
" " + (" " if is_last else "│ "),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def _print_node(self, node: DependencyNode, prefix: str, child_prefix: str) -> None:
|
|
60
|
+
self._io.write_line(f"{prefix}<fg=blue>{node.name}</> <fg=green>{node.version}</>")
|
|
61
|
+
for i, child in enumerate(node.children):
|
|
62
|
+
is_last = i == len(node.children) - 1
|
|
63
|
+
self._print_node(
|
|
64
|
+
child,
|
|
65
|
+
child_prefix + ("└── " if is_last else "├── "),
|
|
66
|
+
child_prefix + (" " if is_last else "│ "),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def render_publish_waves(self, title: str, waves: list[PublishWave]) -> None:
|
|
70
|
+
self._io.write_line("")
|
|
71
|
+
self._io.write_line(f"<fg=magenta>{title}:</>")
|
|
72
|
+
for i, wave in enumerate(waves):
|
|
73
|
+
is_last_wave = i == len(waves) - 1
|
|
74
|
+
wave_prefix = "└── " if is_last_wave else "├── "
|
|
75
|
+
child_prefix = " " if is_last_wave else "│ "
|
|
76
|
+
self._io.write_line(f" {wave_prefix}<fg=magenta>Wave {wave.index}</>")
|
|
77
|
+
for j, p in enumerate(wave.projects):
|
|
78
|
+
is_last_item = j == len(wave.projects) - 1
|
|
79
|
+
item_prefix = "└── " if is_last_item else "├── "
|
|
80
|
+
self._io.write_line(f" {child_prefix}{item_prefix}<fg=blue>{p.name}</> <fg=green>{p.version}</>")
|
|
81
|
+
|
|
82
|
+
def _render_dependencies(self, dependencies: list[DependencyResolution]) -> None:
|
|
83
|
+
for dep in dependencies:
|
|
84
|
+
if dep.is_project:
|
|
85
|
+
base_line = f" - Project dependency '<fg=cyan>{dep.name}</>'"
|
|
86
|
+
if self._io.is_verbose() and dep.path:
|
|
87
|
+
self._io.write_line(f"{base_line} [<fg=dark_gray>{dep.path}</>]")
|
|
88
|
+
else:
|
|
89
|
+
self._io.write_line(base_line)
|
|
90
|
+
elif dep.source == "skipped":
|
|
91
|
+
if self._io.is_debug():
|
|
92
|
+
self._io.write_line(f" <fg=dark_gray>- Dependency '<fg=cyan>{dep.name}</> skipped (no version constraint).</>")
|
|
93
|
+
else:
|
|
94
|
+
base_line = f" - Dependency '<fg=cyan>{dep.name}</>': <fg=green>{dep.constraint}</>"
|
|
95
|
+
detail = None
|
|
96
|
+
if dep.source == "host":
|
|
97
|
+
detail = f"Resolved from <fg=cyan>host</>: <fg=green>{dep.constraint}</>"
|
|
98
|
+
elif dep.source in ("host-no-constraint", "not-found"):
|
|
99
|
+
detail = f"from <fg=cyan>{dep.source}</>"
|
|
100
|
+
elif dep.source == "direct" and self._io.is_debug():
|
|
101
|
+
detail = "<fg=dark_gray>direct</>"
|
|
102
|
+
|
|
103
|
+
if detail and self._io.is_verbose():
|
|
104
|
+
self._io.write_line(f"{base_line} ({detail})")
|
|
105
|
+
elif not self._io.is_debug() or detail:
|
|
106
|
+
self._io.write_line(base_line)
|
|
107
|
+
|
|
108
|
+
def render_message(self, text: str) -> None:
|
|
109
|
+
self._io.write_line(text)
|
|
110
|
+
|
|
111
|
+
def flush(self) -> None:
|
|
112
|
+
pass
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from cleo.io.io import IO
|
|
5
|
+
|
|
6
|
+
from ._models import DependencyNode, ProjectResolution, PublishWave
|
|
7
|
+
from ._protocol import DeliveryRenderer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class JsonDeliveryRenderer(DeliveryRenderer):
|
|
11
|
+
def __init__(self, io: IO) -> None:
|
|
12
|
+
self._io = io
|
|
13
|
+
self._data: dict[str, Any] = {}
|
|
14
|
+
|
|
15
|
+
def render_resolution(self, title: str, resolutions: list[ProjectResolution]) -> None:
|
|
16
|
+
self._data[self._key(title)] = [
|
|
17
|
+
{
|
|
18
|
+
"name": r.name,
|
|
19
|
+
"path": r.path,
|
|
20
|
+
"version": r.version,
|
|
21
|
+
"deliver": r.deliver,
|
|
22
|
+
"pinning": r.pinning,
|
|
23
|
+
"matched_pattern": r.matched_pattern,
|
|
24
|
+
"pattern_results": [
|
|
25
|
+
{
|
|
26
|
+
"pattern": pr.pattern,
|
|
27
|
+
"condition": pr.condition,
|
|
28
|
+
"condition_matched": pr.condition_matched,
|
|
29
|
+
"resolved_raw": pr.resolved_raw,
|
|
30
|
+
"matched": pr.matched,
|
|
31
|
+
"errors": pr.errors,
|
|
32
|
+
}
|
|
33
|
+
for pr in r.pattern_results
|
|
34
|
+
],
|
|
35
|
+
"dependencies": [
|
|
36
|
+
{
|
|
37
|
+
"name": d.name,
|
|
38
|
+
"constraint": d.constraint,
|
|
39
|
+
"is_project": d.is_project,
|
|
40
|
+
"path": d.path,
|
|
41
|
+
"source": d.source,
|
|
42
|
+
}
|
|
43
|
+
for d in r.dependencies
|
|
44
|
+
],
|
|
45
|
+
}
|
|
46
|
+
for r in resolutions
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
def render_dependency_tree(self, title: str, roots: list[DependencyNode]) -> None:
|
|
50
|
+
self._data[self._key(title)] = [self._node_to_dict(n) for n in roots]
|
|
51
|
+
|
|
52
|
+
def render_publish_waves(self, title: str, waves: list[PublishWave]) -> None:
|
|
53
|
+
self._data[self._key(title)] = [
|
|
54
|
+
{
|
|
55
|
+
"wave": w.index,
|
|
56
|
+
"projects": [{"name": p.name, "version": p.version} for p in w.projects],
|
|
57
|
+
}
|
|
58
|
+
for w in waves
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
def render_message(self, text: str) -> None:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def flush(self) -> None:
|
|
65
|
+
self._io.write_line(json.dumps(self._data, indent=2))
|
|
66
|
+
self._data = {}
|
|
67
|
+
|
|
68
|
+
def _node_to_dict(self, node: DependencyNode) -> dict:
|
|
69
|
+
result: dict[str, Any] = {"name": node.name, "version": node.version}
|
|
70
|
+
if node.children:
|
|
71
|
+
result["children"] = [self._node_to_dict(c) for c in node.children]
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _key(title: str) -> str:
|
|
76
|
+
return title.lower().replace(" ", "_")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class VersionPatternResult:
|
|
7
|
+
pattern: str
|
|
8
|
+
condition: str = ""
|
|
9
|
+
condition_matched: Optional[bool] = None
|
|
10
|
+
resolved_raw: str = ""
|
|
11
|
+
matched: bool = False
|
|
12
|
+
errors: list[str] = field(default_factory=list)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class DependencyResolution:
|
|
17
|
+
name: str
|
|
18
|
+
constraint: str
|
|
19
|
+
is_project: bool = False
|
|
20
|
+
path: str = ""
|
|
21
|
+
source: str = ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ProjectResolution:
|
|
26
|
+
name: str
|
|
27
|
+
path: str
|
|
28
|
+
version: str
|
|
29
|
+
deliver: str
|
|
30
|
+
pinning: str = ""
|
|
31
|
+
matched_pattern: str = ""
|
|
32
|
+
pattern_results: list[VersionPatternResult] = field(default_factory=list)
|
|
33
|
+
dependencies: list[DependencyResolution] = field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ProjectSummary:
|
|
38
|
+
name: str
|
|
39
|
+
version: str
|
|
40
|
+
deliver: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class DependencyNode:
|
|
45
|
+
name: str
|
|
46
|
+
version: str
|
|
47
|
+
children: list["DependencyNode"] = field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class PublishWave:
|
|
52
|
+
index: int
|
|
53
|
+
projects: list[ProjectSummary] = field(default_factory=list)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from typing import Protocol, runtime_checkable
|
|
3
|
+
|
|
4
|
+
from ._models import DependencyNode, ProjectResolution, PublishWave
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@runtime_checkable
|
|
8
|
+
class DeliveryRenderer(Protocol):
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def render_resolution(self, title: str, resolutions: list[ProjectResolution]) -> None: ...
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def render_dependency_tree(self, title: str, roots: list[DependencyNode]) -> None: ...
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def render_publish_waves(self, title: str, waves: list[PublishWave]) -> None: ...
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def render_message(self, text: str) -> None: ...
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def flush(self) -> None: ...
|