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.
- codeupipe/__init__.py +39 -0
- codeupipe/cli.py +1502 -0
- codeupipe/converter/__init__.py +9 -0
- codeupipe/converter/config.py +119 -0
- codeupipe/converter/filters/__init__.py +21 -0
- codeupipe/converter/filters/analyze.py +60 -0
- codeupipe/converter/filters/classify.py +52 -0
- codeupipe/converter/filters/classify_files.py +61 -0
- codeupipe/converter/filters/generate_export.py +187 -0
- codeupipe/converter/filters/generate_import.py +229 -0
- codeupipe/converter/filters/parse_config.py +26 -0
- codeupipe/converter/filters/scan_project.py +52 -0
- codeupipe/converter/pipelines/__init__.py +8 -0
- codeupipe/converter/pipelines/export_pipeline.py +40 -0
- codeupipe/converter/pipelines/import_pipeline.py +40 -0
- codeupipe/converter/taps/__init__.py +7 -0
- codeupipe/converter/taps/conversion_log.py +46 -0
- codeupipe/core/__init__.py +20 -0
- codeupipe/core/filter.py +27 -0
- codeupipe/core/hook.py +34 -0
- codeupipe/core/payload.py +94 -0
- codeupipe/core/pipeline.py +231 -0
- codeupipe/core/state.py +78 -0
- codeupipe/core/stream_filter.py +33 -0
- codeupipe/core/tap.py +27 -0
- codeupipe/core/valve.py +52 -0
- codeupipe/linter/__init__.py +57 -0
- codeupipe/linter/assemble_doc_report.py +97 -0
- codeupipe/linter/assemble_report.py +140 -0
- codeupipe/linter/check_bundle.py +47 -0
- codeupipe/linter/check_index.py +88 -0
- codeupipe/linter/check_naming.py +47 -0
- codeupipe/linter/check_protocols.py +73 -0
- codeupipe/linter/check_structure.py +41 -0
- codeupipe/linter/check_symbols.py +116 -0
- codeupipe/linter/check_tests.py +48 -0
- codeupipe/linter/coverage_pipeline.py +39 -0
- codeupipe/linter/detect_drift.py +44 -0
- codeupipe/linter/detect_orphans.py +106 -0
- codeupipe/linter/doc_check_pipeline.py +28 -0
- codeupipe/linter/git_history.py +130 -0
- codeupipe/linter/lint_pipeline.py +51 -0
- codeupipe/linter/map_coverage.py +84 -0
- codeupipe/linter/report_gaps.py +68 -0
- codeupipe/linter/report_pipeline.py +49 -0
- codeupipe/linter/resolve_refs.py +48 -0
- codeupipe/linter/scan_components.py +95 -0
- codeupipe/linter/scan_directory.py +123 -0
- codeupipe/linter/scan_docs.py +62 -0
- codeupipe/linter/scan_tests.py +104 -0
- codeupipe/py.typed +1 -0
- codeupipe/testing.py +344 -0
- codeupipe/utils/__init__.py +10 -0
- codeupipe/utils/error_handling.py +68 -0
- codeupipe-0.1.0.dist-info/METADATA +216 -0
- codeupipe-0.1.0.dist-info/RECORD +60 -0
- codeupipe-0.1.0.dist-info/WHEEL +5 -0
- codeupipe-0.1.0.dist-info/entry_points.txt +2 -0
- codeupipe-0.1.0.dist-info/licenses/LICENSE +190 -0
- 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)
|