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 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
+ )