compos-cli 0.0.0__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.
- compos/cli/__init__.py +0 -0
- compos/cli/analyzers/__init__.py +10 -0
- compos/cli/analyzers/base.py +66 -0
- compos/cli/analyzers/docker_compose.py +137 -0
- compos/cli/analyzers/python.py +604 -0
- compos/cli/analyzers/typescript.py +823 -0
- compos/cli/git.py +92 -0
- compos/cli/main.py +464 -0
- compos/cli/watcher.py +1 -0
- compos/core/__init__.py +119 -0
- compos/core/diff.py +131 -0
- compos/core/graph.py +648 -0
- compos/core/integrity.py +346 -0
- compos/core/merge.py +289 -0
- compos/core/merge_log.py +128 -0
- compos/core/versioning.py +43 -0
- compos/core/write_pipeline.py +574 -0
- compos/schema/__init__.py +57 -0
- compos/schema/models.py +440 -0
- compos/schema/validation.py +1 -0
- compos/schema/versioning.py +1 -0
- compos/storage/__init__.py +29 -0
- compos/storage/local.py +209 -0
- compos/storage/locking.py +74 -0
- compos/storage/merge_log.py +92 -0
- compos_cli-0.0.0.dist-info/METADATA +16 -0
- compos_cli-0.0.0.dist-info/RECORD +29 -0
- compos_cli-0.0.0.dist-info/WHEEL +4 -0
- compos_cli-0.0.0.dist-info/entry_points.txt +7 -0
compos/cli/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Analyzers package — pluggable static analysis detectors."""
|
|
2
|
+
|
|
3
|
+
from compos.cli.analyzers.base import (
|
|
4
|
+
AnalysisResult,
|
|
5
|
+
Analyzer,
|
|
6
|
+
discover_analyzers,
|
|
7
|
+
merge_results,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = ["Analyzer", "AnalysisResult", "discover_analyzers", "merge_results"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Analyzer Protocol + registry (entry-point extensible)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.metadata
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from compos.schema.models import Component, Relationship
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class AnalysisResult:
|
|
19
|
+
"""Output from a single analyzer run."""
|
|
20
|
+
|
|
21
|
+
components: tuple[Component, ...]
|
|
22
|
+
relationships: tuple[Relationship, ...]
|
|
23
|
+
warnings: tuple[str, ...]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@runtime_checkable
|
|
27
|
+
class Analyzer(Protocol):
|
|
28
|
+
"""Interface every analyzer must satisfy."""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
|
|
32
|
+
def can_analyze(self, project_root: Path) -> bool: ...
|
|
33
|
+
def analyze(self, project_root: Path) -> AnalysisResult: ...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def merge_results(*results: AnalysisResult) -> AnalysisResult:
|
|
37
|
+
"""Combine outputs from multiple analyzers."""
|
|
38
|
+
return AnalysisResult(
|
|
39
|
+
components=tuple(c for r in results for c in r.components),
|
|
40
|
+
relationships=tuple(r2 for r in results for r2 in r.relationships),
|
|
41
|
+
warnings=tuple(w for r in results for w in r.warnings),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def discover_analyzers() -> list[Analyzer]:
|
|
46
|
+
"""Load analyzers from the ``compos.analyzers`` entry-point group."""
|
|
47
|
+
eps = importlib.metadata.entry_points(group="compos.analyzers")
|
|
48
|
+
analyzers: list[Analyzer] = []
|
|
49
|
+
for ep in eps:
|
|
50
|
+
try:
|
|
51
|
+
cls = ep.load()
|
|
52
|
+
instance = cls()
|
|
53
|
+
if isinstance(instance, Analyzer):
|
|
54
|
+
analyzers.append(instance)
|
|
55
|
+
else:
|
|
56
|
+
logger.warning(
|
|
57
|
+
"Entry point %s does not satisfy Analyzer protocol",
|
|
58
|
+
ep.name,
|
|
59
|
+
)
|
|
60
|
+
except Exception:
|
|
61
|
+
logger.warning(
|
|
62
|
+
"Failed to load analyzer entry point: %s",
|
|
63
|
+
ep.name,
|
|
64
|
+
exc_info=True,
|
|
65
|
+
)
|
|
66
|
+
return analyzers
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Docker Compose service and dependency detection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from compos.cli.analyzers.base import AnalysisResult
|
|
12
|
+
from compos.schema.models import (
|
|
13
|
+
Component,
|
|
14
|
+
ComponentType,
|
|
15
|
+
ObjectStatus,
|
|
16
|
+
Provenance,
|
|
17
|
+
ProvenanceSource,
|
|
18
|
+
Relationship,
|
|
19
|
+
RelationshipPattern,
|
|
20
|
+
RelationshipType,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
_COMPOSE_FILENAMES = (
|
|
26
|
+
"docker-compose.yml",
|
|
27
|
+
"docker-compose.yaml",
|
|
28
|
+
"compose.yml",
|
|
29
|
+
"compose.yaml",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_IMAGE_TYPE_MAP: dict[str, ComponentType] = {
|
|
33
|
+
"postgres": ComponentType.DATABASE,
|
|
34
|
+
"mysql": ComponentType.DATABASE,
|
|
35
|
+
"mariadb": ComponentType.DATABASE,
|
|
36
|
+
"mongo": ComponentType.DATABASE,
|
|
37
|
+
"redis": ComponentType.DATABASE,
|
|
38
|
+
"rabbitmq": ComponentType.QUEUE,
|
|
39
|
+
"kafka": ComponentType.QUEUE,
|
|
40
|
+
"zookeeper": ComponentType.OTHER,
|
|
41
|
+
"nginx": ComponentType.OTHER,
|
|
42
|
+
"traefik": ComponentType.OTHER,
|
|
43
|
+
"elasticsearch": ComponentType.DATABASE,
|
|
44
|
+
"memcached": ComponentType.DATABASE,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _make_provenance() -> Provenance:
|
|
49
|
+
return Provenance(
|
|
50
|
+
source=ProvenanceSource.STATIC_ANALYSIS,
|
|
51
|
+
tool="cli-analyze",
|
|
52
|
+
timestamp=datetime.now(UTC),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _type_from_image(image: str) -> ComponentType:
|
|
57
|
+
base = image.split(":")[0].split("/")[-1]
|
|
58
|
+
for key, comp_type in _IMAGE_TYPE_MAP.items():
|
|
59
|
+
if key in base:
|
|
60
|
+
return comp_type
|
|
61
|
+
return ComponentType.SERVICE
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class DockerComposeAnalyzer:
|
|
65
|
+
name = "docker-compose"
|
|
66
|
+
|
|
67
|
+
def can_analyze(self, project_root: Path) -> bool:
|
|
68
|
+
return any((project_root / n).is_file() for n in _COMPOSE_FILENAMES)
|
|
69
|
+
|
|
70
|
+
def analyze(self, project_root: Path) -> AnalysisResult:
|
|
71
|
+
compose_file = next(
|
|
72
|
+
(
|
|
73
|
+
project_root / n
|
|
74
|
+
for n in _COMPOSE_FILENAMES
|
|
75
|
+
if (project_root / n).is_file()
|
|
76
|
+
),
|
|
77
|
+
None,
|
|
78
|
+
)
|
|
79
|
+
if compose_file is None:
|
|
80
|
+
return AnalysisResult((), (), ())
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
data = yaml.safe_load(compose_file.read_text())
|
|
84
|
+
except yaml.YAMLError as exc:
|
|
85
|
+
return AnalysisResult((), (), (f"Failed to parse {compose_file}: {exc}",))
|
|
86
|
+
|
|
87
|
+
services = (data or {}).get("services") or {}
|
|
88
|
+
if not isinstance(services, dict):
|
|
89
|
+
msg = f"Unexpected 'services' format in {compose_file}"
|
|
90
|
+
return AnalysisResult((), (), (msg,))
|
|
91
|
+
|
|
92
|
+
components: list[Component] = []
|
|
93
|
+
relationships: list[Relationship] = []
|
|
94
|
+
prov = _make_provenance()
|
|
95
|
+
|
|
96
|
+
for svc_name, svc_config in services.items():
|
|
97
|
+
if not isinstance(svc_config, dict):
|
|
98
|
+
continue
|
|
99
|
+
image = svc_config.get("image", "")
|
|
100
|
+
comp_type = _type_from_image(image) if image else ComponentType.SERVICE
|
|
101
|
+
|
|
102
|
+
components.append(
|
|
103
|
+
Component(
|
|
104
|
+
id=f"dc-{svc_name}",
|
|
105
|
+
name=svc_name,
|
|
106
|
+
responsibility=f"Docker Compose service: {svc_name}",
|
|
107
|
+
type=comp_type,
|
|
108
|
+
detection_confidence=0.95,
|
|
109
|
+
status=ObjectStatus.CANDIDATE,
|
|
110
|
+
provenance=prov,
|
|
111
|
+
paths=(str(compose_file.relative_to(project_root)),),
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
depends_on = svc_config.get("depends_on", [])
|
|
116
|
+
if isinstance(depends_on, dict):
|
|
117
|
+
depends_on = list(depends_on.keys())
|
|
118
|
+
for dep in depends_on:
|
|
119
|
+
relationships.append(
|
|
120
|
+
Relationship(
|
|
121
|
+
id=f"rel-dc-{svc_name}-dc-{dep}",
|
|
122
|
+
source=f"dc-{svc_name}",
|
|
123
|
+
target=f"dc-{dep}",
|
|
124
|
+
type=RelationshipType.DEPENDENCY,
|
|
125
|
+
pattern=RelationshipPattern.SYNCHRONOUS,
|
|
126
|
+
description=f"Docker Compose depends_on: {svc_name} → {dep}",
|
|
127
|
+
status=ObjectStatus.CANDIDATE,
|
|
128
|
+
provenance=prov,
|
|
129
|
+
detection_confidence=0.90,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return AnalysisResult(
|
|
134
|
+
components=tuple(components),
|
|
135
|
+
relationships=tuple(relationships),
|
|
136
|
+
warnings=(),
|
|
137
|
+
)
|