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,320 @@
1
+ """LLM-based migration for complex cases.
2
+
3
+ LLM migrations (Tier 2/3) are routed through the PyResolve API,
4
+ which handles authentication, quota checking, and billing.
5
+ Users must have a Pro or Unlimited subscription to use LLM features.
6
+
7
+ Tier 1 (deterministic AST transforms) remains free and local.
8
+ """
9
+
10
+ import re
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ from codeshift.migrator.ast_transforms import (
15
+ TransformChange,
16
+ TransformResult,
17
+ TransformStatus,
18
+ )
19
+ from codeshift.utils.api_client import PyResolveAPIClient, get_api_client
20
+ from codeshift.utils.cache import LLMCache, get_llm_cache
21
+ from codeshift.validator.syntax_checker import quick_syntax_check
22
+
23
+
24
+ @dataclass
25
+ class LLMMigrationResult:
26
+ """Result of an LLM-based migration."""
27
+
28
+ original_code: str
29
+ migrated_code: str
30
+ success: bool
31
+ used_cache: bool = False
32
+ error: str | None = None
33
+ validation_passed: bool = True
34
+ usage: dict | None = None
35
+
36
+
37
+ class LLMMigrator:
38
+ """Handles complex migrations using LLM via the PyResolve API.
39
+
40
+ LLM migrations require authentication and a Pro or Unlimited subscription.
41
+ All LLM calls are routed through the PyResolve API, which handles:
42
+ - Authentication and authorization
43
+ - Quota checking and billing
44
+ - Server-side Anthropic API calls
45
+
46
+ This ensures users cannot bypass the subscription model by using
47
+ their own API keys.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ client: PyResolveAPIClient | None = None,
53
+ cache: LLMCache | None = None,
54
+ use_cache: bool = True,
55
+ validate_output: bool = True,
56
+ ):
57
+ """Initialize the LLM migrator.
58
+
59
+ Args:
60
+ client: API client to use. Defaults to singleton.
61
+ cache: Cache to use. Defaults to singleton.
62
+ use_cache: Whether to use caching
63
+ validate_output: Whether to validate migrated code syntax
64
+ """
65
+ self.client = client or get_api_client()
66
+ self.cache = cache or get_llm_cache() if use_cache else None
67
+ self.use_cache = use_cache
68
+ self.validate_output = validate_output
69
+
70
+ @property
71
+ def is_available(self) -> bool:
72
+ """Check if LLM migration is available (user is authenticated)."""
73
+ return self.client.is_available
74
+
75
+ def migrate(
76
+ self,
77
+ code: str,
78
+ library: str,
79
+ from_version: str,
80
+ to_version: str,
81
+ context: str | None = None,
82
+ ) -> LLMMigrationResult:
83
+ """Migrate code using the LLM via PyResolve API.
84
+
85
+ Args:
86
+ code: Source code to migrate
87
+ library: Library being upgraded
88
+ from_version: Current version
89
+ to_version: Target version
90
+ context: Optional context about the code
91
+
92
+ Returns:
93
+ LLMMigrationResult with the migrated code
94
+ """
95
+ if not self.is_available:
96
+ return LLMMigrationResult(
97
+ original_code=code,
98
+ migrated_code=code,
99
+ success=False,
100
+ error=(
101
+ "LLM migrations require authentication. "
102
+ "Run 'codeshift login' to authenticate, then upgrade to Pro tier."
103
+ ),
104
+ )
105
+
106
+ # Check cache first
107
+ if self.use_cache and self.cache:
108
+ cached = self.cache.get_migration(code, library, from_version, to_version)
109
+ if cached:
110
+ return LLMMigrationResult(
111
+ original_code=code,
112
+ migrated_code=cached,
113
+ success=True,
114
+ used_cache=True,
115
+ )
116
+
117
+ # Call PyResolve API (which calls Anthropic server-side)
118
+ response = self.client.migrate_code(
119
+ code=code,
120
+ library=library,
121
+ from_version=from_version,
122
+ to_version=to_version,
123
+ context=context,
124
+ )
125
+
126
+ if not response.success:
127
+ return LLMMigrationResult(
128
+ original_code=code,
129
+ migrated_code=code,
130
+ success=False,
131
+ error=response.error,
132
+ )
133
+
134
+ migrated_code = response.content
135
+
136
+ # Validate syntax
137
+ validation_passed = True
138
+ if self.validate_output:
139
+ if not quick_syntax_check(migrated_code):
140
+ validation_passed = False
141
+ # Try to fix common issues
142
+ migrated_code = self._attempt_fix(migrated_code)
143
+ if not quick_syntax_check(migrated_code):
144
+ return LLMMigrationResult(
145
+ original_code=code,
146
+ migrated_code=code,
147
+ success=False,
148
+ error="LLM output has syntax errors",
149
+ validation_passed=False,
150
+ )
151
+
152
+ # Cache the result
153
+ if self.use_cache and self.cache:
154
+ self.cache.set_migration(code, library, from_version, to_version, migrated_code)
155
+
156
+ return LLMMigrationResult(
157
+ original_code=code,
158
+ migrated_code=migrated_code,
159
+ success=True,
160
+ validation_passed=validation_passed,
161
+ usage=response.usage,
162
+ )
163
+
164
+ def _attempt_fix(self, code: str) -> str:
165
+ """Attempt to fix common syntax issues in LLM output.
166
+
167
+ Args:
168
+ code: Code with potential issues
169
+
170
+ Returns:
171
+ Fixed code (or original if unfixable)
172
+ """
173
+ # Common fixes
174
+ fixes = [
175
+ # Remove trailing incomplete lines
176
+ (r"\n\s*$", "\n"),
177
+ # Fix unclosed strings (simple cases)
178
+ (r'(["\'])([^"\'\n]*?)$', r"\1\2\1"),
179
+ ]
180
+
181
+ fixed = code
182
+ for pattern, replacement in fixes:
183
+ fixed = re.sub(pattern, replacement, fixed)
184
+
185
+ return fixed
186
+
187
+ def explain_migration(
188
+ self,
189
+ original: str,
190
+ transformed: str,
191
+ library: str,
192
+ ) -> str | None:
193
+ """Get an explanation of a migration change.
194
+
195
+ Args:
196
+ original: Original code
197
+ transformed: Transformed code
198
+ library: Library being upgraded
199
+
200
+ Returns:
201
+ Explanation string or None if unavailable
202
+ """
203
+ if not self.is_available:
204
+ return None
205
+
206
+ response = self.client.explain_change(original, transformed, library)
207
+ if response.success:
208
+ return response.content.strip()
209
+ return None
210
+
211
+
212
+ def migrate_with_llm_fallback(
213
+ code: str,
214
+ library: str,
215
+ from_version: str,
216
+ to_version: str,
217
+ deterministic_result: TransformResult | None = None,
218
+ ) -> TransformResult:
219
+ """Migrate code with LLM as a fallback for failures.
220
+
221
+ IMPORTANT: LLM migrations require Pro tier or higher subscription.
222
+ If the user is not authenticated or doesn't have the required tier,
223
+ only deterministic transforms will be applied.
224
+
225
+ Args:
226
+ code: Source code to migrate
227
+ library: Library being upgraded
228
+ from_version: Current version
229
+ to_version: Target version
230
+ deterministic_result: Optional result from deterministic transform
231
+
232
+ Returns:
233
+ TransformResult combining deterministic and LLM migrations
234
+ """
235
+ # If deterministic transform succeeded fully, use it
236
+ if deterministic_result and deterministic_result.status == TransformStatus.SUCCESS:
237
+ return deterministic_result
238
+
239
+ # Try LLM migration
240
+ migrator = LLMMigrator()
241
+
242
+ if not migrator.is_available:
243
+ # Return deterministic result if available, or no changes
244
+ if deterministic_result:
245
+ # Add a note about LLM not being available
246
+ errors = list(deterministic_result.errors)
247
+ errors.append(
248
+ "LLM fallback not available. Run 'codeshift login' and upgrade to Pro "
249
+ "for LLM-powered migrations."
250
+ )
251
+ return TransformResult(
252
+ file_path=deterministic_result.file_path,
253
+ status=deterministic_result.status,
254
+ original_code=deterministic_result.original_code,
255
+ transformed_code=deterministic_result.transformed_code,
256
+ changes=deterministic_result.changes,
257
+ errors=errors,
258
+ )
259
+ return TransformResult(
260
+ file_path=Path("<unknown>"),
261
+ status=TransformStatus.NO_CHANGES,
262
+ original_code=code,
263
+ transformed_code=code,
264
+ errors=[
265
+ "LLM not available. Run 'codeshift login' and upgrade to Pro "
266
+ "for LLM-powered migrations."
267
+ ],
268
+ )
269
+
270
+ # Use deterministic result as base if available
271
+ base_code = deterministic_result.transformed_code if deterministic_result else code
272
+ context = None
273
+
274
+ if deterministic_result and deterministic_result.errors:
275
+ context = f"Deterministic transform had issues: {', '.join(deterministic_result.errors)}"
276
+
277
+ result = migrator.migrate(
278
+ code=base_code,
279
+ library=library,
280
+ from_version=from_version,
281
+ to_version=to_version,
282
+ context=context,
283
+ )
284
+
285
+ # Combine results
286
+ changes = []
287
+ if deterministic_result:
288
+ changes.extend(deterministic_result.changes)
289
+
290
+ if result.success and result.migrated_code != base_code:
291
+ changes.append(
292
+ TransformChange(
293
+ description="LLM-assisted migration",
294
+ line_number=1,
295
+ original="(various)",
296
+ replacement="(LLM migrated)",
297
+ transform_name="llm_migration",
298
+ confidence=0.8, # Lower confidence for LLM
299
+ notes="This change was made by the LLM and should be reviewed carefully",
300
+ )
301
+ )
302
+
303
+ status = TransformStatus.SUCCESS if result.success else TransformStatus.PARTIAL
304
+ if not changes:
305
+ status = TransformStatus.NO_CHANGES
306
+
307
+ errors = []
308
+ if deterministic_result:
309
+ errors.extend(deterministic_result.errors)
310
+ if result.error:
311
+ errors.append(result.error)
312
+
313
+ return TransformResult(
314
+ file_path=deterministic_result.file_path if deterministic_result else Path("<unknown>"),
315
+ status=status,
316
+ original_code=code,
317
+ transformed_code=result.migrated_code if result.success else base_code,
318
+ changes=changes,
319
+ errors=errors,
320
+ )
@@ -0,0 +1,19 @@
1
+ """Library-specific transformation modules."""
2
+
3
+ from codeshift.migrator.transforms.fastapi_transformer import FastAPITransformer
4
+ from codeshift.migrator.transforms.pandas_transformer import (
5
+ PandasAppendTransformer,
6
+ PandasTransformer,
7
+ )
8
+ from codeshift.migrator.transforms.pydantic_v1_to_v2 import PydanticV1ToV2Transformer
9
+ from codeshift.migrator.transforms.requests_transformer import RequestsTransformer
10
+ from codeshift.migrator.transforms.sqlalchemy_transformer import SQLAlchemyTransformer
11
+
12
+ __all__ = [
13
+ "PydanticV1ToV2Transformer",
14
+ "FastAPITransformer",
15
+ "SQLAlchemyTransformer",
16
+ "PandasTransformer",
17
+ "PandasAppendTransformer",
18
+ "RequestsTransformer",
19
+ ]
@@ -0,0 +1,174 @@
1
+ """FastAPI transformation using LibCST."""
2
+
3
+ import libcst as cst
4
+
5
+ from codeshift.migrator.ast_transforms import BaseTransformer
6
+
7
+
8
+ class FastAPITransformer(BaseTransformer):
9
+ """Transform FastAPI code for version upgrades (0.99 to 0.100+)."""
10
+
11
+ def __init__(self) -> None:
12
+ super().__init__()
13
+ self._in_fastapi_context = False
14
+
15
+ def leave_ImportFrom(
16
+ self, original_node: cst.ImportFrom, updated_node: cst.ImportFrom
17
+ ) -> cst.ImportFrom:
18
+ """Transform starlette imports to fastapi imports."""
19
+ if original_node.module is None:
20
+ return updated_node
21
+
22
+ module_name = self._get_module_name(original_node.module)
23
+
24
+ # Transform starlette.responses imports
25
+ if module_name == "starlette.responses":
26
+ self.record_change(
27
+ description="Import from fastapi.responses instead of starlette.responses",
28
+ line_number=1,
29
+ original=f"from {module_name}",
30
+ replacement="from fastapi.responses",
31
+ transform_name="starlette_to_fastapi_responses",
32
+ )
33
+ return updated_node.with_changes(
34
+ module=cst.Attribute(
35
+ value=cst.Name("fastapi"),
36
+ attr=cst.Name("responses"),
37
+ )
38
+ )
39
+
40
+ # Transform starlette.requests imports
41
+ if module_name == "starlette.requests":
42
+ self.record_change(
43
+ description="Import Request from fastapi instead of starlette.requests",
44
+ line_number=1,
45
+ original=f"from {module_name}",
46
+ replacement="from fastapi",
47
+ transform_name="starlette_to_fastapi_request",
48
+ )
49
+ return updated_node.with_changes(module=cst.Name("fastapi"))
50
+
51
+ # Transform starlette.websockets imports
52
+ if module_name == "starlette.websockets":
53
+ self.record_change(
54
+ description="Import WebSocket from fastapi instead of starlette.websockets",
55
+ line_number=1,
56
+ original=f"from {module_name}",
57
+ replacement="from fastapi",
58
+ transform_name="starlette_to_fastapi_websocket",
59
+ )
60
+ return updated_node.with_changes(module=cst.Name("fastapi"))
61
+
62
+ # Transform starlette.status imports
63
+ if module_name == "starlette.status":
64
+ self.record_change(
65
+ description="Import status from fastapi instead of starlette",
66
+ line_number=1,
67
+ original=f"from {module_name}",
68
+ replacement="from fastapi import status",
69
+ transform_name="starlette_to_fastapi_status",
70
+ )
71
+ return updated_node.with_changes(module=cst.Name("fastapi"))
72
+
73
+ return updated_node
74
+
75
+ def leave_Call(self, original_node: cst.Call, updated_node: cst.Call) -> cst.Call:
76
+ """Transform FastAPI function calls."""
77
+ # Handle Field, Query, Path, Body regex -> pattern
78
+ if isinstance(updated_node.func, cst.Name):
79
+ func_name = updated_node.func.value
80
+ if func_name in ("Field", "Query", "Path", "Body"):
81
+ new_args = []
82
+ changed = False
83
+ for arg in updated_node.args:
84
+ if isinstance(arg.keyword, cst.Name) and arg.keyword.value == "regex":
85
+ new_args.append(arg.with_changes(keyword=cst.Name("pattern")))
86
+ changed = True
87
+ self.record_change(
88
+ description=f"Rename {func_name}(regex=...) to {func_name}(pattern=...)",
89
+ line_number=1,
90
+ original=f"{func_name}(regex=...)",
91
+ replacement=f"{func_name}(pattern=...)",
92
+ transform_name=f"{func_name.lower()}_regex_to_pattern",
93
+ )
94
+ else:
95
+ new_args.append(arg)
96
+
97
+ if changed:
98
+ return updated_node.with_changes(args=new_args)
99
+
100
+ # Handle Depends(use_cache=...) -> Depends(use_cached=...)
101
+ if isinstance(updated_node.func, cst.Name) and updated_node.func.value == "Depends":
102
+ new_args = []
103
+ changed = False
104
+ for arg in updated_node.args:
105
+ if isinstance(arg.keyword, cst.Name) and arg.keyword.value == "use_cache":
106
+ new_args.append(arg.with_changes(keyword=cst.Name("use_cached")))
107
+ changed = True
108
+ self.record_change(
109
+ description="Rename Depends(use_cache=...) to Depends(use_cached=...)",
110
+ line_number=1,
111
+ original="Depends(use_cache=...)",
112
+ replacement="Depends(use_cached=...)",
113
+ transform_name="depends_use_cache_rename",
114
+ )
115
+ else:
116
+ new_args.append(arg)
117
+
118
+ if changed:
119
+ return updated_node.with_changes(args=new_args)
120
+
121
+ # Handle FastAPI(openapi_prefix=...) -> FastAPI(root_path=...)
122
+ if isinstance(updated_node.func, cst.Name) and updated_node.func.value == "FastAPI":
123
+ new_args = []
124
+ changed = False
125
+ for arg in updated_node.args:
126
+ if isinstance(arg.keyword, cst.Name) and arg.keyword.value == "openapi_prefix":
127
+ new_args.append(arg.with_changes(keyword=cst.Name("root_path")))
128
+ changed = True
129
+ self.record_change(
130
+ description="Rename openapi_prefix to root_path",
131
+ line_number=1,
132
+ original="FastAPI(openapi_prefix=...)",
133
+ replacement="FastAPI(root_path=...)",
134
+ transform_name="openapi_prefix_to_root_path",
135
+ )
136
+ else:
137
+ new_args.append(arg)
138
+
139
+ if changed:
140
+ return updated_node.with_changes(args=new_args)
141
+
142
+ return updated_node
143
+
144
+ def _get_module_name(self, module: cst.BaseExpression) -> str:
145
+ """Get the full module name from a Name or Attribute node."""
146
+ if isinstance(module, cst.Name):
147
+ return str(module.value)
148
+ elif isinstance(module, cst.Attribute):
149
+ return f"{self._get_module_name(module.value)}.{module.attr.value}"
150
+ return ""
151
+
152
+
153
+ def transform_fastapi(source_code: str) -> tuple[str, list]:
154
+ """Transform FastAPI code.
155
+
156
+ Args:
157
+ source_code: The source code to transform
158
+
159
+ Returns:
160
+ Tuple of (transformed_code, list of changes)
161
+ """
162
+ try:
163
+ tree = cst.parse_module(source_code)
164
+ except cst.ParserSyntaxError:
165
+ return source_code, []
166
+
167
+ transformer = FastAPITransformer()
168
+ transformer.set_source(source_code)
169
+
170
+ try:
171
+ transformed_tree = tree.visit(transformer)
172
+ return transformed_tree.code, transformer.changes
173
+ except Exception:
174
+ return source_code, []