codeupipe 0.1.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.
Files changed (60) hide show
  1. codeupipe/__init__.py +39 -0
  2. codeupipe/cli.py +1502 -0
  3. codeupipe/converter/__init__.py +9 -0
  4. codeupipe/converter/config.py +119 -0
  5. codeupipe/converter/filters/__init__.py +21 -0
  6. codeupipe/converter/filters/analyze.py +60 -0
  7. codeupipe/converter/filters/classify.py +52 -0
  8. codeupipe/converter/filters/classify_files.py +61 -0
  9. codeupipe/converter/filters/generate_export.py +187 -0
  10. codeupipe/converter/filters/generate_import.py +229 -0
  11. codeupipe/converter/filters/parse_config.py +26 -0
  12. codeupipe/converter/filters/scan_project.py +52 -0
  13. codeupipe/converter/pipelines/__init__.py +8 -0
  14. codeupipe/converter/pipelines/export_pipeline.py +40 -0
  15. codeupipe/converter/pipelines/import_pipeline.py +40 -0
  16. codeupipe/converter/taps/__init__.py +7 -0
  17. codeupipe/converter/taps/conversion_log.py +46 -0
  18. codeupipe/core/__init__.py +20 -0
  19. codeupipe/core/filter.py +27 -0
  20. codeupipe/core/hook.py +34 -0
  21. codeupipe/core/payload.py +94 -0
  22. codeupipe/core/pipeline.py +231 -0
  23. codeupipe/core/state.py +78 -0
  24. codeupipe/core/stream_filter.py +33 -0
  25. codeupipe/core/tap.py +27 -0
  26. codeupipe/core/valve.py +52 -0
  27. codeupipe/linter/__init__.py +57 -0
  28. codeupipe/linter/assemble_doc_report.py +97 -0
  29. codeupipe/linter/assemble_report.py +140 -0
  30. codeupipe/linter/check_bundle.py +47 -0
  31. codeupipe/linter/check_index.py +88 -0
  32. codeupipe/linter/check_naming.py +47 -0
  33. codeupipe/linter/check_protocols.py +73 -0
  34. codeupipe/linter/check_structure.py +41 -0
  35. codeupipe/linter/check_symbols.py +116 -0
  36. codeupipe/linter/check_tests.py +48 -0
  37. codeupipe/linter/coverage_pipeline.py +39 -0
  38. codeupipe/linter/detect_drift.py +44 -0
  39. codeupipe/linter/detect_orphans.py +106 -0
  40. codeupipe/linter/doc_check_pipeline.py +28 -0
  41. codeupipe/linter/git_history.py +130 -0
  42. codeupipe/linter/lint_pipeline.py +51 -0
  43. codeupipe/linter/map_coverage.py +84 -0
  44. codeupipe/linter/report_gaps.py +68 -0
  45. codeupipe/linter/report_pipeline.py +49 -0
  46. codeupipe/linter/resolve_refs.py +48 -0
  47. codeupipe/linter/scan_components.py +95 -0
  48. codeupipe/linter/scan_directory.py +123 -0
  49. codeupipe/linter/scan_docs.py +62 -0
  50. codeupipe/linter/scan_tests.py +104 -0
  51. codeupipe/py.typed +1 -0
  52. codeupipe/testing.py +344 -0
  53. codeupipe/utils/__init__.py +10 -0
  54. codeupipe/utils/error_handling.py +68 -0
  55. codeupipe-0.1.0.dist-info/METADATA +216 -0
  56. codeupipe-0.1.0.dist-info/RECORD +60 -0
  57. codeupipe-0.1.0.dist-info/WHEEL +5 -0
  58. codeupipe-0.1.0.dist-info/entry_points.txt +2 -0
  59. codeupipe-0.1.0.dist-info/licenses/LICENSE +190 -0
  60. codeupipe-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,9 @@
