kumoai 2.14.0.dev202601011731__cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.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.
Potentially problematic release.
This version of kumoai might be problematic. Click here for more details.
- kumoai/__init__.py +300 -0
- kumoai/_logging.py +29 -0
- kumoai/_singleton.py +25 -0
- kumoai/_version.py +1 -0
- kumoai/artifact_export/__init__.py +9 -0
- kumoai/artifact_export/config.py +209 -0
- kumoai/artifact_export/job.py +108 -0
- kumoai/client/__init__.py +5 -0
- kumoai/client/client.py +223 -0
- kumoai/client/connector.py +110 -0
- kumoai/client/endpoints.py +150 -0
- kumoai/client/graph.py +120 -0
- kumoai/client/jobs.py +471 -0
- kumoai/client/online.py +78 -0
- kumoai/client/pquery.py +207 -0
- kumoai/client/rfm.py +112 -0
- kumoai/client/source_table.py +53 -0
- kumoai/client/table.py +101 -0
- kumoai/client/utils.py +130 -0
- kumoai/codegen/__init__.py +19 -0
- kumoai/codegen/cli.py +100 -0
- kumoai/codegen/context.py +16 -0
- kumoai/codegen/edits.py +473 -0
- kumoai/codegen/exceptions.py +10 -0
- kumoai/codegen/generate.py +222 -0
- kumoai/codegen/handlers/__init__.py +4 -0
- kumoai/codegen/handlers/connector.py +118 -0
- kumoai/codegen/handlers/graph.py +71 -0
- kumoai/codegen/handlers/pquery.py +62 -0
- kumoai/codegen/handlers/table.py +109 -0
- kumoai/codegen/handlers/utils.py +42 -0
- kumoai/codegen/identity.py +114 -0
- kumoai/codegen/loader.py +93 -0
- kumoai/codegen/naming.py +94 -0
- kumoai/codegen/registry.py +121 -0
- kumoai/connector/__init__.py +31 -0
- kumoai/connector/base.py +153 -0
- kumoai/connector/bigquery_connector.py +200 -0
- kumoai/connector/databricks_connector.py +213 -0
- kumoai/connector/file_upload_connector.py +189 -0
- kumoai/connector/glue_connector.py +150 -0
- kumoai/connector/s3_connector.py +278 -0
- kumoai/connector/snowflake_connector.py +252 -0
- kumoai/connector/source_table.py +471 -0
- kumoai/connector/utils.py +1796 -0
- kumoai/databricks.py +14 -0
- kumoai/encoder/__init__.py +4 -0
- kumoai/exceptions.py +26 -0
- kumoai/experimental/__init__.py +0 -0
- kumoai/experimental/rfm/__init__.py +210 -0
- kumoai/experimental/rfm/authenticate.py +432 -0
- kumoai/experimental/rfm/backend/__init__.py +0 -0
- kumoai/experimental/rfm/backend/local/__init__.py +42 -0
- kumoai/experimental/rfm/backend/local/graph_store.py +297 -0
- kumoai/experimental/rfm/backend/local/sampler.py +312 -0
- kumoai/experimental/rfm/backend/local/table.py +113 -0
- kumoai/experimental/rfm/backend/snow/__init__.py +37 -0
- kumoai/experimental/rfm/backend/snow/sampler.py +297 -0
- kumoai/experimental/rfm/backend/snow/table.py +242 -0
- kumoai/experimental/rfm/backend/sqlite/__init__.py +32 -0
- kumoai/experimental/rfm/backend/sqlite/sampler.py +398 -0
- kumoai/experimental/rfm/backend/sqlite/table.py +184 -0
- kumoai/experimental/rfm/base/__init__.py +30 -0
- kumoai/experimental/rfm/base/column.py +152 -0
- kumoai/experimental/rfm/base/expression.py +44 -0
- kumoai/experimental/rfm/base/sampler.py +761 -0
- kumoai/experimental/rfm/base/source.py +19 -0
- kumoai/experimental/rfm/base/sql_sampler.py +143 -0
- kumoai/experimental/rfm/base/table.py +736 -0
- kumoai/experimental/rfm/graph.py +1237 -0
- kumoai/experimental/rfm/infer/__init__.py +19 -0
- kumoai/experimental/rfm/infer/categorical.py +40 -0
- kumoai/experimental/rfm/infer/dtype.py +82 -0
- kumoai/experimental/rfm/infer/id.py +46 -0
- kumoai/experimental/rfm/infer/multicategorical.py +48 -0
- kumoai/experimental/rfm/infer/pkey.py +128 -0
- kumoai/experimental/rfm/infer/stype.py +35 -0
- kumoai/experimental/rfm/infer/time_col.py +61 -0
- kumoai/experimental/rfm/infer/timestamp.py +41 -0
- kumoai/experimental/rfm/pquery/__init__.py +7 -0
- kumoai/experimental/rfm/pquery/executor.py +102 -0
- kumoai/experimental/rfm/pquery/pandas_executor.py +530 -0
- kumoai/experimental/rfm/relbench.py +76 -0
- kumoai/experimental/rfm/rfm.py +1184 -0
- kumoai/experimental/rfm/sagemaker.py +138 -0
- kumoai/experimental/rfm/task_table.py +231 -0
- kumoai/formatting.py +30 -0
- kumoai/futures.py +99 -0
- kumoai/graph/__init__.py +12 -0
- kumoai/graph/column.py +106 -0
- kumoai/graph/graph.py +948 -0
- kumoai/graph/table.py +838 -0
- kumoai/jobs.py +80 -0
- kumoai/kumolib.cpython-310-x86_64-linux-gnu.so +0 -0
- kumoai/mixin.py +28 -0
- kumoai/pquery/__init__.py +25 -0
- kumoai/pquery/prediction_table.py +287 -0
- kumoai/pquery/predictive_query.py +641 -0
- kumoai/pquery/training_table.py +424 -0
- kumoai/spcs.py +121 -0
- kumoai/testing/__init__.py +8 -0
- kumoai/testing/decorators.py +57 -0
- kumoai/testing/snow.py +50 -0
- kumoai/trainer/__init__.py +42 -0
- kumoai/trainer/baseline_trainer.py +93 -0
- kumoai/trainer/config.py +2 -0
- kumoai/trainer/distilled_trainer.py +175 -0
- kumoai/trainer/job.py +1192 -0
- kumoai/trainer/online_serving.py +258 -0
- kumoai/trainer/trainer.py +475 -0
- kumoai/trainer/util.py +103 -0
- kumoai/utils/__init__.py +11 -0
- kumoai/utils/datasets.py +83 -0
- kumoai/utils/display.py +51 -0
- kumoai/utils/forecasting.py +209 -0
- kumoai/utils/progress_logger.py +343 -0
- kumoai/utils/sql.py +3 -0
- kumoai-2.14.0.dev202601011731.dist-info/METADATA +71 -0
- kumoai-2.14.0.dev202601011731.dist-info/RECORD +122 -0
- kumoai-2.14.0.dev202601011731.dist-info/WHEEL +6 -0
- kumoai-2.14.0.dev202601011731.dist-info/licenses/LICENSE +9 -0
- kumoai-2.14.0.dev202601011731.dist-info/top_level.txt +1 -0
kumoai/codegen/cli.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""CLI interface for Kumo SDK code generation utility."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from kumoai.codegen import generate_code
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> None:
|
|
17
|
+
"""CLI entry point for kumo-codegen command."""
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
description="Generate Python SDK code from Kumo entities",
|
|
20
|
+
epilog="""
|
|
21
|
+
Examples:
|
|
22
|
+
kumo-codegen --id myconnector --entity-class S3Connector
|
|
23
|
+
kumo-codegen --id trainingjob-abc123
|
|
24
|
+
kumo-codegen --id myconnector --entity-class S3Connector -o output.py
|
|
25
|
+
kumo-codegen --json config.json -o output.py
|
|
26
|
+
""",
|
|
27
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
input_group = parser.add_mutually_exclusive_group(required=True)
|
|
31
|
+
input_group.add_argument("--id", help="Entity ID to generate code for")
|
|
32
|
+
input_group.add_argument("--json", type=Path,
|
|
33
|
+
help="JSON file with entity specification")
|
|
34
|
+
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--entity-class",
|
|
37
|
+
help="Entity class for ID mode (e.g., S3Connector, TrainingJob)",
|
|
38
|
+
type=str,
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--output",
|
|
42
|
+
"-o",
|
|
43
|
+
type=Path,
|
|
44
|
+
help="Output file path (default: stdout)",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--verbose",
|
|
48
|
+
"-v",
|
|
49
|
+
action="store_true",
|
|
50
|
+
help="Enable verbose output",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
args = parser.parse_args()
|
|
54
|
+
|
|
55
|
+
if args.verbose:
|
|
56
|
+
logger.setLevel(logging.INFO)
|
|
57
|
+
|
|
58
|
+
# Build input_spec based on mode
|
|
59
|
+
if args.id:
|
|
60
|
+
input_spec = {"id": args.id}
|
|
61
|
+
if args.entity_class:
|
|
62
|
+
input_spec["entity_class"] = args.entity_class
|
|
63
|
+
|
|
64
|
+
if args.verbose:
|
|
65
|
+
entity_info = f"ID: {args.id}"
|
|
66
|
+
if args.entity_class:
|
|
67
|
+
entity_info += f", Class: {args.entity_class}"
|
|
68
|
+
logger.info(f"Generating code for {entity_info}")
|
|
69
|
+
|
|
70
|
+
else:
|
|
71
|
+
if args.verbose:
|
|
72
|
+
logger.info(f"Using JSON mode with file: {args.json}")
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
with open(args.json, "r") as f:
|
|
76
|
+
json_data = json.load(f)
|
|
77
|
+
input_spec = {"json": json_data}
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(f"Error reading JSON file {args.json}: {e}")
|
|
80
|
+
sys.exit(1)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
output_path = str(args.output) if args.output else None
|
|
84
|
+
code = generate_code(input_spec, output_path=output_path)
|
|
85
|
+
|
|
86
|
+
if args.verbose and args.output:
|
|
87
|
+
logger.info(f"Code written to {args.output}")
|
|
88
|
+
elif not args.output:
|
|
89
|
+
print(code, end="")
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.error(f"Error: {e}")
|
|
92
|
+
if args.verbose:
|
|
93
|
+
import traceback
|
|
94
|
+
|
|
95
|
+
traceback.print_exc(file=sys.stderr)
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
main()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class CodegenContext:
|
|
7
|
+
"""Context for code generation containing shared state and mappings."""
|
|
8
|
+
|
|
9
|
+
# Maps config IDs to shared parent data
|
|
10
|
+
shared_parents: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
11
|
+
|
|
12
|
+
# Maps config IDs to variable names
|
|
13
|
+
object_to_var: Dict[str, str] = field(default_factory=dict)
|
|
14
|
+
|
|
15
|
+
# Execution environment for generated code
|
|
16
|
+
execution_env: Dict[str, Any] = field(default_factory=dict)
|
kumoai/codegen/edits.py
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, List, NamedTuple, Set
|
|
6
|
+
|
|
7
|
+
from kumoai.codegen.naming import NameManager
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EditResult(NamedTuple):
|
|
13
|
+
edits: List["UniversalReplacementEdit"]
|
|
14
|
+
imports: List[str]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _is_primitive(obj: object) -> bool:
|
|
18
|
+
return obj is None or isinstance(obj, (str, int, float, bool))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _is_collection(obj: object) -> bool:
|
|
22
|
+
return isinstance(obj, (list, dict, set, tuple))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _collect_required_imports(obj: object) -> List[str]:
|
|
26
|
+
if _is_primitive(obj):
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
from kumoai.codegen.handlers.utils import _get_canonical_import_path
|
|
30
|
+
|
|
31
|
+
obj_type = type(obj)
|
|
32
|
+
if hasattr(obj_type, "__module__") and hasattr(obj_type, "__name__"):
|
|
33
|
+
canonical_module = _get_canonical_import_path(obj_type)
|
|
34
|
+
if canonical_module:
|
|
35
|
+
return [f"from {canonical_module} import {obj_type.__name__}"]
|
|
36
|
+
|
|
37
|
+
return []
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_TYPE_DEFAULTS = {
|
|
41
|
+
str: "",
|
|
42
|
+
int: 0,
|
|
43
|
+
float: 0.0,
|
|
44
|
+
bool: False,
|
|
45
|
+
list: [],
|
|
46
|
+
dict: {},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _get_constructor_requirements(obj_type: type) -> dict[str, Any]:
|
|
51
|
+
"""Analyze constructor to determine required
|
|
52
|
+
parameters and their default values.
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
sig = inspect.signature(obj_type.__init__) # type: ignore
|
|
56
|
+
required_params = {}
|
|
57
|
+
|
|
58
|
+
for name, param in sig.parameters.items():
|
|
59
|
+
if name == "self":
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
if param.kind == inspect.Parameter.VAR_POSITIONAL or \
|
|
63
|
+
param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
if param.default is param.empty:
|
|
67
|
+
if param.annotation != param.empty:
|
|
68
|
+
required_params[name] = _TYPE_DEFAULTS.get(
|
|
69
|
+
param.annotation, None)
|
|
70
|
+
else:
|
|
71
|
+
required_params[name] = None
|
|
72
|
+
|
|
73
|
+
return required_params
|
|
74
|
+
|
|
75
|
+
except (ValueError, TypeError):
|
|
76
|
+
return {}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _get_value_repr(value: object) -> str:
|
|
80
|
+
"""Get proper string representation for a value
|
|
81
|
+
, handling enums specially.
|
|
82
|
+
"""
|
|
83
|
+
if hasattr(value, "value") and hasattr(value, "name"):
|
|
84
|
+
try:
|
|
85
|
+
enum_class = type(value)
|
|
86
|
+
string_value = str(value)
|
|
87
|
+
reconstructed = getattr(enum_class, string_value, None)
|
|
88
|
+
if reconstructed == value:
|
|
89
|
+
return repr(string_value)
|
|
90
|
+
except (ValueError, TypeError, AttributeError):
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
enum_class_name = type(value).__name__
|
|
94
|
+
return f"{enum_class_name}('{str(value)}')"
|
|
95
|
+
elif hasattr(value, "__str__") and not _is_primitive(value):
|
|
96
|
+
return repr(str(value))
|
|
97
|
+
else:
|
|
98
|
+
return repr(value)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_editable_attributes(obj: object) -> List[str]:
|
|
102
|
+
"""Extract editable attributes from an object using __dict__."""
|
|
103
|
+
if not hasattr(obj, "__dict__"):
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
editable_attrs = []
|
|
107
|
+
for key, value in obj.__dict__.items():
|
|
108
|
+
if callable(value):
|
|
109
|
+
continue
|
|
110
|
+
if key.startswith("__"):
|
|
111
|
+
continue
|
|
112
|
+
if key.startswith("_"):
|
|
113
|
+
public_key = key[1:]
|
|
114
|
+
if hasattr(obj, public_key):
|
|
115
|
+
editable_attrs.append(key)
|
|
116
|
+
else:
|
|
117
|
+
editable_attrs.append(key)
|
|
118
|
+
|
|
119
|
+
return editable_attrs
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class UniversalReplacementEdit:
|
|
123
|
+
"""Represents a single edit operation for an object's attribute or element.
|
|
124
|
+
|
|
125
|
+
This class generates Python code lines to update
|
|
126
|
+
an object's property, collection element, or assign a new value.
|
|
127
|
+
|
|
128
|
+
It handles primitives, collections, and complex objects,
|
|
129
|
+
producing the necessary assignment
|
|
130
|
+
or construction code to perform the edit programmatically.
|
|
131
|
+
|
|
132
|
+
Example usage:
|
|
133
|
+
# Primitive attribute edit
|
|
134
|
+
nm = NameManager()
|
|
135
|
+
edit = UniversalReplacementEdit("name", "Alice", nm)
|
|
136
|
+
lines = edit.emit_lines("person")
|
|
137
|
+
# lines == ["person.name = 'Alice'"]
|
|
138
|
+
|
|
139
|
+
# Collection attribute edit
|
|
140
|
+
edit = UniversalReplacementEdit("items", [1, 2, 3], nm)
|
|
141
|
+
lines = edit.emit_lines("container")
|
|
142
|
+
# lines == ["items_1 = [1, 2, 3]", "container.items = items_1"]
|
|
143
|
+
|
|
144
|
+
# Complex object attribute edit
|
|
145
|
+
address = Address("123 Main St", "NYC")
|
|
146
|
+
edit = UniversalReplacementEdit("address", address, nm)
|
|
147
|
+
lines = edit.emit_lines("person")
|
|
148
|
+
# lines contains something like:
|
|
149
|
+
# ["address_1 = Address(street='123 Main St', city='NYC')",
|
|
150
|
+
# "person.address = address_1"]
|
|
151
|
+
"""
|
|
152
|
+
def __init__(self, path: str, value: object, name_manager: NameManager):
|
|
153
|
+
if path is None:
|
|
154
|
+
raise TypeError("path cannot be None")
|
|
155
|
+
self.path = path
|
|
156
|
+
self.value = value
|
|
157
|
+
self.temp_var_name = name_manager.assign_temp_variable(path, value)
|
|
158
|
+
self.required_imports = _collect_required_imports(value)
|
|
159
|
+
self.name_manager = name_manager
|
|
160
|
+
|
|
161
|
+
def emit_lines(self, var_name: str) -> List[str]:
|
|
162
|
+
"""Generate Python code lines for this edit.
|
|
163
|
+
|
|
164
|
+
This method handles three cases:
|
|
165
|
+
1. Primitives: Direct assignment (obj.name = "value")
|
|
166
|
+
2. Collections: temp_var = [1, 2, 3]; target = temp_var
|
|
167
|
+
3. Complex objects: Construct object + set properties + assign
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
var_name: The target variable name to assign to
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
List of Python code lines to execute this edit
|
|
174
|
+
|
|
175
|
+
Examples:
|
|
176
|
+
For primitive:
|
|
177
|
+
'obj.name = "new_value"'
|
|
178
|
+
|
|
179
|
+
For object:
|
|
180
|
+
'user_1 = User(id=123)',
|
|
181
|
+
'user_1.active = True',
|
|
182
|
+
'obj.user = user_1'
|
|
183
|
+
"""
|
|
184
|
+
lines = []
|
|
185
|
+
|
|
186
|
+
if _is_primitive(self.value):
|
|
187
|
+
# For primitives, assign directly without temp variable
|
|
188
|
+
value_repr = _get_value_repr(self.value)
|
|
189
|
+
if self.path:
|
|
190
|
+
target = (f"{var_name}{self.path}" if self.path.startswith("[")
|
|
191
|
+
or self.path.startswith(".") else
|
|
192
|
+
f"{var_name}.{self.path}")
|
|
193
|
+
lines.append(f"{target} = {value_repr}")
|
|
194
|
+
else:
|
|
195
|
+
lines.append(f"{var_name} = {value_repr}")
|
|
196
|
+
return lines
|
|
197
|
+
|
|
198
|
+
elif _is_collection(self.value):
|
|
199
|
+
lines.append(f"{self.temp_var_name} = {repr(self.value)}")
|
|
200
|
+
|
|
201
|
+
else:
|
|
202
|
+
# Complex object: construction + property decomposition
|
|
203
|
+
obj_type = type(self.value)
|
|
204
|
+
required_params = _get_constructor_requirements(obj_type)
|
|
205
|
+
# For required parameters, use actual values from the object
|
|
206
|
+
# instead of defaults
|
|
207
|
+
for param_name, param_value in required_params.items():
|
|
208
|
+
if hasattr(self.value, param_name):
|
|
209
|
+
required_params[param_name] = getattr(
|
|
210
|
+
self.value, param_name)
|
|
211
|
+
else:
|
|
212
|
+
required_params[param_name] = param_value
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
if required_params:
|
|
216
|
+
baseline_obj = obj_type(**required_params)
|
|
217
|
+
else:
|
|
218
|
+
baseline_obj = obj_type()
|
|
219
|
+
|
|
220
|
+
property_changes = detect_edits_recursive(
|
|
221
|
+
self.value, baseline_obj, "")
|
|
222
|
+
self.required_imports = self.required_imports + \
|
|
223
|
+
property_changes.imports
|
|
224
|
+
constructor_params = (required_params.copy()
|
|
225
|
+
if required_params else {})
|
|
226
|
+
remaining_edits = []
|
|
227
|
+
|
|
228
|
+
for edit in property_changes.edits:
|
|
229
|
+
remaining_edits.append(edit)
|
|
230
|
+
|
|
231
|
+
if constructor_params:
|
|
232
|
+
params_str = ", ".join(
|
|
233
|
+
f"{name}={_get_value_repr(val)}"
|
|
234
|
+
for name, val in constructor_params.items())
|
|
235
|
+
lines.append(f"{self.temp_var_name} = "
|
|
236
|
+
f"{obj_type.__name__}({params_str})")
|
|
237
|
+
else:
|
|
238
|
+
lines.append(
|
|
239
|
+
f"{self.temp_var_name} = {obj_type.__name__}()")
|
|
240
|
+
|
|
241
|
+
for edit in remaining_edits:
|
|
242
|
+
prop_lines = edit._emit_lines_for_target(
|
|
243
|
+
self.temp_var_name)
|
|
244
|
+
lines.extend(prop_lines)
|
|
245
|
+
|
|
246
|
+
except Exception:
|
|
247
|
+
lines = [f"{self.temp_var_name} = {repr(self.value)}"]
|
|
248
|
+
|
|
249
|
+
# For collections and complex objects, add the final
|
|
250
|
+
# assignment
|
|
251
|
+
if self.path:
|
|
252
|
+
target = (f"{var_name}{self.path}" if self.path.startswith("[") or
|
|
253
|
+
self.path.startswith(".") else f"{var_name}.{self.path}")
|
|
254
|
+
lines.append(f"{target} = {self.temp_var_name}")
|
|
255
|
+
else:
|
|
256
|
+
lines.append(f"{var_name} = {self.temp_var_name}")
|
|
257
|
+
|
|
258
|
+
return lines
|
|
259
|
+
|
|
260
|
+
def _emit_lines_for_target(self, target_var: str) -> List[str]:
|
|
261
|
+
"""Helper method to generate edits targeting a specific variable."""
|
|
262
|
+
lines = []
|
|
263
|
+
|
|
264
|
+
if _is_primitive(self.value):
|
|
265
|
+
if self.path.startswith("[") or self.path.startswith("."):
|
|
266
|
+
full_path = f"{target_var}{self.path}"
|
|
267
|
+
else:
|
|
268
|
+
full_path = (f"{target_var}.{self.path}"
|
|
269
|
+
if self.path else target_var)
|
|
270
|
+
|
|
271
|
+
value_repr = _get_value_repr(self.value)
|
|
272
|
+
lines.append(f"{full_path} = {value_repr}")
|
|
273
|
+
else:
|
|
274
|
+
lines.extend(self.emit_lines(target_var))
|
|
275
|
+
|
|
276
|
+
return lines
|
|
277
|
+
|
|
278
|
+
def __repr__(self) -> str:
|
|
279
|
+
return (f"UniversalReplacementEdit(path={self.path}, "
|
|
280
|
+
f"value_type={type(self.value).__name__})")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def get_element_replacement(path: str, value: object,
|
|
284
|
+
name_manager: NameManager) -> EditResult:
|
|
285
|
+
"""Mode 1: Generate edit to replace any value at
|
|
286
|
+
any path using universal temp pattern.
|
|
287
|
+
"""
|
|
288
|
+
edit = UniversalReplacementEdit(path, value, name_manager)
|
|
289
|
+
return EditResult([edit], edit.required_imports)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def get_collection_element_replacement(target: object, baseline: object,
|
|
293
|
+
path: str, name_manager: NameManager,
|
|
294
|
+
visited: Set[int]) -> EditResult:
|
|
295
|
+
"""Mode 2: Generate edits for changes within collections
|
|
296
|
+
(lists, dicts, sets, tuples).
|
|
297
|
+
"""
|
|
298
|
+
edits = []
|
|
299
|
+
all_imports = []
|
|
300
|
+
|
|
301
|
+
if isinstance(target, list) and isinstance(baseline, list):
|
|
302
|
+
max_len = max(len(target), len(baseline))
|
|
303
|
+
|
|
304
|
+
for i in range(max_len):
|
|
305
|
+
target_item = target[i] if i < len(target) else None
|
|
306
|
+
base_item = baseline[i] if i < len(baseline) else None
|
|
307
|
+
|
|
308
|
+
if target_item != base_item:
|
|
309
|
+
element_path = f"{path}[{i}]"
|
|
310
|
+
result = get_element_replacement(element_path, target_item,
|
|
311
|
+
name_manager)
|
|
312
|
+
edits.extend(result.edits)
|
|
313
|
+
all_imports.extend(result.imports)
|
|
314
|
+
|
|
315
|
+
elif isinstance(target, dict) and isinstance(baseline, dict):
|
|
316
|
+
all_keys = set(target.keys()) | set(baseline.keys())
|
|
317
|
+
|
|
318
|
+
for key in sorted(all_keys):
|
|
319
|
+
target_val = target.get(key)
|
|
320
|
+
base_val = baseline.get(key)
|
|
321
|
+
|
|
322
|
+
if target_val != base_val:
|
|
323
|
+
key_path = f"{path}['{key}']"
|
|
324
|
+
|
|
325
|
+
if (not _is_primitive(target_val)
|
|
326
|
+
and not _is_collection(target_val)
|
|
327
|
+
and target_val is not None):
|
|
328
|
+
result = detect_edits_recursive(target_val, base_val,
|
|
329
|
+
key_path, name_manager,
|
|
330
|
+
visited)
|
|
331
|
+
edits.extend(result.edits)
|
|
332
|
+
all_imports.extend(result.imports)
|
|
333
|
+
elif _is_collection(target_val) and _is_collection(base_val):
|
|
334
|
+
result = get_collection_element_replacement(
|
|
335
|
+
target_val, base_val, key_path, name_manager, visited)
|
|
336
|
+
edits.extend(result.edits)
|
|
337
|
+
all_imports.extend(result.imports)
|
|
338
|
+
else:
|
|
339
|
+
result = get_element_replacement(key_path, target_val,
|
|
340
|
+
name_manager)
|
|
341
|
+
edits.extend(result.edits)
|
|
342
|
+
all_imports.extend(result.imports)
|
|
343
|
+
|
|
344
|
+
elif isinstance(target, (set, tuple)):
|
|
345
|
+
if target != baseline:
|
|
346
|
+
result = get_element_replacement(path, target, name_manager)
|
|
347
|
+
edits.extend(result.edits)
|
|
348
|
+
all_imports.extend(result.imports)
|
|
349
|
+
|
|
350
|
+
return EditResult(edits, list(set(all_imports)))
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def get_property_recursion(target: object, baseline: object, path: str,
|
|
354
|
+
name_manager: NameManager,
|
|
355
|
+
visited: Set[int]) -> EditResult:
|
|
356
|
+
"""Mode 3: Generate edits by recursively detecting
|
|
357
|
+
property-level changes in objects.
|
|
358
|
+
"""
|
|
359
|
+
edits = []
|
|
360
|
+
all_imports = []
|
|
361
|
+
|
|
362
|
+
attrs = get_editable_attributes(target)
|
|
363
|
+
|
|
364
|
+
for attr in sorted(attrs):
|
|
365
|
+
try:
|
|
366
|
+
target_val = getattr(target, attr)
|
|
367
|
+
base_val = getattr(baseline, attr, None)
|
|
368
|
+
|
|
369
|
+
attr_path = f"{path}.{attr}" if path else attr
|
|
370
|
+
|
|
371
|
+
result = detect_edits_recursive(target_val, base_val, attr_path,
|
|
372
|
+
name_manager, visited)
|
|
373
|
+
edits.extend(result.edits)
|
|
374
|
+
all_imports.extend(result.imports)
|
|
375
|
+
|
|
376
|
+
except (AttributeError, RuntimeError, TypeError):
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
return EditResult(edits, list(set(all_imports)))
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _determine_edit_strategy(target: object, baseline: object) -> str:
|
|
383
|
+
"""Determine which edit strategy to use based
|
|
384
|
+
on object types and values.
|
|
385
|
+
"""
|
|
386
|
+
# Type mismatch -> Element replacement
|
|
387
|
+
if not isinstance(target, type(baseline)):
|
|
388
|
+
return "element_replacement"
|
|
389
|
+
|
|
390
|
+
# Equality check -> No edits needed
|
|
391
|
+
if target == baseline:
|
|
392
|
+
return "no_edit"
|
|
393
|
+
|
|
394
|
+
# Primitives -> Element replacement
|
|
395
|
+
if _is_primitive(target):
|
|
396
|
+
return "element_replacement"
|
|
397
|
+
|
|
398
|
+
# Collections -> Collection element replacement
|
|
399
|
+
if _is_collection(target):
|
|
400
|
+
return "collection_element_replacement"
|
|
401
|
+
|
|
402
|
+
# Complex objects -> Property recursion
|
|
403
|
+
return "property_recursion"
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
# Strategy dispatch table: maps strategy names to handler functions
|
|
407
|
+
_EDIT_STRATEGIES = {
|
|
408
|
+
"no_edit":
|
|
409
|
+
lambda target, base, path, nm, visited: EditResult([], []),
|
|
410
|
+
"element_replacement":
|
|
411
|
+
lambda target, base, path, nm, visited:
|
|
412
|
+
(get_element_replacement(path, target, nm)),
|
|
413
|
+
"collection_element_replacement":
|
|
414
|
+
get_collection_element_replacement,
|
|
415
|
+
"property_recursion":
|
|
416
|
+
get_property_recursion,
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def detect_edits_recursive(
|
|
421
|
+
target: object,
|
|
422
|
+
baseline: object,
|
|
423
|
+
path: str = "",
|
|
424
|
+
name_manager: NameManager | None = None,
|
|
425
|
+
visited: Set[int] | None = None,
|
|
426
|
+
) -> EditResult:
|
|
427
|
+
"""Generate edits to transform baseline
|
|
428
|
+
into target using recursive analysis.
|
|
429
|
+
|
|
430
|
+
This is the main entry point for edit detection.
|
|
431
|
+
It analyzes two objects and
|
|
432
|
+
determines what changes are needed, then routes
|
|
433
|
+
to the appropriate mode:
|
|
434
|
+
|
|
435
|
+
Decision:
|
|
436
|
+
1. Type mismatch or primitives -> Element replacement mode
|
|
437
|
+
2. Collections -> Collection element replacement mode
|
|
438
|
+
3. Complex objects -> Property recursion mode
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
target: Target object state to achieve
|
|
442
|
+
baseline: Starting object state
|
|
443
|
+
path: Current path in object hierarchy (e.g., "user.profile.name")
|
|
444
|
+
name_manager: The NameManager instance for variable naming.
|
|
445
|
+
visited: Set of visited object IDs for cycle detection
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
EditResult containing list of edits and required imports
|
|
449
|
+
|
|
450
|
+
Examples:
|
|
451
|
+
Simple change: obj.name = "new" ->
|
|
452
|
+
[UniversalReplacementEdit("name", "new")]
|
|
453
|
+
Nested change: obj.user.active = True ->
|
|
454
|
+
[UniversalReplacementEdit("user.active", True)]
|
|
455
|
+
Collection: obj.items[0] = val ->
|
|
456
|
+
[UniversalReplacementEdit("items[0]", val)]
|
|
457
|
+
"""
|
|
458
|
+
if visited is None:
|
|
459
|
+
visited = set()
|
|
460
|
+
|
|
461
|
+
if name_manager is None:
|
|
462
|
+
name_manager = NameManager()
|
|
463
|
+
|
|
464
|
+
target_id, base_id = id(target), id(baseline)
|
|
465
|
+
if target_id in visited:
|
|
466
|
+
return EditResult([], [])
|
|
467
|
+
|
|
468
|
+
visited_extended = visited | {target_id, base_id}
|
|
469
|
+
|
|
470
|
+
strategy = _determine_edit_strategy(target, baseline)
|
|
471
|
+
handler = _EDIT_STRATEGIES[strategy]
|
|
472
|
+
|
|
473
|
+
return handler(target, baseline, path, name_manager, visited_extended)
|