codeshift 0.2.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 (65) hide show
  1. codeshift/__init__.py +8 -0
  2. codeshift/analyzer/__init__.py +5 -0
  3. codeshift/analyzer/risk_assessor.py +388 -0
  4. codeshift/api/__init__.py +1 -0
  5. codeshift/api/auth.py +182 -0
  6. codeshift/api/config.py +73 -0
  7. codeshift/api/database.py +215 -0
  8. codeshift/api/main.py +103 -0
  9. codeshift/api/models/__init__.py +55 -0
  10. codeshift/api/models/auth.py +108 -0
  11. codeshift/api/models/billing.py +92 -0
  12. codeshift/api/models/migrate.py +42 -0
  13. codeshift/api/models/usage.py +116 -0
  14. codeshift/api/routers/__init__.py +5 -0
  15. codeshift/api/routers/auth.py +440 -0
  16. codeshift/api/routers/billing.py +395 -0
  17. codeshift/api/routers/migrate.py +304 -0
  18. codeshift/api/routers/usage.py +291 -0
  19. codeshift/api/routers/webhooks.py +289 -0
  20. codeshift/cli/__init__.py +5 -0
  21. codeshift/cli/commands/__init__.py +7 -0
  22. codeshift/cli/commands/apply.py +352 -0
  23. codeshift/cli/commands/auth.py +842 -0
  24. codeshift/cli/commands/diff.py +221 -0
  25. codeshift/cli/commands/scan.py +368 -0
  26. codeshift/cli/commands/upgrade.py +436 -0
  27. codeshift/cli/commands/upgrade_all.py +518 -0
  28. codeshift/cli/main.py +221 -0
  29. codeshift/cli/quota.py +210 -0
  30. codeshift/knowledge/__init__.py +50 -0
  31. codeshift/knowledge/cache.py +167 -0
  32. codeshift/knowledge/generator.py +231 -0
  33. codeshift/knowledge/models.py +151 -0
  34. codeshift/knowledge/parser.py +270 -0
  35. codeshift/knowledge/sources.py +388 -0
  36. codeshift/knowledge_base/__init__.py +17 -0
  37. codeshift/knowledge_base/loader.py +102 -0
  38. codeshift/knowledge_base/models.py +110 -0
  39. codeshift/migrator/__init__.py +23 -0
  40. codeshift/migrator/ast_transforms.py +256 -0
  41. codeshift/migrator/engine.py +395 -0
  42. codeshift/migrator/llm_migrator.py +320 -0
  43. codeshift/migrator/transforms/__init__.py +19 -0
  44. codeshift/migrator/transforms/fastapi_transformer.py +174 -0
  45. codeshift/migrator/transforms/pandas_transformer.py +236 -0
  46. codeshift/migrator/transforms/pydantic_v1_to_v2.py +637 -0
  47. codeshift/migrator/transforms/requests_transformer.py +218 -0
  48. codeshift/migrator/transforms/sqlalchemy_transformer.py +175 -0
  49. codeshift/scanner/__init__.py +6 -0
  50. codeshift/scanner/code_scanner.py +352 -0
  51. codeshift/scanner/dependency_parser.py +473 -0
  52. codeshift/utils/__init__.py +5 -0
  53. codeshift/utils/api_client.py +266 -0
  54. codeshift/utils/cache.py +318 -0
  55. codeshift/utils/config.py +71 -0
  56. codeshift/utils/llm_client.py +221 -0
  57. codeshift/validator/__init__.py +6 -0
  58. codeshift/validator/syntax_checker.py +183 -0
  59. codeshift/validator/test_runner.py +224 -0
  60. codeshift-0.2.0.dist-info/METADATA +326 -0
  61. codeshift-0.2.0.dist-info/RECORD +65 -0
  62. codeshift-0.2.0.dist-info/WHEEL +5 -0
  63. codeshift-0.2.0.dist-info/entry_points.txt +2 -0
  64. codeshift-0.2.0.dist-info/licenses/LICENSE +21 -0
  65. codeshift-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,256 @@
