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,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,8 @@
1
+ """
2
+ Converter Pipelines — CUP Pipelines for bidirectional conversion.
3
+ """
4
+
5
+ from .export_pipeline import build_export_pipeline
6
+ from .import_pipeline import build_import_pipeline
7
+
8
+ __all__ = ["build_export_pipeline", "build_import_pipeline"]
@@ -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,7 @@
1
+ """
2
+ Converter Taps — observation points for conversion pipelines.
3
+ """
4
+
5
+ from .conversion_log import ConversionLogTap
6
+
7
+ __all__ = ["ConversionLogTap"]
@@ -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
+ ]
@@ -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})"