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,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GenerateImportFilter: Generates CUP code from standard Python project files.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
from codeupipe import Payload
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GenerateImportFilter:
|
|
11
|
+
"""
|
|
12
|
+
Filter: Generate CUP Filter/Tap/Valve/Pipeline code from standard Python files.
|
|
13
|
+
|
|
14
|
+
Input payload keys:
|
|
15
|
+
- source_files (list[dict]): Files from ScanProjectFilter
|
|
16
|
+
- config (dict): Config with output dirs and roles
|
|
17
|
+
- classified_files (dict[str, list[dict]]): role → files (from ClassifyFilesFilter)
|
|
18
|
+
|
|
19
|
+
Output payload adds:
|
|
20
|
+
- cup_files (list[dict]): {"path": str, "content": str} for generated CUP code
|
|
21
|
+
- cup_pipeline (str): Generated Pipeline composition code
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def call(self, payload):
|
|
25
|
+
classified_files = payload.get("classified_files", {})
|
|
26
|
+
config = payload.get("config", {})
|
|
27
|
+
|
|
28
|
+
cup_files: List[Dict[str, str]] = []
|
|
29
|
+
pipeline_steps: List[Dict[str, str]] = []
|
|
30
|
+
|
|
31
|
+
for role, files in classified_files.items():
|
|
32
|
+
cup_type = _role_to_cup_type(role, config.get("pattern", "flat"))
|
|
33
|
+
|
|
34
|
+
for file_info in files:
|
|
35
|
+
name = file_info["name"]
|
|
36
|
+
functions = _extract_functions(file_info["content"])
|
|
37
|
+
|
|
38
|
+
if not functions:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
for fn_name, fn_sig, fn_body, returns_value in functions:
|
|
42
|
+
if cup_type == "tap" or not returns_value:
|
|
43
|
+
cup_code = _generate_tap_class(fn_name, fn_body)
|
|
44
|
+
pipeline_steps.append({"name": fn_name, "type": "tap"})
|
|
45
|
+
else:
|
|
46
|
+
cup_code = _generate_filter_class(fn_name, fn_body)
|
|
47
|
+
pipeline_steps.append({"name": fn_name, "type": "filter"})
|
|
48
|
+
|
|
49
|
+
cup_files.append({
|
|
50
|
+
"path": f"filters/{fn_name}.py",
|
|
51
|
+
"content": cup_code,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
# Generate pipeline composition
|
|
55
|
+
pipeline_code = _generate_pipeline(pipeline_steps)
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
payload
|
|
59
|
+
.insert("cup_files", cup_files)
|
|
60
|
+
.insert("cup_pipeline", pipeline_code)
|
|
61
|
+
.insert("cup_steps", pipeline_steps)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _role_to_cup_type(role: str, pattern: str) -> str:
|
|
66
|
+
"""Map an architectural role back to a CUP type."""
|
|
67
|
+
tap_roles = {"middleware", "observability", "infrastructure", "framework"}
|
|
68
|
+
if role in tap_roles:
|
|
69
|
+
return "tap"
|
|
70
|
+
return "filter"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _extract_functions(source: str) -> List[tuple]:
|
|
74
|
+
"""
|
|
75
|
+
Extract top-level function definitions from Python source.
|
|
76
|
+
|
|
77
|
+
Returns list of (name, signature, body, returns_value).
|
|
78
|
+
"""
|
|
79
|
+
functions = []
|
|
80
|
+
# Match def function_name(args): with body
|
|
81
|
+
pattern = re.compile(
|
|
82
|
+
r'^def\s+(\w+)\s*\(([^)]*)\)\s*(?:->\s*[\w\[\], ]+)?\s*:',
|
|
83
|
+
re.MULTILINE,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
for match in pattern.finditer(source):
|
|
87
|
+
fn_name = match.group(1)
|
|
88
|
+
fn_sig = match.group(2)
|
|
89
|
+
start = match.end()
|
|
90
|
+
|
|
91
|
+
# Find the body (indented block after the def)
|
|
92
|
+
body_lines = []
|
|
93
|
+
for line in source[start:].split("\n"):
|
|
94
|
+
if line.strip() == "":
|
|
95
|
+
body_lines.append("")
|
|
96
|
+
continue
|
|
97
|
+
if line and not line[0].isspace():
|
|
98
|
+
break
|
|
99
|
+
body_lines.append(line)
|
|
100
|
+
|
|
101
|
+
# Strip leading/trailing empty lines
|
|
102
|
+
while body_lines and not body_lines[0].strip():
|
|
103
|
+
body_lines.pop(0)
|
|
104
|
+
while body_lines and not body_lines[-1].strip():
|
|
105
|
+
body_lines.pop()
|
|
106
|
+
|
|
107
|
+
body = "\n".join(body_lines)
|
|
108
|
+
returns_value = "return " in body and ("return data" in body.lower() or "return {" in body)
|
|
109
|
+
|
|
110
|
+
functions.append((fn_name, fn_sig, body, returns_value))
|
|
111
|
+
|
|
112
|
+
return functions
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _generate_filter_class(name: str, body: str) -> str:
|
|
116
|
+
"""Generate a CUP Filter class wrapping a standard function."""
|
|
117
|
+
class_name = "".join(w.capitalize() for w in name.split("_")) + "Filter"
|
|
118
|
+
indented_body = _indent_body(body)
|
|
119
|
+
|
|
120
|
+
return f'''"""
|
|
121
|
+
{class_name}: Imported from standard Python function '{name}'.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
from codeupipe import Payload
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class {class_name}:
|
|
128
|
+
"""Filter wrapping the '{name}' function."""
|
|
129
|
+
|
|
130
|
+
def call(self, payload):
|
|
131
|
+
data = payload.to_dict()
|
|
132
|
+
{indented_body}
|
|
133
|
+
return Payload(data)
|
|
134
|
+
'''
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _generate_tap_class(name: str, body: str) -> str:
|
|
138
|
+
"""Generate a CUP Tap class wrapping a standard observation function."""
|
|
139
|
+
class_name = "".join(w.capitalize() for w in name.split("_")) + "Tap"
|
|
140
|
+
indented_body = _indent_body(body)
|
|
141
|
+
|
|
142
|
+
return f'''"""
|
|
143
|
+
{class_name}: Imported from standard Python function '{name}'.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
from codeupipe import Payload
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class {class_name}:
|
|
150
|
+
"""Tap wrapping the '{name}' function."""
|
|
151
|
+
|
|
152
|
+
def observe(self, payload):
|
|
153
|
+
data = payload.to_dict()
|
|
154
|
+
{indented_body}
|
|
155
|
+
'''
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _generate_pipeline(steps: List[Dict[str, str]]) -> str:
|
|
159
|
+
"""Generate Pipeline composition code."""
|
|
160
|
+
lines = [
|
|
161
|
+
'"""',
|
|
162
|
+
"Pipeline: Auto-generated from standard Python project import.",
|
|
163
|
+
'"""',
|
|
164
|
+
"",
|
|
165
|
+
"from codeupipe import Pipeline, Payload",
|
|
166
|
+
"",
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
# Import each filter/tap
|
|
170
|
+
for step in steps:
|
|
171
|
+
name = step["name"]
|
|
172
|
+
class_name = "".join(w.capitalize() for w in name.split("_"))
|
|
173
|
+
if step["type"] == "tap":
|
|
174
|
+
class_name += "Tap"
|
|
175
|
+
lines.append(f"from filters.{name} import {class_name}")
|
|
176
|
+
else:
|
|
177
|
+
class_name += "Filter"
|
|
178
|
+
lines.append(f"from filters.{name} import {class_name}")
|
|
179
|
+
|
|
180
|
+
lines.extend([
|
|
181
|
+
"",
|
|
182
|
+
"",
|
|
183
|
+
"def build_pipeline() -> Pipeline:",
|
|
184
|
+
' """Build the pipeline from imported components."""',
|
|
185
|
+
" pipeline = Pipeline()",
|
|
186
|
+
])
|
|
187
|
+
|
|
188
|
+
for step in steps:
|
|
189
|
+
name = step["name"]
|
|
190
|
+
class_name = "".join(w.capitalize() for w in name.split("_"))
|
|
191
|
+
if step["type"] == "tap":
|
|
192
|
+
class_name += "Tap"
|
|
193
|
+
lines.append(f' pipeline.add_tap({class_name}(), name="{name}")')
|
|
194
|
+
else:
|
|
195
|
+
class_name += "Filter"
|
|
196
|
+
lines.append(f' pipeline.add_filter({class_name}(), name="{name}")')
|
|
197
|
+
|
|
198
|
+
lines.extend([
|
|
199
|
+
" return pipeline",
|
|
200
|
+
"",
|
|
201
|
+
])
|
|
202
|
+
|
|
203
|
+
return "\n".join(lines)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _indent_body(body: str) -> str:
|
|
207
|
+
"""Indent body to 8 spaces (inside a class method), preserving relative indentation."""
|
|
208
|
+
lines = body.split("\n")
|
|
209
|
+
|
|
210
|
+
# Find the minimum indentation of non-empty lines
|
|
211
|
+
min_indent = float("inf")
|
|
212
|
+
for line in lines:
|
|
213
|
+
stripped = line.rstrip()
|
|
214
|
+
if stripped:
|
|
215
|
+
leading = len(line) - len(line.lstrip())
|
|
216
|
+
min_indent = min(min_indent, leading)
|
|
217
|
+
if min_indent == float("inf"):
|
|
218
|
+
min_indent = 0
|
|
219
|
+
|
|
220
|
+
result = []
|
|
221
|
+
for line in lines:
|
|
222
|
+
stripped = line.rstrip()
|
|
223
|
+
if stripped:
|
|
224
|
+
# Preserve relative indent: strip min_indent, add 8 spaces
|
|
225
|
+
relative = line[min_indent:] if len(line) > min_indent else line.lstrip()
|
|
226
|
+
result.append(f" {relative}")
|
|
227
|
+
else:
|
|
228
|
+
result.append("")
|
|
229
|
+
return "\n".join(result)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ParseConfigFilter: Reads conversion config from file or applies defaults.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from codeupipe import Payload
|
|
7
|
+
from codeupipe.converter.config import load_config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ParseConfigFilter:
|
|
11
|
+
"""
|
|
12
|
+
Filter: Parse a .cup.json config or apply pattern defaults.
|
|
13
|
+
|
|
14
|
+
Input payload keys:
|
|
15
|
+
- config_path (str, optional): Path to .cup.json
|
|
16
|
+
- pattern (str, optional): Pattern name fallback (mvc, clean, hexagonal, flat)
|
|
17
|
+
|
|
18
|
+
Output payload adds:
|
|
19
|
+
- config (dict): Resolved configuration
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def call(self, payload):
|
|
23
|
+
config_path = payload.get("config_path")
|
|
24
|
+
pattern = payload.get("pattern")
|
|
25
|
+
config = load_config(config_path=config_path, pattern=pattern)
|
|
26
|
+
return payload.insert("config", config)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ScanProjectFilter: Scans a standard Python project directory for source files.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
from codeupipe import Payload
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ScanProjectFilter:
|
|
11
|
+
"""
|
|
12
|
+
Filter: Scan a project directory and collect Python source files.
|
|
13
|
+
|
|
14
|
+
Input payload keys:
|
|
15
|
+
- project_path (str): Root directory to scan
|
|
16
|
+
- config (dict): Config with output dirs (used to determine role by location)
|
|
17
|
+
|
|
18
|
+
Output payload adds:
|
|
19
|
+
- source_files (list[dict]): {"path": str, "relative": str, "content": str, "dir": str}
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def call(self, payload):
|
|
23
|
+
project_path = payload.get("project_path")
|
|
24
|
+
if not project_path:
|
|
25
|
+
raise ValueError("Payload must contain 'project_path' key")
|
|
26
|
+
|
|
27
|
+
root = Path(project_path)
|
|
28
|
+
if not root.is_dir():
|
|
29
|
+
raise ValueError(f"Not a directory: {project_path}")
|
|
30
|
+
|
|
31
|
+
source_files: List[Dict[str, Any]] = []
|
|
32
|
+
|
|
33
|
+
for py_file in sorted(root.rglob("*.py")):
|
|
34
|
+
# Skip __pycache__, __init__.py, and hidden dirs
|
|
35
|
+
parts = py_file.relative_to(root).parts
|
|
36
|
+
if any(p.startswith(".") or p == "__pycache__" for p in parts):
|
|
37
|
+
continue
|
|
38
|
+
if py_file.name == "__init__.py":
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
relative = str(py_file.relative_to(root))
|
|
42
|
+
parent_dir = str(py_file.parent.relative_to(root)) if py_file.parent != root else ""
|
|
43
|
+
|
|
44
|
+
source_files.append({
|
|
45
|
+
"path": str(py_file),
|
|
46
|
+
"relative": relative,
|
|
47
|
+
"content": py_file.read_text(encoding="utf-8"),
|
|
48
|
+
"dir": parent_dir,
|
|
49
|
+
"name": py_file.stem,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return payload.insert("source_files", source_files)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Export Pipeline: CUP → Standard Python
|
|
3
|
+
|
|
4
|
+
Analyzes a CUP Pipeline, classifies its steps by architectural role,
|
|
5
|
+
and generates standard Python files in the target pattern.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from codeupipe import Pipeline
|
|
9
|
+
from codeupipe.converter.filters.parse_config import ParseConfigFilter
|
|
10
|
+
from codeupipe.converter.filters.analyze import AnalyzePipelineFilter
|
|
11
|
+
from codeupipe.converter.filters.classify import ClassifyStepsFilter
|
|
12
|
+
from codeupipe.converter.filters.generate_export import GenerateExportFilter
|
|
13
|
+
from codeupipe.converter.taps.conversion_log import ConversionLogTap
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_export_pipeline(log_tap: ConversionLogTap = None) -> Pipeline:
|
|
17
|
+
"""
|
|
18
|
+
Build the CUP → Standard export pipeline.
|
|
19
|
+
|
|
20
|
+
Steps:
|
|
21
|
+
1. ParseConfig — load .cup.json or pattern defaults
|
|
22
|
+
2. AnalyzePipeline — introspect the pipeline instance
|
|
23
|
+
3. ClassifySteps — assign steps to architectural roles
|
|
24
|
+
4. GenerateExport — produce standard Python files
|
|
25
|
+
|
|
26
|
+
Returns a Pipeline ready to .run() with a Payload containing:
|
|
27
|
+
- pipeline: The CUP Pipeline instance to export
|
|
28
|
+
- config_path (optional): Path to .cup.json
|
|
29
|
+
- pattern (optional): Pattern name fallback
|
|
30
|
+
"""
|
|
31
|
+
export = Pipeline()
|
|
32
|
+
export.add_filter(ParseConfigFilter(), name="parse_config")
|
|
33
|
+
export.add_filter(AnalyzePipelineFilter(), name="analyze_pipeline")
|
|
34
|
+
export.add_filter(ClassifyStepsFilter(), name="classify_steps")
|
|
35
|
+
export.add_filter(GenerateExportFilter(), name="generate_export")
|
|
36
|
+
|
|
37
|
+
if log_tap:
|
|
38
|
+
export.add_tap(log_tap, name="conversion_log")
|
|
39
|
+
|
|
40
|
+
return export
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Import Pipeline: Standard Python → CUP
|
|
3
|
+
|
|
4
|
+
Scans a standard Python project, classifies files by directory/role,
|
|
5
|
+
and generates CUP Filter/Tap classes plus Pipeline composition code.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from codeupipe import Pipeline
|
|
9
|
+
from codeupipe.converter.filters.parse_config import ParseConfigFilter
|
|
10
|
+
from codeupipe.converter.filters.scan_project import ScanProjectFilter
|
|
11
|
+
from codeupipe.converter.filters.classify_files import ClassifyFilesFilter
|
|
12
|
+
from codeupipe.converter.filters.generate_import import GenerateImportFilter
|
|
13
|
+
from codeupipe.converter.taps.conversion_log import ConversionLogTap
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_import_pipeline(log_tap: ConversionLogTap = None) -> Pipeline:
|
|
17
|
+
"""
|
|
18
|
+
Build the Standard → CUP import pipeline.
|
|
19
|
+
|
|
20
|
+
Steps:
|
|
21
|
+
1. ParseConfig — load .cup.json or pattern defaults
|
|
22
|
+
2. ScanProject — find Python files in the project
|
|
23
|
+
3. ClassifyFiles — map files to roles by directory
|
|
24
|
+
4. GenerateImport — produce CUP Filter/Tap/Pipeline code
|
|
25
|
+
|
|
26
|
+
Returns a Pipeline ready to .run() with a Payload containing:
|
|
27
|
+
- project_path: Root directory of the standard Python project
|
|
28
|
+
- config_path (optional): Path to .cup.json
|
|
29
|
+
- pattern (optional): Pattern name fallback
|
|
30
|
+
"""
|
|
31
|
+
imp = Pipeline()
|
|
32
|
+
imp.add_filter(ParseConfigFilter(), name="parse_config")
|
|
33
|
+
imp.add_filter(ScanProjectFilter(), name="scan_project")
|
|
34
|
+
imp.add_filter(ClassifyFilesFilter(), name="classify_files")
|
|
35
|
+
imp.add_filter(GenerateImportFilter(), name="generate_import")
|
|
36
|
+
|
|
37
|
+
if log_tap:
|
|
38
|
+
imp.add_tap(log_tap, name="conversion_log")
|
|
39
|
+
|
|
40
|
+
return imp
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ConversionLogTap: Logs conversion progress without modifying the payload.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConversionLogTap:
|
|
9
|
+
"""
|
|
10
|
+
Tap: Logs the current conversion state for observability.
|
|
11
|
+
|
|
12
|
+
Captures log entries into an internal list for inspection.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.entries: List[str] = []
|
|
17
|
+
|
|
18
|
+
def observe(self, payload):
|
|
19
|
+
config = payload.get("config")
|
|
20
|
+
steps = payload.get("steps")
|
|
21
|
+
classified = payload.get("classified")
|
|
22
|
+
classified_files = payload.get("classified_files")
|
|
23
|
+
files = payload.get("files")
|
|
24
|
+
cup_files = payload.get("cup_files")
|
|
25
|
+
|
|
26
|
+
if config is not None and not steps and not classified:
|
|
27
|
+
pattern = config.get("pattern", "?")
|
|
28
|
+
self.entries.append(f"Config loaded: pattern={pattern}")
|
|
29
|
+
|
|
30
|
+
if steps and not classified:
|
|
31
|
+
self.entries.append(f"Analyzed: {len(steps)} steps")
|
|
32
|
+
|
|
33
|
+
if classified:
|
|
34
|
+
roles = list(classified.keys())
|
|
35
|
+
self.entries.append(f"Classified into roles: {roles}")
|
|
36
|
+
|
|
37
|
+
if classified_files:
|
|
38
|
+
roles = list(classified_files.keys())
|
|
39
|
+
total = sum(len(v) for v in classified_files.values())
|
|
40
|
+
self.entries.append(f"Scanned {total} files into roles: {roles}")
|
|
41
|
+
|
|
42
|
+
if files:
|
|
43
|
+
self.entries.append(f"Generated {len(files)} export files")
|
|
44
|
+
|
|
45
|
+
if cup_files:
|
|
46
|
+
self.entries.append(f"Generated {len(cup_files)} CUP files")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core Module: Base Protocols and Classes
|
|
3
|
+
|
|
4
|
+
The foundation — protocols, abstract base classes, and fundamental types.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .payload import Payload, MutablePayload
|
|
8
|
+
from .filter import Filter
|
|
9
|
+
from .stream_filter import StreamFilter
|
|
10
|
+
from .pipeline import Pipeline
|
|
11
|
+
from .valve import Valve
|
|
12
|
+
from .tap import Tap
|
|
13
|
+
from .state import State
|
|
14
|
+
from .hook import Hook
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Payload", "MutablePayload",
|
|
18
|
+
"Filter", "StreamFilter", "Pipeline", "Valve", "Tap",
|
|
19
|
+
"State", "Hook",
|
|
20
|
+
]
|
codeupipe/core/filter.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filter Protocol: The Processing Unit
|
|
3
|
+
|
|
4
|
+
The Filter protocol defines the interface for payload processors.
|
|
5
|
+
Each Filter takes a Payload in, processes it, and returns a (potentially transformed) Payload out.
|
|
6
|
+
Enhanced with generic typing for type-safe workflows.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Protocol, TypeVar
|
|
10
|
+
from .payload import Payload
|
|
11
|
+
|
|
12
|
+
__all__ = ["Filter"]
|
|
13
|
+
|
|
14
|
+
TInput = TypeVar('TInput')
|
|
15
|
+
TOutput = TypeVar('TOutput')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Filter(Protocol[TInput, TOutput]):
|
|
19
|
+
"""
|
|
20
|
+
Processing unit — takes a payload in, returns a transformed payload out.
|
|
21
|
+
The core protocol that all filter implementations must follow.
|
|
22
|
+
Enhanced with generic typing for type-safe workflows.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
async def call(self, payload: Payload[TInput]) -> Payload[TOutput]:
|
|
26
|
+
"""Process the payload and return a transformed result."""
|
|
27
|
+
...
|
codeupipe/core/hook.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hook ABC: The Enhancement Layer
|
|
3
|
+
|
|
4
|
+
The Hook ABC defines optional lifecycle hooks for pipeline execution.
|
|
5
|
+
Subclasses can override any combination of before(), after(), and on_error().
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC
|
|
9
|
+
from typing import Optional, TypeVar
|
|
10
|
+
from .payload import Payload
|
|
11
|
+
from .filter import Filter
|
|
12
|
+
|
|
13
|
+
__all__ = ["Hook"]
|
|
14
|
+
|
|
15
|
+
T = TypeVar('T')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Hook(ABC):
|
|
19
|
+
"""
|
|
20
|
+
Lifecycle hook for pipeline execution.
|
|
21
|
+
Subclasses can override any combination of before(), after(), and on_error().
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
async def before(self, filter: Optional[Filter], payload: Payload[T]) -> None:
|
|
25
|
+
"""Called before a filter executes, or before the pipeline starts (filter=None)."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
async def after(self, filter: Optional[Filter], payload: Payload[T]) -> None:
|
|
29
|
+
"""Called after a filter executes, or after the pipeline ends (filter=None)."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
async def on_error(self, filter: Optional[Filter], error: Exception, payload: Payload[T]) -> None:
|
|
33
|
+
"""Called when an error occurs."""
|
|
34
|
+
pass
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Payload: The Data Container
|
|
3
|
+
|
|
4
|
+
The Payload carries data through the pipeline — immutable by default for safety,
|
|
5
|
+
mutable when flexibility is needed.
|
|
6
|
+
Enhanced with generic typing for type-safe workflows.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, Optional, TypeVar, Generic, Union
|
|
10
|
+
|
|
11
|
+
__all__ = ["Payload", "MutablePayload"]
|
|
12
|
+
|
|
13
|
+
T = TypeVar('T')
|
|
14
|
+
TInput = TypeVar('TInput')
|
|
15
|
+
TOutput = TypeVar('TOutput')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Payload(Generic[T]):
|
|
19
|
+
"""
|
|
20
|
+
Immutable data container — holds data flowing through the pipeline.
|
|
21
|
+
Returns fresh copies on modification for safety.
|
|
22
|
+
Enhanced with generic typing for type-safe workflows.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, data: Optional[Union[Dict[str, Any], T]] = None):
|
|
26
|
+
if data is None:
|
|
27
|
+
self._data: Dict[str, Any] = {}
|
|
28
|
+
elif isinstance(data, dict):
|
|
29
|
+
self._data = data.copy() if data else {}
|
|
30
|
+
else:
|
|
31
|
+
try:
|
|
32
|
+
self._data = dict(data) # type: ignore
|
|
33
|
+
except (TypeError, ValueError):
|
|
34
|
+
self._data = {}
|
|
35
|
+
|
|
36
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
37
|
+
"""Return the value for key, or default if absent."""
|
|
38
|
+
return self._data.get(key, default)
|
|
39
|
+
|
|
40
|
+
def insert(self, key: str, value: Any) -> 'Payload[T]':
|
|
41
|
+
"""Return a fresh Payload with the addition."""
|
|
42
|
+
new_data = self._data.copy()
|
|
43
|
+
new_data[key] = value
|
|
44
|
+
return Payload[T](new_data)
|
|
45
|
+
|
|
46
|
+
def insert_as(self, key: str, value: Any) -> 'Payload[T]':
|
|
47
|
+
"""
|
|
48
|
+
Create a new Payload with type evolution — allows clean transformation
|
|
49
|
+
between TypedDict shapes without explicit casting.
|
|
50
|
+
"""
|
|
51
|
+
new_data = self._data.copy()
|
|
52
|
+
new_data[key] = value
|
|
53
|
+
return Payload[T](new_data)
|
|
54
|
+
|
|
55
|
+
def with_mutation(self) -> 'MutablePayload[T]':
|
|
56
|
+
"""Convert to a mutable sibling for performance-critical sections."""
|
|
57
|
+
return MutablePayload[T](self._data.copy())
|
|
58
|
+
|
|
59
|
+
def merge(self, other: 'Payload[T]') -> 'Payload[T]':
|
|
60
|
+
"""Combine payloads, with other taking precedence on conflicts."""
|
|
61
|
+
new_data = self._data.copy()
|
|
62
|
+
new_data.update(other._data)
|
|
63
|
+
return Payload[T](new_data)
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
66
|
+
"""Express as dict for ecosystem integration."""
|
|
67
|
+
return self._data.copy()
|
|
68
|
+
|
|
69
|
+
def __repr__(self) -> str:
|
|
70
|
+
return f"Payload({self._data})"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class MutablePayload(Generic[T]):
|
|
74
|
+
"""
|
|
75
|
+
Mutable data container for performance-critical sections.
|
|
76
|
+
Enhanced with generic typing for type-safe workflows.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self, data: Optional[Dict[str, Any]] = None):
|
|
80
|
+
self._data = data or {}
|
|
81
|
+
|
|
82
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
83
|
+
return self._data.get(key, default)
|
|
84
|
+
|
|
85
|
+
def set(self, key: str, value: Any) -> None:
|
|
86
|
+
"""Change in place."""
|
|
87
|
+
self._data[key] = value
|
|
88
|
+
|
|
89
|
+
def to_immutable(self) -> Payload[T]:
|
|
90
|
+
"""Return to safety with a fresh immutable copy."""
|
|
91
|
+
return Payload[T](self._data.copy())
|
|
92
|
+
|
|
93
|
+
def __repr__(self) -> str:
|
|
94
|
+
return f"MutablePayload({self._data})"
|