1
+ """Base transformer infrastructure for AST-based code migrations."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from pathlib import Path
6
+
7
+ import libcst as cst
8
+
9
+
10
+ class TransformStatus(Enum):
11
+ """Status of a transformation."""
12
+
13
+ SUCCESS = "success"
14
+ PARTIAL = "partial" # Some transforms applied, some failed
15
+ FAILED = "failed"
16
+ NO_CHANGES = "no_changes"
17
+
18
+
19
+ @dataclass
20
+ class TransformChange:
21
+ """Represents a single code change made by a transform."""
22
+
23
+ description: str
24
+ line_number: int
25
+ original: str
26
+ replacement: str
27
+ transform_name: str
28
+ confidence: float = 1.0 # 0.0 to 1.0
29
+ notes: str | None = None
30
+
31
+
32
+ @dataclass
33
+ class TransformResult:
34
+ """Result of applying transforms to a file."""
35
+
36
+ file_path: Path
37
+ status: TransformStatus
38
+ original_code: str
39
+ transformed_code: str
40
+ changes: list[TransformChange] = field(default_factory=list)
41
+ errors: list[str] = field(default_factory=list)
42
+
43
+ @property
44
+ def has_changes(self) -> bool:
45
+ """Check if any changes were made."""
46
+ return self.original_code != self.transformed_code
47
+
48
+ @property
49
+ def change_count(self) -> int:
50
+ """Get the number of changes made."""
51
+ return len(self.changes)
52
+
53
+ def get_diff_lines(self) -> list[str]:
54
+ """Get a simple diff representation."""
55
+ import difflib
56
+
57
+ original_lines = self.original_code.splitlines(keepends=True)
58
+ transformed_lines = self.transformed_code.splitlines(keepends=True)
59
+
60
+ diff = difflib.unified_diff(
61
+ original_lines,
62
+ transformed_lines,
63
+ fromfile=f"{self.file_path} (original)",
64
+ tofile=f"{self.file_path} (transformed)",
65
+ )
66
+ return list(diff)
67
+
68
+
69
+ class BaseTransformer(cst.CSTTransformer):
70
+ """Base class for LibCST transformers with change tracking."""
71
+
72
+ def __init__(self) -> None:
73
+ super().__init__()
74
+ self.changes: list[TransformChange] = []
75
+ self.errors: list[str] = []
76
+ self._source_lines: list[str] = []
77
+
78
+ def set_source(self, source: str) -> None:
79
+ """Set the source code for reference during transforms."""
80
+ self._source_lines = source.splitlines()
81
+
82
+ def record_change(
83
+ self,
84
+ description: str,
85
+ line_number: int,
86
+ original: str,
87
+ replacement: str,
88
+ transform_name: str,
89
+ confidence: float = 1.0,
90
+ notes: str | None = None,
91
+ ) -> None:
92
+ """Record a change made by the transformer."""
93
+ self.changes.append(
94
+ TransformChange(
95
+ description=description,
96
+ line_number=line_number,
97
+ original=original,
98
+ replacement=replacement,
99
+ transform_name=transform_name,
100
+ confidence=confidence,
101
+ notes=notes,
102
+ )
103
+ )
104
+
105
+ def record_error(self, error: str) -> None:
106
+ """Record an error that occurred during transformation."""
107
+ self.errors.append(error)
108
+
109
+ def get_line(self, line_number: int) -> str:
110
+ """Get a specific line from the source."""
111
+ if 0 < line_number <= len(self._source_lines):
112
+ return self._source_lines[line_number - 1]
113
+ return ""
114
+
115
+
116
+ def transform_file(
117
+ file_path: Path,
118
+ transformer: BaseTransformer,
119
+ ) -> TransformResult:
120
+ """Transform a file using the given transformer.
121
+
122
+ Args:
123
+ file_path: Path to the file to transform
124
+ transformer: The transformer to use
125
+
126
+ Returns:
127
+ TransformResult with the original and transformed code
128
+ """
129
+ try:
130
+ original_code = file_path.read_text()
131
+ except Exception as e:
132
+ return TransformResult(
133
+ file_path=file_path,
134
+ status=TransformStatus.FAILED,
135
+ original_code="",
136
+ transformed_code="",
137
+ errors=[f"Failed to read file: {e}"],
138
+ )
139
+
140
+ return transform_code(original_code, file_path, transformer)
141
+
142
+
143
+ def transform_code(
144
+ source_code: str,
145
+ file_path: Path,
146
+ transformer: BaseTransformer,
147
+ ) -> TransformResult:
148
+ """Transform source code using the given transformer.
149
+
150
+ Args:
151
+ source_code: The source code to transform
152
+ file_path: Path for reference in results
153
+ transformer: The transformer to use
154
+
155
+ Returns:
156
+ TransformResult with the original and transformed code
157
+ """
158
+ try:
159
+ tree = cst.parse_module(source_code)
160
+ except cst.ParserSyntaxError as e:
161
+ return TransformResult(
162
+ file_path=file_path,
163
+ status=TransformStatus.FAILED,
164
+ original_code=source_code,
165
+ transformed_code=source_code,
166
+ errors=[f"Failed to parse file: {e}"],
167
+ )
168
+
169
+ transformer.set_source(source_code)
170
+
171
+ try:
172
+ transformed_tree = tree.visit(transformer)
173
+ transformed_code = transformed_tree.code
174
+ except Exception as e:
175
+ return TransformResult(
176
+ file_path=file_path,
177
+ status=TransformStatus.FAILED,
178
+ original_code=source_code,
179
+ transformed_code=source_code,
180
+ errors=[f"Transform failed: {e}"],
181
+ )
182
+
183
+ # Determine status
184
+ if transformer.errors:
185
+ if transformer.changes:
186
+ status = TransformStatus.PARTIAL
187
+ else:
188
+ status = TransformStatus.FAILED
189
+ elif transformer.changes:
190
+ status = TransformStatus.SUCCESS
191
+ else:
192
+ status = TransformStatus.NO_CHANGES
193
+
194
+ return TransformResult(
195
+ file_path=file_path,
196
+ status=status,
197
+ original_code=source_code,
198
+ transformed_code=transformed_code,
199
+ changes=transformer.changes,
200
+ errors=transformer.errors,
201
+ )
202
+
203
+
204
+ def apply_transforms(
205
+ file_path: Path,
206
+ transformers: list[BaseTransformer],
207
+ ) -> TransformResult:
208
+ """Apply multiple transformers to a file in sequence.
209
+
210
+ Args:
211
+ file_path: Path to the file to transform
212
+ transformers: List of transformers to apply in order
213
+
214
+ Returns:
215
+ Combined TransformResult
216
+ """
217
+ try:
218
+ current_code = file_path.read_text()
219
+ except Exception as e:
220
+ return TransformResult(
221
+ file_path=file_path,
222
+ status=TransformStatus.FAILED,
223
+ original_code="",
224
+ transformed_code="",
225
+ errors=[f"Failed to read file: {e}"],
226
+ )
227
+
228
+ original_code = current_code
229
+ all_changes = []
230
+ all_errors = []
231
+
232
+ for transformer in transformers:
233
+ result = transform_code(current_code, file_path, transformer)
234
+ current_code = result.transformed_code
235
+ all_changes.extend(result.changes)
236
+ all_errors.extend(result.errors)
237
+
238
+ # Determine final status
239
+ if all_errors:
240
+ if all_changes:
241
+ status = TransformStatus.PARTIAL
242
+ else:
243
+ status = TransformStatus.FAILED
244
+ elif all_changes:
245
+ status = TransformStatus.SUCCESS
246
+ else:
247
+ status = TransformStatus.NO_CHANGES
248
+
249
+ return TransformResult(
250
+ file_path=file_path,
251
+ status=status,
252
+ original_code=original_code,
253
+ transformed_code=current_code,
254
+ changes=all_changes,
255
+ errors=all_errors,
256
+ )
@@ -0,0 +1,395 @@
1
+ """Migration engine with tiered approach."""
2
+
3
+ from collections.abc import Callable
4
+ from pathlib import Path
5
+
6
+ from codeshift.knowledge import (
7
+ Confidence,
8
+ GeneratedKnowledgeBase,
9
+ is_tier_1_library,
10
+ )
11
+ from codeshift.migrator.ast_transforms import (
12
+ TransformChange,
13
+ TransformResult,
14
+ TransformStatus,
15
+ )
16
+ from codeshift.migrator.llm_migrator import LLMMigrator
17
+
18
+
19
+ class MigrationEngine:
20
+ """Orchestrates migrations using a tiered approach.
21
+
22
+ Tier 1: Deterministic AST transforms for well-known libraries
23
+ Tier 2: Knowledge base guided migration with LLM assistance
24
+ Tier 3: Pure LLM migration for unknown patterns
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ llm_migrator: LLMMigrator | None = None,
30
+ ):
31
+ """Initialize the migration engine.
32
+
33
+ Args:
34
+ llm_migrator: Optional LLM migrator instance.
35
+ """
36
+ self.llm_migrator = llm_migrator or LLMMigrator()
37
+
38
+ def run_migration(
39
+ self,
40
+ code: str,
41
+ file_path: Path,
42
+ library: str,
43
+ old_version: str,
44
+ new_version: str,
45
+ knowledge_base: GeneratedKnowledgeBase | None = None,
46
+ progress_callback: Callable[[str], None] | None = None,
47
+ ) -> TransformResult:
48
+ """Run migration using the appropriate tier.
49
+
50
+ Args:
51
+ code: Source code to migrate.
52
+ file_path: Path to the file being migrated.
53
+ library: Library being upgraded.
54
+ old_version: Current version.
55
+ new_version: Target version.
56
+ knowledge_base: Optional generated knowledge base.
57
+ progress_callback: Optional progress callback.
58
+
59
+ Returns:
60
+ TransformResult with migrated code.
61
+ """
62
+
63
+ def report(msg: str) -> None:
64
+ if progress_callback:
65
+ progress_callback(msg)
66
+
67
+ # Tier 1: Try deterministic AST transforms for known libraries
68
+ if is_tier_1_library(library):
69
+ report(f"Using Tier 1 (deterministic AST transforms) for {library}")
70
+ result = self._apply_tier1_transform(code, file_path, library)
71
+
72
+ if result.status == TransformStatus.SUCCESS:
73
+ return result
74
+
75
+ # If partial success, try Tier 2/3 for remaining
76
+ if result.status == TransformStatus.PARTIAL:
77
+ report("Tier 1 partial - attempting Tier 2/3 for remaining changes")
78
+ # Use transformed code as base for next tier
79
+ code = result.transformed_code
80
+
81
+ # Tier 2: Knowledge base guided migration
82
+ if knowledge_base and knowledge_base.overall_confidence >= Confidence.MEDIUM:
83
+ report("Using Tier 2 (KB-guided migration)")
84
+ result = self._apply_tier2_transform(
85
+ code,
86
+ file_path,
87
+ library,
88
+ old_version,
89
+ new_version,
90
+ knowledge_base,
91
+ )
92
+
93
+ if result.status == TransformStatus.SUCCESS:
94
+ return result
95
+
96
+ # Tier 3: Pure LLM migration
97
+ if self.llm_migrator.is_available:
98
+ report("Using Tier 3 (LLM-assisted migration)")
99
+ return self._apply_tier3_transform(
100
+ code,
101
+ file_path,
102
+ library,
103
+ old_version,
104
+ new_version,
105
+ )
106
+
107
+ # No migration possible
108
+ return TransformResult(
109
+ file_path=file_path,
110
+ status=TransformStatus.NO_CHANGES,
111
+ original_code=code,
112
+ transformed_code=code,
113
+ errors=["No migration method available"],
114
+ )
115
+
116
+ def _apply_tier1_transform(
117
+ self,
118
+ code: str,
119
+ file_path: Path,
120
+ library: str,
121
+ ) -> TransformResult:
122
+ """Apply Tier 1 deterministic AST transforms.
123
+
124
+ Args:
125
+ code: Source code to transform.
126
+ file_path: Path to the file.
127
+ library: Library being upgraded.
128
+
129
+ Returns:
130
+ TransformResult.
131
+ """
132
+ # Import transformers dynamically based on library
133
+ transform_func = self._get_transform_func(library)
134
+
135
+ if transform_func is None:
136
+ return TransformResult(
137
+ file_path=file_path,
138
+ status=TransformStatus.NO_CHANGES,
139
+ original_code=code,
140
+ transformed_code=code,
141
+ errors=[f"No Tier 1 transformer for {library}"],
142
+ )
143
+
144
+ try:
145
+ transformed_code, changes = transform_func(code)
146
+
147
+ return TransformResult(
148
+ file_path=file_path,
149
+ status=TransformStatus.SUCCESS if changes else TransformStatus.NO_CHANGES,
150
+ original_code=code,
151
+ transformed_code=transformed_code,
152
+ changes=[
153
+ TransformChange(
154
+ description=c.description,
155
+ line_number=c.line_number,
156
+ original=c.original,
157
+ replacement=c.replacement,
158
+ transform_name=c.transform_name,
159
+ confidence=getattr(c, "confidence", 1.0),
160
+ )
161
+ for c in changes
162
+ ],
163
+ )
164
+ except Exception as e:
165
+ return TransformResult(
166
+ file_path=file_path,
167
+ status=TransformStatus.FAILED,
168
+ original_code=code,
169
+ transformed_code=code,
170
+ errors=[f"Tier 1 transform failed: {e}"],
171
+ )
172
+
173
+ def _apply_tier2_transform(
174
+ self,
175
+ code: str,
176
+ file_path: Path,
177
+ library: str,
178
+ old_version: str,
179
+ new_version: str,
180
+ knowledge_base: GeneratedKnowledgeBase,
181
+ ) -> TransformResult:
182
+ """Apply Tier 2 knowledge base guided migration.
183
+
184
+ Args:
185
+ code: Source code to transform.
186
+ file_path: Path to the file.
187
+ library: Library being upgraded.
188
+ old_version: Current version.
189
+ new_version: Target version.
190
+ knowledge_base: Generated knowledge base.
191
+
192
+ Returns:
193
+ TransformResult.
194
+ """
195
+ if not self.llm_migrator.is_available:
196
+ return TransformResult(
197
+ file_path=file_path,
198
+ status=TransformStatus.NO_CHANGES,
199
+ original_code=code,
200
+ transformed_code=code,
201
+ errors=["LLM not available for Tier 2"],
202
+ )
203
+
204
+ # Build context from knowledge base
205
+ context_parts = [
206
+ f"Breaking changes for {library} {old_version} -> {new_version}:",
207
+ ]
208
+
209
+ for change in knowledge_base.breaking_changes:
210
+ if change.new_api:
211
+ context_parts.append(
212
+ f"- {change.old_api} -> {change.new_api}: {change.description}"
213
+ )
214
+ else:
215
+ context_parts.append(f"- {change.old_api} (removed): {change.description}")
216
+
217
+ context = "\n".join(context_parts)
218
+
219
+ # Use LLM with context
220
+ result = self.llm_migrator.migrate(
221
+ code=code,
222
+ library=library,
223
+ from_version=old_version,
224
+ to_version=new_version,
225
+ context=context,
226
+ )
227
+
228
+ if result.success:
229
+ return TransformResult(
230
+ file_path=file_path,
231
+ status=TransformStatus.SUCCESS,
232
+ original_code=code,
233
+ transformed_code=result.migrated_code,
234
+ changes=[
235
+ TransformChange(
236
+ description="KB-guided LLM migration",
237
+ line_number=1,
238
+ original="(various)",
239
+ replacement="(migrated)",
240
+ transform_name="tier2_kb_guided",
241
+ confidence=0.9,
242
+ )
243
+ ],
244
+ )
245
+
246
+ return TransformResult(
247
+ file_path=file_path,
248
+ status=TransformStatus.FAILED,
249
+ original_code=code,
250
+ transformed_code=code,
251
+ errors=[result.error or "Tier 2 migration failed"],
252
+ )
253
+
254
+ def _apply_tier3_transform(
255
+ self,
256
+ code: str,
257
+ file_path: Path,
258
+ library: str,
259
+ old_version: str,
260
+ new_version: str,
261
+ ) -> TransformResult:
262
+ """Apply Tier 3 pure LLM migration.
263
+
264
+ Args:
265
+ code: Source code to transform.
266
+ file_path: Path to the file.
267
+ library: Library being upgraded.
268
+ old_version: Current version.
269
+ new_version: Target version.
270
+
271
+ Returns:
272
+ TransformResult.
273
+ """
274
+ result = self.llm_migrator.migrate(
275
+ code=code,
276
+ library=library,
277
+ from_version=old_version,
278
+ to_version=new_version,
279
+ )
280
+
281
+ if result.success:
282
+ return TransformResult(
283
+ file_path=file_path,
284
+ status=TransformStatus.SUCCESS,
285
+ original_code=code,
286
+ transformed_code=result.migrated_code,
287
+ changes=[
288
+ TransformChange(
289
+ description="LLM-assisted migration",
290
+ line_number=1,
291
+ original="(various)",
292
+ replacement="(migrated)",
293
+ transform_name="tier3_llm",
294
+ confidence=0.7,
295
+ notes="Review carefully - LLM-generated changes",
296
+ )
297
+ ],
298
+ )
299
+
300
+ return TransformResult(
301
+ file_path=file_path,
302
+ status=TransformStatus.NO_CHANGES,
303
+ original_code=code,
304
+ transformed_code=code,
305
+ errors=[result.error or "Tier 3 migration failed"],
306
+ )
307
+
308
+ def _get_transform_func(self, library: str) -> Callable | None:
309
+ """Get the transform function for a library.
310
+
311
+ Args:
312
+ library: Library name.
313
+
314
+ Returns:
315
+ Transform function or None.
316
+ """
317
+ try:
318
+ if library == "pydantic":
319
+ from codeshift.migrator.transforms.pydantic_v1_to_v2 import (
320
+ transform_pydantic_v1_to_v2,
321
+ )
322
+
323
+ return transform_pydantic_v1_to_v2
324
+ elif library == "fastapi":
325
+ from codeshift.migrator.transforms.fastapi_transformer import (
326
+ transform_fastapi,
327
+ )
328
+
329
+ return transform_fastapi
330
+ elif library == "sqlalchemy":
331
+ from codeshift.migrator.transforms.sqlalchemy_transformer import (
332
+ transform_sqlalchemy,
333
+ )
334
+
335
+ return transform_sqlalchemy
336
+ elif library == "pandas":
337
+ from codeshift.migrator.transforms.pandas_transformer import (
338
+ transform_pandas,
339
+ )
340
+
341
+ return transform_pandas
342
+ elif library == "requests":
343
+ from codeshift.migrator.transforms.requests_transformer import (
344
+ transform_requests,
345
+ )
346
+
347
+ return transform_requests
348
+ except ImportError:
349
+ pass
350
+
351
+ return None
352
+
353
+
354
+ # Singleton instance
355
+ _default_engine: MigrationEngine | None = None
356
+
357
+
358
+ def get_migration_engine() -> MigrationEngine:
359
+ """Get the default migration engine instance."""
360
+ global _default_engine
361
+ if _default_engine is None:
362
+ _default_engine = MigrationEngine()
363
+ return _default_engine
364
+
365
+
366
+ def run_migration(
367
+ code: str,
368
+ file_path: Path,
369
+ library: str,
370
+ old_version: str,
371
+ new_version: str,
372
+ knowledge_base: GeneratedKnowledgeBase | None = None,
373
+ ) -> TransformResult:
374
+ """Convenience function to run a migration.
375
+
376
+ Args:
377
+ code: Source code to migrate.
378
+ file_path: Path to the file.
379
+ library: Library being upgraded.
380
+ old_version: Current version.
381
+ new_version: Target version.
382
+ knowledge_base: Optional generated knowledge base.
383
+
384
+ Returns:
385
+ TransformResult.
386
+ """
387
+ engine = get_migration_engine()
388
+ return engine.run_migration(
389
+ code=code,
390
+ file_path=file_path,
391
+ library=library,
392
+ old_version=old_version,
393
+ new_version=new_version,
394
+ knowledge_base=knowledge_base,
395
+ )