pactown 0.1.4__py3-none-any.whl → 0.1.47__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.
- pactown/__init__.py +178 -4
- pactown/cli.py +539 -37
- pactown/config.py +12 -11
- pactown/deploy/__init__.py +17 -3
- pactown/deploy/base.py +35 -33
- pactown/deploy/compose.py +59 -58
- pactown/deploy/docker.py +40 -41
- pactown/deploy/kubernetes.py +43 -42
- pactown/deploy/podman.py +55 -56
- pactown/deploy/quadlet.py +1021 -0
- pactown/deploy/quadlet_api.py +533 -0
- pactown/deploy/quadlet_shell.py +557 -0
- pactown/events.py +1066 -0
- pactown/fast_start.py +514 -0
- pactown/generator.py +31 -30
- pactown/llm.py +450 -0
- pactown/markpact_blocks.py +50 -0
- pactown/network.py +59 -38
- pactown/orchestrator.py +90 -93
- pactown/parallel.py +40 -40
- pactown/platform.py +146 -0
- pactown/registry/__init__.py +1 -1
- pactown/registry/client.py +45 -46
- pactown/registry/models.py +25 -25
- pactown/registry/server.py +24 -24
- pactown/resolver.py +30 -30
- pactown/runner_api.py +458 -0
- pactown/sandbox_manager.py +480 -79
- pactown/security.py +682 -0
- pactown/service_runner.py +1201 -0
- pactown/user_isolation.py +458 -0
- {pactown-0.1.4.dist-info → pactown-0.1.47.dist-info}/METADATA +65 -9
- pactown-0.1.47.dist-info/RECORD +36 -0
- pactown-0.1.47.dist-info/entry_points.txt +5 -0
- pactown-0.1.4.dist-info/RECORD +0 -24
- pactown-0.1.4.dist-info/entry_points.txt +0 -3
- {pactown-0.1.4.dist-info → pactown-0.1.47.dist-info}/WHEEL +0 -0
- {pactown-0.1.4.dist-info → pactown-0.1.47.dist-info}/licenses/LICENSE +0 -0
pactown/registry/models.py
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
|
-
from datetime import datetime
|
|
7
|
-
from typing import Optional, Any
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
import
|
|
9
|
+
from typing import Any, Optional
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
@dataclass
|
|
@@ -15,9 +15,9 @@ class ArtifactVersion:
|
|
|
15
15
|
version: str
|
|
16
16
|
readme_content: str
|
|
17
17
|
checksum: str
|
|
18
|
-
published_at: datetime = field(default_factory=datetime.
|
|
18
|
+
published_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
19
19
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
def to_dict(self) -> dict:
|
|
22
22
|
return {
|
|
23
23
|
"version": self.version,
|
|
@@ -26,7 +26,7 @@ class ArtifactVersion:
|
|
|
26
26
|
"published_at": self.published_at.isoformat(),
|
|
27
27
|
"metadata": self.metadata,
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
@classmethod
|
|
31
31
|
def from_dict(cls, data: dict) -> "ArtifactVersion":
|
|
32
32
|
return cls(
|
|
@@ -46,24 +46,24 @@ class Artifact:
|
|
|
46
46
|
description: str = ""
|
|
47
47
|
versions: dict[str, ArtifactVersion] = field(default_factory=dict)
|
|
48
48
|
latest_version: Optional[str] = None
|
|
49
|
-
created_at: datetime = field(default_factory=datetime.
|
|
50
|
-
updated_at: datetime = field(default_factory=datetime.
|
|
49
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
50
|
+
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
51
51
|
tags: list[str] = field(default_factory=list)
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
@property
|
|
54
54
|
def full_name(self) -> str:
|
|
55
55
|
return f"{self.namespace}/{self.name}"
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
def add_version(self, version: ArtifactVersion) -> None:
|
|
58
58
|
self.versions[version.version] = version
|
|
59
59
|
self.latest_version = version.version
|
|
60
|
-
self.updated_at = datetime.
|
|
61
|
-
|
|
60
|
+
self.updated_at = datetime.now(timezone.utc)
|
|
61
|
+
|
|
62
62
|
def get_version(self, version: str = "latest") -> Optional[ArtifactVersion]:
|
|
63
63
|
if version == "latest" or version == "*":
|
|
64
64
|
version = self.latest_version
|
|
65
65
|
return self.versions.get(version)
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
def to_dict(self) -> dict:
|
|
68
68
|
return {
|
|
69
69
|
"name": self.name,
|
|
@@ -75,11 +75,11 @@ class Artifact:
|
|
|
75
75
|
"updated_at": self.updated_at.isoformat(),
|
|
76
76
|
"tags": self.tags,
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
@classmethod
|
|
80
80
|
def from_dict(cls, data: dict) -> "Artifact":
|
|
81
81
|
versions = {
|
|
82
|
-
k: ArtifactVersion.from_dict(v)
|
|
82
|
+
k: ArtifactVersion.from_dict(v)
|
|
83
83
|
for k, v in data.get("versions", {}).items()
|
|
84
84
|
}
|
|
85
85
|
return cls(
|
|
@@ -96,41 +96,41 @@ class Artifact:
|
|
|
96
96
|
|
|
97
97
|
class RegistryStorage:
|
|
98
98
|
"""File-based storage for registry artifacts."""
|
|
99
|
-
|
|
99
|
+
|
|
100
100
|
def __init__(self, storage_path: Path):
|
|
101
101
|
self.storage_path = Path(storage_path)
|
|
102
102
|
self.storage_path.mkdir(parents=True, exist_ok=True)
|
|
103
103
|
self._index_path = self.storage_path / "index.json"
|
|
104
104
|
self._artifacts: dict[str, Artifact] = {}
|
|
105
105
|
self._load()
|
|
106
|
-
|
|
106
|
+
|
|
107
107
|
def _load(self) -> None:
|
|
108
108
|
if self._index_path.exists():
|
|
109
109
|
with open(self._index_path) as f:
|
|
110
110
|
data = json.load(f)
|
|
111
111
|
for full_name, artifact_data in data.get("artifacts", {}).items():
|
|
112
112
|
self._artifacts[full_name] = Artifact.from_dict(artifact_data)
|
|
113
|
-
|
|
113
|
+
|
|
114
114
|
def _save(self) -> None:
|
|
115
115
|
data = {
|
|
116
116
|
"artifacts": {k: v.to_dict() for k, v in self._artifacts.items()},
|
|
117
|
-
"updated_at": datetime.
|
|
117
|
+
"updated_at": datetime.now(timezone.utc).isoformat(),
|
|
118
118
|
}
|
|
119
119
|
with open(self._index_path, "w") as f:
|
|
120
120
|
json.dump(data, f, indent=2)
|
|
121
|
-
|
|
121
|
+
|
|
122
122
|
def get(self, namespace: str, name: str) -> Optional[Artifact]:
|
|
123
123
|
return self._artifacts.get(f"{namespace}/{name}")
|
|
124
|
-
|
|
124
|
+
|
|
125
125
|
def list(self, namespace: Optional[str] = None) -> list[Artifact]:
|
|
126
126
|
if namespace:
|
|
127
127
|
return [a for a in self._artifacts.values() if a.namespace == namespace]
|
|
128
128
|
return list(self._artifacts.values())
|
|
129
|
-
|
|
129
|
+
|
|
130
130
|
def save_artifact(self, artifact: Artifact) -> None:
|
|
131
131
|
self._artifacts[artifact.full_name] = artifact
|
|
132
132
|
self._save()
|
|
133
|
-
|
|
133
|
+
|
|
134
134
|
def delete(self, namespace: str, name: str) -> bool:
|
|
135
135
|
key = f"{namespace}/{name}"
|
|
136
136
|
if key in self._artifacts:
|
|
@@ -138,12 +138,12 @@ class RegistryStorage:
|
|
|
138
138
|
self._save()
|
|
139
139
|
return True
|
|
140
140
|
return False
|
|
141
|
-
|
|
141
|
+
|
|
142
142
|
def search(self, query: str) -> list[Artifact]:
|
|
143
143
|
query = query.lower()
|
|
144
144
|
results = []
|
|
145
145
|
for artifact in self._artifacts.values():
|
|
146
|
-
if (query in artifact.name.lower() or
|
|
146
|
+
if (query in artifact.name.lower() or
|
|
147
147
|
query in artifact.description.lower() or
|
|
148
148
|
any(query in tag.lower() for tag in artifact.tags)):
|
|
149
149
|
results.append(artifact)
|
pactown/registry/server.py
CHANGED
|
@@ -49,13 +49,13 @@ class VersionInfo(BaseModel):
|
|
|
49
49
|
|
|
50
50
|
def create_app(storage_path: str = "./.pactown-registry") -> FastAPI:
|
|
51
51
|
"""Create the registry FastAPI application."""
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
app = FastAPI(
|
|
54
54
|
title="Pactown Registry",
|
|
55
55
|
description="Local artifact registry for markpact modules",
|
|
56
56
|
version="0.1.0",
|
|
57
57
|
)
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
app.add_middleware(
|
|
60
60
|
CORSMiddleware,
|
|
61
61
|
allow_origins=["*"],
|
|
@@ -63,13 +63,13 @@ def create_app(storage_path: str = "./.pactown-registry") -> FastAPI:
|
|
|
63
63
|
allow_methods=["*"],
|
|
64
64
|
allow_headers=["*"],
|
|
65
65
|
)
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
storage = RegistryStorage(Path(storage_path))
|
|
68
|
-
|
|
68
|
+
|
|
69
69
|
@app.get("/health")
|
|
70
70
|
def health():
|
|
71
71
|
return {"status": "ok", "service": "pactown-registry"}
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
@app.get("/v1/artifacts", response_model=list[ArtifactInfo])
|
|
74
74
|
def list_artifacts(
|
|
75
75
|
namespace: Optional[str] = Query(None, description="Filter by namespace"),
|
|
@@ -79,7 +79,7 @@ def create_app(storage_path: str = "./.pactown-registry") -> FastAPI:
|
|
|
79
79
|
artifacts = storage.search(search)
|
|
80
80
|
else:
|
|
81
81
|
artifacts = storage.list(namespace)
|
|
82
|
-
|
|
82
|
+
|
|
83
83
|
return [
|
|
84
84
|
ArtifactInfo(
|
|
85
85
|
name=a.name,
|
|
@@ -91,13 +91,13 @@ def create_app(storage_path: str = "./.pactown-registry") -> FastAPI:
|
|
|
91
91
|
)
|
|
92
92
|
for a in artifacts
|
|
93
93
|
]
|
|
94
|
-
|
|
94
|
+
|
|
95
95
|
@app.get("/v1/artifacts/{namespace}/{name}", response_model=ArtifactInfo)
|
|
96
96
|
def get_artifact(namespace: str, name: str):
|
|
97
97
|
artifact = storage.get(namespace, name)
|
|
98
98
|
if not artifact:
|
|
99
99
|
raise HTTPException(status_code=404, detail="Artifact not found")
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
return ArtifactInfo(
|
|
102
102
|
name=artifact.name,
|
|
103
103
|
namespace=artifact.namespace,
|
|
@@ -106,17 +106,17 @@ def create_app(storage_path: str = "./.pactown-registry") -> FastAPI:
|
|
|
106
106
|
versions=list(artifact.versions.keys()),
|
|
107
107
|
tags=artifact.tags,
|
|
108
108
|
)
|
|
109
|
-
|
|
109
|
+
|
|
110
110
|
@app.get("/v1/artifacts/{namespace}/{name}/{version}", response_model=VersionInfo)
|
|
111
111
|
def get_version(namespace: str, name: str, version: str):
|
|
112
112
|
artifact = storage.get(namespace, name)
|
|
113
113
|
if not artifact:
|
|
114
114
|
raise HTTPException(status_code=404, detail="Artifact not found")
|
|
115
|
-
|
|
115
|
+
|
|
116
116
|
ver = artifact.get_version(version)
|
|
117
117
|
if not ver:
|
|
118
118
|
raise HTTPException(status_code=404, detail="Version not found")
|
|
119
|
-
|
|
119
|
+
|
|
120
120
|
return VersionInfo(
|
|
121
121
|
version=ver.version,
|
|
122
122
|
readme_content=ver.readme_content,
|
|
@@ -124,23 +124,23 @@ def create_app(storage_path: str = "./.pactown-registry") -> FastAPI:
|
|
|
124
124
|
published_at=ver.published_at.isoformat(),
|
|
125
125
|
metadata=ver.metadata,
|
|
126
126
|
)
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
@app.get("/v1/artifacts/{namespace}/{name}/{version}/readme")
|
|
129
129
|
def get_readme(namespace: str, name: str, version: str):
|
|
130
130
|
artifact = storage.get(namespace, name)
|
|
131
131
|
if not artifact:
|
|
132
132
|
raise HTTPException(status_code=404, detail="Artifact not found")
|
|
133
|
-
|
|
133
|
+
|
|
134
134
|
ver = artifact.get_version(version)
|
|
135
135
|
if not ver:
|
|
136
136
|
raise HTTPException(status_code=404, detail="Version not found")
|
|
137
|
-
|
|
137
|
+
|
|
138
138
|
return {"content": ver.readme_content}
|
|
139
|
-
|
|
139
|
+
|
|
140
140
|
@app.post("/v1/publish", response_model=PublishResponse)
|
|
141
141
|
def publish(req: PublishRequest):
|
|
142
142
|
checksum = hashlib.sha256(req.readme_content.encode()).hexdigest()
|
|
143
|
-
|
|
143
|
+
|
|
144
144
|
artifact = storage.get(req.namespace, req.name)
|
|
145
145
|
if not artifact:
|
|
146
146
|
artifact = Artifact(
|
|
@@ -149,40 +149,40 @@ def create_app(storage_path: str = "./.pactown-registry") -> FastAPI:
|
|
|
149
149
|
description=req.description,
|
|
150
150
|
tags=req.tags,
|
|
151
151
|
)
|
|
152
|
-
|
|
152
|
+
|
|
153
153
|
version = ArtifactVersion(
|
|
154
154
|
version=req.version,
|
|
155
155
|
readme_content=req.readme_content,
|
|
156
156
|
checksum=checksum,
|
|
157
157
|
metadata=req.metadata,
|
|
158
158
|
)
|
|
159
|
-
|
|
159
|
+
|
|
160
160
|
artifact.add_version(version)
|
|
161
161
|
if req.description:
|
|
162
162
|
artifact.description = req.description
|
|
163
163
|
if req.tags:
|
|
164
164
|
artifact.tags = list(set(artifact.tags + req.tags))
|
|
165
|
-
|
|
165
|
+
|
|
166
166
|
storage.save_artifact(artifact)
|
|
167
|
-
|
|
167
|
+
|
|
168
168
|
return PublishResponse(
|
|
169
169
|
success=True,
|
|
170
170
|
artifact=artifact.full_name,
|
|
171
171
|
version=req.version,
|
|
172
172
|
checksum=checksum,
|
|
173
173
|
)
|
|
174
|
-
|
|
174
|
+
|
|
175
175
|
@app.delete("/v1/artifacts/{namespace}/{name}")
|
|
176
176
|
def delete_artifact(namespace: str, name: str):
|
|
177
177
|
if storage.delete(namespace, name):
|
|
178
178
|
return {"success": True, "message": f"Deleted {namespace}/{name}"}
|
|
179
179
|
raise HTTPException(status_code=404, detail="Artifact not found")
|
|
180
|
-
|
|
180
|
+
|
|
181
181
|
@app.get("/v1/namespaces")
|
|
182
182
|
def list_namespaces():
|
|
183
183
|
namespaces = set(a.namespace for a in storage.list())
|
|
184
184
|
return {"namespaces": sorted(namespaces)}
|
|
185
|
-
|
|
185
|
+
|
|
186
186
|
return app
|
|
187
187
|
|
|
188
188
|
|
|
@@ -193,7 +193,7 @@ def create_app(storage_path: str = "./.pactown-registry") -> FastAPI:
|
|
|
193
193
|
@click.option("--reload", is_flag=True, help="Enable auto-reload")
|
|
194
194
|
def main(host: str, port: int, storage: str, reload: bool):
|
|
195
195
|
"""Start the pactown registry server."""
|
|
196
|
-
|
|
196
|
+
create_app(storage)
|
|
197
197
|
uvicorn.run(
|
|
198
198
|
"pactown.registry.server:create_app",
|
|
199
199
|
host=host,
|
pactown/resolver.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Dependency resolver for pactown ecosystems."""
|
|
2
2
|
|
|
3
|
+
from collections import deque
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from typing import Optional
|
|
5
|
-
from collections import deque
|
|
6
6
|
|
|
7
7
|
from .config import EcosystemConfig, ServiceConfig
|
|
8
8
|
|
|
@@ -19,12 +19,12 @@ class ResolvedDependency:
|
|
|
19
19
|
|
|
20
20
|
class DependencyResolver:
|
|
21
21
|
"""Resolves dependencies between services in an ecosystem."""
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
def __init__(self, config: EcosystemConfig):
|
|
24
24
|
self.config = config
|
|
25
25
|
self._graph: dict[str, list[str]] = {}
|
|
26
26
|
self._build_graph()
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
def _build_graph(self) -> None:
|
|
29
29
|
"""Build dependency graph from configuration."""
|
|
30
30
|
for name, service in self.config.services.items():
|
|
@@ -32,7 +32,7 @@ class DependencyResolver:
|
|
|
32
32
|
for dep in service.depends_on:
|
|
33
33
|
if dep.name in self.config.services:
|
|
34
34
|
self._graph[name].append(dep.name)
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
def get_startup_order(self) -> list[str]:
|
|
37
37
|
"""
|
|
38
38
|
Get services in topological order for startup.
|
|
@@ -40,46 +40,46 @@ class DependencyResolver:
|
|
|
40
40
|
"""
|
|
41
41
|
# in_degree[X] = number of dependencies X has
|
|
42
42
|
in_degree = {name: len(deps) for name, deps in self._graph.items()}
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
# Start with services that have no dependencies
|
|
45
45
|
queue = deque([name for name, degree in in_degree.items() if degree == 0])
|
|
46
46
|
order = []
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
while queue:
|
|
49
49
|
current = queue.popleft()
|
|
50
50
|
order.append(current)
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
# For each service that depends on current, decrease its in_degree
|
|
53
53
|
for name, deps in self._graph.items():
|
|
54
54
|
if current in deps:
|
|
55
55
|
in_degree[name] -= 1
|
|
56
56
|
if in_degree[name] == 0:
|
|
57
57
|
queue.append(name)
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
if len(order) != len(self._graph):
|
|
60
60
|
missing = set(self._graph.keys()) - set(order)
|
|
61
61
|
raise ValueError(f"Circular dependency detected involving: {missing}")
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
return order
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
def get_shutdown_order(self) -> list[str]:
|
|
66
66
|
"""Get services in reverse order for shutdown."""
|
|
67
67
|
return list(reversed(self.get_startup_order()))
|
|
68
|
-
|
|
68
|
+
|
|
69
69
|
def resolve_service_deps(self, service_name: str) -> list[ResolvedDependency]:
|
|
70
70
|
"""Resolve all dependencies for a service."""
|
|
71
71
|
if service_name not in self.config.services:
|
|
72
72
|
raise ValueError(f"Unknown service: {service_name}")
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
service = self.config.services[service_name]
|
|
75
75
|
resolved = []
|
|
76
|
-
|
|
76
|
+
|
|
77
77
|
for dep in service.depends_on:
|
|
78
78
|
if dep.name in self.config.services:
|
|
79
79
|
dep_service = self.config.services[dep.name]
|
|
80
80
|
endpoint = dep.endpoint or f"http://localhost:{dep_service.port}"
|
|
81
81
|
env_var = dep.env_var or f"{dep.name.upper().replace('-', '_')}_URL"
|
|
82
|
-
|
|
82
|
+
|
|
83
83
|
resolved.append(ResolvedDependency(
|
|
84
84
|
name=dep.name,
|
|
85
85
|
version=dep.version,
|
|
@@ -90,43 +90,43 @@ class DependencyResolver:
|
|
|
90
90
|
else:
|
|
91
91
|
endpoint = dep.endpoint or f"http://localhost:8800/v1/{dep.name}"
|
|
92
92
|
env_var = dep.env_var or f"{dep.name.upper().replace('-', '_')}_URL"
|
|
93
|
-
|
|
93
|
+
|
|
94
94
|
resolved.append(ResolvedDependency(
|
|
95
95
|
name=dep.name,
|
|
96
96
|
version=dep.version,
|
|
97
97
|
endpoint=endpoint,
|
|
98
98
|
env_var=env_var,
|
|
99
99
|
))
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
return resolved
|
|
102
|
-
|
|
102
|
+
|
|
103
103
|
def get_environment(self, service_name: str) -> dict[str, str]:
|
|
104
104
|
"""Get environment variables for a service including dependency endpoints."""
|
|
105
105
|
if service_name not in self.config.services:
|
|
106
106
|
raise ValueError(f"Unknown service: {service_name}")
|
|
107
|
-
|
|
107
|
+
|
|
108
108
|
service = self.config.services[service_name]
|
|
109
109
|
env = dict(service.env)
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
for dep in self.resolve_service_deps(service_name):
|
|
112
112
|
env[dep.env_var] = dep.endpoint
|
|
113
|
-
|
|
113
|
+
|
|
114
114
|
env["PACTOWN_SERVICE_NAME"] = service_name
|
|
115
115
|
env["PACTOWN_ECOSYSTEM"] = self.config.name
|
|
116
116
|
if service.port:
|
|
117
117
|
env["MARKPACT_PORT"] = str(service.port)
|
|
118
|
-
|
|
118
|
+
|
|
119
119
|
return env
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
def validate(self) -> list[str]:
|
|
122
122
|
"""Validate the dependency graph and return any issues."""
|
|
123
123
|
issues = []
|
|
124
|
-
|
|
124
|
+
|
|
125
125
|
try:
|
|
126
126
|
self.get_startup_order()
|
|
127
127
|
except ValueError as e:
|
|
128
128
|
issues.append(str(e))
|
|
129
|
-
|
|
129
|
+
|
|
130
130
|
for name, service in self.config.services.items():
|
|
131
131
|
for dep in service.depends_on:
|
|
132
132
|
if dep.name not in self.config.services:
|
|
@@ -135,26 +135,26 @@ class DependencyResolver:
|
|
|
135
135
|
f"Service '{name}' depends on '{dep.name}' which is not "
|
|
136
136
|
f"defined locally and no registry is configured"
|
|
137
137
|
)
|
|
138
|
-
|
|
138
|
+
|
|
139
139
|
return issues
|
|
140
|
-
|
|
140
|
+
|
|
141
141
|
def print_graph(self) -> str:
|
|
142
142
|
"""Return ASCII representation of dependency graph."""
|
|
143
143
|
lines = [f"Ecosystem: {self.config.name}", ""]
|
|
144
|
-
|
|
144
|
+
|
|
145
145
|
try:
|
|
146
146
|
order = self.get_startup_order()
|
|
147
147
|
except ValueError:
|
|
148
148
|
order = list(self._graph.keys())
|
|
149
|
-
|
|
149
|
+
|
|
150
150
|
for name in order:
|
|
151
151
|
service = self.config.services[name]
|
|
152
152
|
deps = [d.name for d in service.depends_on]
|
|
153
153
|
port = f":{service.port}" if service.port else ""
|
|
154
|
-
|
|
154
|
+
|
|
155
155
|
if deps:
|
|
156
156
|
lines.append(f" [{name}{port}] → {', '.join(deps)}")
|
|
157
157
|
else:
|
|
158
158
|
lines.append(f" [{name}{port}] (no deps)")
|
|
159
|
-
|
|
159
|
+
|
|
160
160
|
return "\n".join(lines)
|