1
+ """
2
+ Converter: CUP ↔ Standard Python bidirectional conversion.
3
+
4
+ Built with CUP — the converter itself is a set of Filters and Pipelines.
5
+ """
6
+
7
+ from .config import load_config, DEFAULT_CONFIG, PATTERN_DEFAULTS
8
+
9
+ __all__ = ["load_config", "DEFAULT_CONFIG", "PATTERN_DEFAULTS"]
@@ -0,0 +1,119 @@
1
+ """
2
+ Config: Conversion configuration parsing.
3
+
4
+ Reads .cup.json (zero deps) or pyproject.toml [tool.cup] (Python 3.11+).
5
+ Provides sensible defaults for MVC, Clean, Hexagonal, and Flat patterns.
6
+ """
7
+
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Any, Dict, Optional
11
+
12
+ __all__ = ["load_config", "DEFAULT_CONFIG", "PATTERN_DEFAULTS"]
13
+
14
+
15
+ PATTERN_DEFAULTS: Dict[str, Dict[str, Any]] = {
16
+ "mvc": {
17
+ "roles": {
18
+ "model": ["fetch_*", "save_*", "db_*", "load_*", "store_*"],
19
+ "view": ["format_*", "render_*", "serialize_*", "display_*"],
20
+ "controller": ["validate_*", "authorize_*", "calc_*", "process_*", "route_*"],
21
+ "middleware": ["_tap", "_hook", "_valve"],
22
+ },
23
+ "output": {
24
+ "model": "models/",
25
+ "view": "views/",
26
+ "controller": "controllers/",
27
+ "middleware": "middleware/",
28
+ },
29
+ },
30
+ "clean": {
31
+ "roles": {
32
+ "entity": ["_payload"],
33
+ "use_case": ["calc_*", "process_*", "validate_*", "apply_*", "check_*"],
34
+ "interface_adapter": ["fetch_*", "save_*", "db_*", "send_*", "_tap", "_valve"],
35
+ "framework": ["_hook", "retry_*", "log_*"],
36
+ },
37
+ "output": {
38
+ "entity": "entities/",
39
+ "use_case": "use_cases/",
40
+ "interface_adapter": "interface_adapters/",
41
+ "framework": "frameworks/",
42
+ },
43
+ },
44
+ "hexagonal": {
45
+ "roles": {
46
+ "domain": ["calc_*", "process_*", "validate_*", "apply_*", "check_*"],
47
+ "adapter_inbound": ["parse_*", "deserialize_*", "route_*"],
48
+ "adapter_outbound": ["fetch_*", "save_*", "db_*", "send_*", "publish_*"],
49
+ "port": ["_protocol"],
50
+ "infrastructure": ["_tap", "_hook", "_valve", "retry_*", "log_*"],
51
+ },
52
+ "output": {
53
+ "domain": "domain/",
54
+ "adapter_inbound": "adapters/inbound/",
55
+ "adapter_outbound": "adapters/outbound/",
56
+ "port": "ports/",
57
+ "infrastructure": "infrastructure/",
58
+ },
59
+ },
60
+ "flat": {
61
+ "roles": {
62
+ "step": ["*"],
63
+ },
64
+ "output": {
65
+ "step": "steps/",
66
+ },
67
+ },
68
+ }
69
+
70
+ DEFAULT_CONFIG: Dict[str, Any] = {
71
+ "pattern": "flat",
72
+ "roles": PATTERN_DEFAULTS["flat"]["roles"],
73
+ "output": {
74
+ "base": "src/",
75
+ **PATTERN_DEFAULTS["flat"]["output"],
76
+ },
77
+ }
78
+
79
+
80
+ def load_config(config_path: Optional[str] = None, pattern: Optional[str] = None) -> Dict[str, Any]:
81
+ """
82
+ Load conversion config from .cup.json, or fall back to pattern defaults.
83
+
84
+ Priority:
85
+ 1. Explicit config_path (.cup.json)
86
+ 2. Pattern name → built-in defaults
87
+ 3. DEFAULT_CONFIG (flat)
88
+ """
89
+ if config_path:
90
+ path = Path(config_path)
91
+ if path.exists() and path.suffix == ".json":
92
+ with open(path) as f:
93
+ config = json.load(f)
94
+ return _resolve_config(config)
95
+
96
+ if pattern and pattern in PATTERN_DEFAULTS:
97
+ return _resolve_config({"pattern": pattern})
98
+
99
+ return DEFAULT_CONFIG.copy()
100
+
101
+
102
+ def _resolve_config(raw: Dict[str, Any]) -> Dict[str, Any]:
103
+ """Merge raw config with pattern defaults."""
104
+ pattern = raw.get("pattern", "flat")
105
+ defaults = PATTERN_DEFAULTS.get(pattern, PATTERN_DEFAULTS["flat"])
106
+
107
+ roles = raw.get("roles", defaults["roles"])
108
+ output_dirs = raw.get("output", {})
109
+ base = output_dirs.pop("base", "src/") if isinstance(output_dirs, dict) else "src/"
110
+
111
+ # Merge: explicit output dirs override pattern defaults
112
+ resolved_output = {**defaults["output"], **output_dirs}
113
+ resolved_output["base"] = base
114
+
115
+ return {
116
+ "pattern": pattern,
117
+ "roles": roles,
118
+ "output": resolved_output,
119
+ }
@@ -0,0 +1,21 @@
1
+ """
2
+ Converter Filters — CUP Filters that power the conversion pipelines.
3
+ """
4
+
5
+ from .parse_config import ParseConfigFilter
6
+ from .analyze import AnalyzePipelineFilter
7
+ from .classify import ClassifyStepsFilter
8
+ from .classify_files import ClassifyFilesFilter
9
+ from .generate_export import GenerateExportFilter
10
+ from .scan_project import ScanProjectFilter
11
+ from .generate_import import GenerateImportFilter
12
+
13
+ __all__ = [
14
+ "ParseConfigFilter",
15
+ "AnalyzePipelineFilter",
16
+ "ClassifyStepsFilter",
17
+ "ClassifyFilesFilter",
18
+ "GenerateExportFilter",
19
+ "ScanProjectFilter",
20
+ "GenerateImportFilter",
21
+ ]
@@ -0,0 +1,60 @@
1
+ """
2
+ AnalyzePipelineFilter: Introspects a CUP Pipeline and extracts its step manifest.
3
+ """
4
+
5
+ import inspect
6
+ from typing import Any, Dict, List
7
+ from codeupipe import Payload
8
+
9
+
10
+ class AnalyzePipelineFilter:
11
+ """
12
+ Filter: Analyze a CUP Pipeline instance and extract step metadata.
13
+
14
+ Input payload keys:
15
+ - pipeline: A Pipeline instance to analyze
16
+
17
+ Output payload adds:
18
+ - steps (list[dict]): Step manifest with name, type, class info
19
+ - hooks (list[dict]): Hook manifest with class info
20
+ """
21
+
22
+ def call(self, payload):
23
+ pipeline = payload.get("pipeline")
24
+ if pipeline is None:
25
+ raise ValueError("Payload must contain 'pipeline' key")
26
+
27
+ steps = []
28
+ for name, instance, step_type in pipeline._steps:
29
+ info: Dict[str, Any] = {
30
+ "name": name,
31
+ "type": step_type,
32
+ "class_name": instance.__class__.__name__,
33
+ "is_valve": hasattr(instance, "_predicate"),
34
+ }
35
+
36
+ if info["is_valve"]:
37
+ info["type"] = "valve"
38
+ info["inner_class"] = instance._inner.__class__.__name__
39
+
40
+ # Try to capture source for export
41
+ try:
42
+ info["source"] = inspect.getsource(instance.__class__)
43
+ except (OSError, TypeError):
44
+ info["source"] = None
45
+
46
+ steps.append(info)
47
+
48
+ hooks: List[Dict[str, Any]] = []
49
+ for hook in pipeline._hooks:
50
+ hook_info: Dict[str, Any] = {
51
+ "class_name": hook.__class__.__name__,
52
+ "type": "hook",
53
+ }
54
+ try:
55
+ hook_info["source"] = inspect.getsource(hook.__class__)
56
+ except (OSError, TypeError):
57
+ hook_info["source"] = None
58
+ hooks.append(hook_info)
59
+
60
+ return payload.insert("steps", steps).insert("hooks", hooks)
@@ -0,0 +1,52 @@
1
+ """
2
+ ClassifyStepsFilter: Maps pipeline steps to architectural roles using config patterns.
3
+ """
4
+
5
+ import fnmatch
6
+ from typing import Any, Dict, List
7
+ from codeupipe import Payload
8
+
9
+
10
+ class ClassifyStepsFilter:
11
+ """
12
+ Filter: Classify pipeline steps into architectural roles.
13
+
14
+ Input payload keys:
15
+ - steps (list[dict]): Step manifest from AnalyzePipelineFilter
16
+ - hooks (list[dict]): Hook manifest from AnalyzePipelineFilter
17
+ - config (dict): Resolved config with 'roles' mapping
18
+
19
+ Output payload adds:
20
+ - classified (dict[str, list[dict]]): role_name → list of steps
21
+ """
22
+
23
+ def call(self, payload):
24
+ steps = payload.get("steps", [])
25
+ hooks = payload.get("hooks", [])
26
+ config = payload.get("config", {})
27
+ roles = config.get("roles", {})
28
+
29
+ classified: Dict[str, List[Dict[str, Any]]] = {}
30
+
31
+ for step in steps:
32
+ role = _match_role(step["name"], step["type"], roles)
33
+ classified.setdefault(role, []).append(step)
34
+
35
+ for hook in hooks:
36
+ role = _match_role(hook["class_name"], "hook", roles)
37
+ classified.setdefault(role, []).append(hook)
38
+
39
+ return payload.insert("classified", classified)
40
+
41
+
42
+ def _match_role(name: str, step_type: str, roles: Dict[str, List[str]]) -> str:
43
+ """Match a step name/type against role glob patterns."""
44
+ for role, patterns in roles.items():
45
+ for pattern in patterns:
46
+ # Type-based special tokens
47
+ if pattern.startswith("_") and pattern == f"_{step_type}":
48
+ return role
49
+ # Name-based glob matching
50
+ if fnmatch.fnmatch(name, pattern):
51
+ return role
52
+ return "uncategorized"
@@ -0,0 +1,61 @@
1
+ """
2
+ ClassifyFilesFilter: Maps scanned project files to CUP roles by directory location.
3
+ """
4
+
5
+ from typing import Any, Dict, List
6
+ from codeupipe import Payload
7
+
8
+
9
+ class ClassifyFilesFilter:
10
+ """
11
+ Filter: Classify scanned files into roles based on their directory.
12
+
13
+ Input payload keys:
14
+ - source_files (list[dict]): Files from ScanProjectFilter
15
+ - config (dict): Config with output dirs mapping
16
+
17
+ Output payload adds:
18
+ - classified_files (dict[str, list[dict]]): role → files
19
+ """
20
+
21
+ def call(self, payload):
22
+ source_files = payload.get("source_files", [])
23
+ config = payload.get("config", {})
24
+ output = config.get("output", {})
25
+
26
+ # Build reverse map: directory → role
27
+ dir_to_role: Dict[str, str] = {}
28
+ for role, directory in output.items():
29
+ if role == "base":
30
+ continue
31
+ # Normalize: strip trailing slash
32
+ normalized = directory.rstrip("/")
33
+ dir_to_role[normalized] = role
34
+
35
+ classified: Dict[str, List[Dict[str, Any]]] = {}
36
+
37
+ for file_info in source_files:
38
+ file_dir = file_info.get("dir", "")
39
+ role = _match_dir_to_role(file_dir, dir_to_role)
40
+ classified.setdefault(role, []).append(file_info)
41
+
42
+ return payload.insert("classified_files", classified)
43
+
44
+
45
+ def _match_dir_to_role(file_dir: str, dir_to_role: Dict[str, str]) -> str:
46
+ """Match a file's directory to the closest role."""
47
+ normalized = file_dir.rstrip("/")
48
+
49
+ # Normalize keys too (strip trailing slashes)
50
+ normalized_map = {k.rstrip("/"): v for k, v in dir_to_role.items()}
51
+
52
+ # Exact match
53
+ if normalized in normalized_map:
54
+ return normalized_map[normalized]
55
+
56
+ # Check if file_dir is under a role directory (must have / separator)
57
+ for directory, role in normalized_map.items():
58
+ if normalized.startswith(directory + "/"):
59
+ return role
60
+
61
+ return "uncategorized"
@@ -0,0 +1,187 @@
1
+ """
2
+ GenerateExportFilter: Generates standard Python code from classified CUP steps.
3
+ """
4
+
5
+ import os
6
+ from typing import Any, Dict, List
7
+ from codeupipe import Payload
8
+
9
+
10
+ class GenerateExportFilter:
11
+ """
12
+ Filter: Generate standard Python files from classified CUP steps.
13
+
14
+ Input payload keys:
15
+ - classified (dict[str, list[dict]]): role → steps
16
+ - config (dict): Config with output dirs
17
+ - steps (list[dict]): Original step order (for orchestrator)
18
+
19
+ Output payload adds:
20
+ - files (list[dict]): {"path": str, "content": str} for each generated file
21
+ """
22
+
23
+ def call(self, payload):
24
+ classified = payload.get("classified", {})
25
+ config = payload.get("config", {})
26
+ steps = payload.get("steps", [])
27
+ hooks = payload.get("hooks", [])
28
+
29
+ output = config.get("output", {})
30
+ base = output.get("base", "src/")
31
+ pattern = config.get("pattern", "flat")
32
+
33
+ files: List[Dict[str, str]] = []
34
+
35
+ # Generate a file for each step
36
+ for role, role_steps in classified.items():
37
+ target_dir = output.get(role, f"{role}/")
38
+ for step in role_steps:
39
+ name = step.get("name", step.get("class_name", "unknown"))
40
+ filename = f"{name}.py"
41
+ filepath = os.path.join(base, target_dir, filename)
42
+ content = _generate_step_file(step)
43
+ files.append({"path": filepath, "content": content})
44
+
45
+ # Generate the orchestrator
46
+ orchestrator = _generate_orchestrator(steps, hooks, classified, config)
47
+ # Orchestrator goes in controller dir (MVC) or root (flat)
48
+ orch_dir = output.get("controller", output.get("step", ""))
49
+ orch_path = os.path.join(base, orch_dir, "pipeline.py")
50
+ files.append({"path": orch_path, "content": orchestrator})
51
+
52
+ return payload.insert("files", files)
53
+
54
+
55
+ def _generate_step_file(step: Dict[str, Any]) -> str:
56
+ """Generate a standard Python file for one step."""
57
+ name = step.get("name", step.get("class_name", "unknown"))
58
+ step_type = step.get("type", "filter")
59
+ class_name = step.get("class_name", "Unknown")
60
+ source = step.get("source")
61
+
62
+ lines = [
63
+ f'"""',
64
+ f"{name} — converted from CUP {class_name}",
65
+ f'"""',
66
+ "",
67
+ ]
68
+
69
+ if step_type in ("filter", "valve"):
70
+ lines.extend([
71
+ "",
72
+ f"def {name}(data: dict) -> dict:",
73
+ f' """Converted from CUP Filter: {class_name}."""',
74
+ ])
75
+ if source:
76
+ lines.append(f" # Original CUP source:")
77
+ for src_line in source.strip().split("\n"):
78
+ lines.append(f" # {src_line}")
79
+ lines.append("")
80
+ lines.extend([
81
+ " # TODO: implement business logic",
82
+ " return data",
83
+ ])
84
+
85
+ # For valves, also emit the predicate
86
+ if step_type == "valve":
87
+ lines.extend([
88
+ "",
89
+ "",
90
+ f"def {name}_predicate(data: dict) -> bool:",
91
+ f' """Predicate for conditional execution of {name}."""',
92
+ " # TODO: implement predicate logic",
93
+ " return True",
94
+ ])
95
+
96
+ elif step_type == "tap":
97
+ lines.extend([
98
+ "",
99
+ f"def {name}(data: dict) -> None:",
100
+ f' """Converted from CUP Tap: {class_name}. Observe only, no modification."""',
101
+ ])
102
+ if source:
103
+ lines.append(f" # Original CUP source:")
104
+ for src_line in source.strip().split("\n"):
105
+ lines.append(f" # {src_line}")
106
+ lines.append("")
107
+ lines.extend([
108
+ " # TODO: implement observation logic (logging, metrics, etc.)",
109
+ " pass",
110
+ ])
111
+
112
+ elif step_type == "hook":
113
+ lines.extend([
114
+ "",
115
+ f"class {class_name}Hook:",
116
+ f' """Converted from CUP Hook: {class_name}."""',
117
+ "",
118
+ " def before(self, step_name, data):",
119
+ ' """Called before each step."""',
120
+ " pass",
121
+ "",
122
+ " def after(self, step_name, data):",
123
+ ' """Called after each step."""',
124
+ " pass",
125
+ "",
126
+ " def on_error(self, step_name, error, data):",
127
+ ' """Called when a step raises."""',
128
+ " pass",
129
+ ])
130
+
131
+ lines.append("")
132
+ return "\n".join(lines)
133
+
134
+
135
+ def _generate_orchestrator(
136
+ steps: List[Dict[str, Any]],
137
+ hooks: List[Dict[str, Any]],
138
+ classified: Dict[str, List[Dict[str, Any]]],
139
+ config: Dict[str, Any],
140
+ ) -> str:
141
+ """Generate the main orchestrator that calls all steps in sequence."""
142
+ output = config.get("output", {})
143
+ base = output.get("base", "src/")
144
+
145
+ lines = [
146
+ '"""',
147
+ "Pipeline orchestrator — calls all steps in sequence.",
148
+ f"Converted from CUP Pipeline. Pattern: {config.get('pattern', 'flat')}",
149
+ '"""',
150
+ "",
151
+ ]
152
+
153
+ # Imports
154
+ for role, role_steps in classified.items():
155
+ target_dir = output.get(role, f"{role}/")
156
+ module_base = target_dir.rstrip("/").replace("/", ".")
157
+ for step in role_steps:
158
+ name = step.get("name", step.get("class_name", "unknown"))
159
+ step_type = step.get("type", "filter")
160
+ if step_type == "hook":
161
+ lines.append(f"from {module_base}.{name} import {step.get('class_name', name)}Hook")
162
+ else:
163
+ fn_name = name
164
+ lines.append(f"from {module_base}.{name} import {fn_name}")
165
+ if step_type == "valve":
166
+ lines.append(f"from {module_base}.{name} import {fn_name}_predicate")
167
+
168
+ lines.extend(["", "", "def run_pipeline(data: dict) -> dict:", ' """Execute all steps in order."""'])
169
+
170
+ # Step calls in original order
171
+ for step in steps:
172
+ name = step["name"]
173
+ step_type = step["type"]
174
+ if step_type == "valve":
175
+ lines.append(f" if {name}_predicate(data):")
176
+ lines.append(f" data = {name}(data)")
177
+ elif step_type == "tap":
178
+ lines.append(f" {name}(data)")
179
+ else:
180
+ lines.append(f" data = {name}(data)")
181
+
182
+ lines.extend([
183
+ " return data",
184
+ "",
185
+ ])
186
+
187
+ return "\n".join(lines)