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,221 @@
1
+ """Diff command for viewing proposed changes."""
2
+
3
+ import difflib
4
+ from pathlib import Path
5
+
6
+ import click
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.syntax import Syntax
10
+
11
+ from codeshift.cli.commands.upgrade import load_state
12
+
13
+ console = Console()
14
+
15
+
16
+ @click.command()
17
+ @click.option(
18
+ "--path",
19
+ "-p",
20
+ type=click.Path(exists=True),
21
+ default=".",
22
+ help="Path to the project",
23
+ )
24
+ @click.option(
25
+ "--file",
26
+ "-f",
27
+ type=str,
28
+ help="Show diff for a specific file only",
29
+ )
30
+ @click.option(
31
+ "--no-color",
32
+ is_flag=True,
33
+ help="Disable colored output",
34
+ )
35
+ @click.option(
36
+ "--context",
37
+ "-c",
38
+ type=int,
39
+ default=3,
40
+ help="Number of context lines in diff (default: 3)",
41
+ )
42
+ @click.option(
43
+ "--summary",
44
+ is_flag=True,
45
+ help="Show only a summary of changes without full diff",
46
+ )
47
+ def diff(
48
+ path: str,
49
+ file: str | None,
50
+ no_color: bool,
51
+ context: int,
52
+ summary: bool,
53
+ ) -> None:
54
+ """View detailed diff of proposed changes.
55
+
56
+ \b
57
+ Examples:
58
+ codeshift diff
59
+ codeshift diff --file models.py
60
+ codeshift diff --summary
61
+ """
62
+ project_path = Path(path).resolve()
63
+ state = load_state(project_path)
64
+
65
+ if state is None:
66
+ console.print(
67
+ Panel(
68
+ "[yellow]No pending migration found.[/]\n\n"
69
+ "Run [cyan]codeshift upgrade <library> --target <version>[/] first.",
70
+ title="No Changes",
71
+ )
72
+ )
73
+ return
74
+
75
+ library = state.get("library", "unknown")
76
+ target_version = state.get("target_version", "unknown")
77
+ results = state.get("results", [])
78
+
79
+ if not results:
80
+ console.print("[yellow]No changes pending.[/]")
81
+ return
82
+
83
+ console.print(
84
+ Panel(
85
+ f"[bold]Migration: {library}[/] → v{target_version}\n"
86
+ f"Files: {len(results)} | Total changes: {sum(r.get('change_count', 0) for r in results)}",
87
+ title="Proposed Changes",
88
+ )
89
+ )
90
+
91
+ for result in results:
92
+ file_path = Path(result["file_path"])
93
+ relative_path = (
94
+ file_path.relative_to(project_path)
95
+ if file_path.is_relative_to(project_path)
96
+ else file_path
97
+ )
98
+
99
+ # Filter by file if specified
100
+ if file and str(relative_path) != file and file_path.name != file:
101
+ continue
102
+
103
+ original = result.get("original_code", "")
104
+ transformed = result.get("transformed_code", "")
105
+ changes = result.get("changes", [])
106
+ change_count = result.get("change_count", 0)
107
+
108
+ console.print(f"\n[bold cyan]{relative_path}[/] ({change_count} changes)")
109
+ console.print("─" * 60)
110
+
111
+ if summary:
112
+ # Just show change summaries
113
+ for change in changes:
114
+ console.print(f" • {change['description']}")
115
+ continue
116
+
117
+ # Generate unified diff
118
+ original_lines = original.splitlines(keepends=True)
119
+ transformed_lines = transformed.splitlines(keepends=True)
120
+
121
+ diff_lines = list(
122
+ difflib.unified_diff(
123
+ original_lines,
124
+ transformed_lines,
125
+ fromfile=f"a/{relative_path}",
126
+ tofile=f"b/{relative_path}",
127
+ n=context,
128
+ )
129
+ )
130
+
131
+ if not diff_lines:
132
+ console.print(" [dim]No textual differences[/]")
133
+ continue
134
+
135
+ # Display diff with syntax highlighting
136
+ diff_text = "".join(diff_lines)
137
+
138
+ if no_color:
139
+ console.print(diff_text)
140
+ else:
141
+ # Color the diff manually for better visibility
142
+ for line in diff_lines:
143
+ if line.startswith("+++") or line.startswith("---"):
144
+ console.print(f"[bold]{line.rstrip()}[/]")
145
+ elif line.startswith("@@"):
146
+ console.print(f"[cyan]{line.rstrip()}[/]")
147
+ elif line.startswith("+"):
148
+ console.print(f"[green]{line.rstrip()}[/]")
149
+ elif line.startswith("-"):
150
+ console.print(f"[red]{line.rstrip()}[/]")
151
+ else:
152
+ console.print(line.rstrip())
153
+
154
+ # Show change descriptions
155
+ console.print("\n[bold]Changes:[/]")
156
+ for change in changes:
157
+ console.print(f" • {change['description']}")
158
+
159
+ # Show next steps
160
+ console.print("\n" + "─" * 60)
161
+ console.print("Next steps:")
162
+ console.print(" [cyan]codeshift apply[/] - Apply all changes")
163
+ console.print(" [cyan]codeshift apply --file X[/] - Apply changes to specific file")
164
+ console.print(" [cyan]codeshift apply --backup[/] - Apply with backup files")
165
+
166
+
167
+ @click.command(name="show")
168
+ @click.argument("file_path", type=str)
169
+ @click.option(
170
+ "--path",
171
+ "-p",
172
+ type=click.Path(exists=True),
173
+ default=".",
174
+ help="Path to the project",
175
+ )
176
+ @click.option(
177
+ "--original",
178
+ is_flag=True,
179
+ help="Show original code instead of transformed",
180
+ )
181
+ def show_file(file_path: str, path: str, original: bool) -> None:
182
+ """Show the full transformed (or original) code for a file.
183
+
184
+ \b
185
+ Examples:
186
+ codeshift show models.py
187
+ codeshift show models.py --original
188
+ """
189
+ project_path = Path(path).resolve()
190
+ state = load_state(project_path)
191
+
192
+ if state is None:
193
+ console.print("[yellow]No pending migration found.[/]")
194
+ return
195
+
196
+ results = state.get("results", [])
197
+
198
+ for result in results:
199
+ result_file = Path(result["file_path"])
200
+ relative_path = (
201
+ result_file.relative_to(project_path)
202
+ if result_file.is_relative_to(project_path)
203
+ else result_file
204
+ )
205
+
206
+ if str(relative_path) == file_path or result_file.name == file_path:
207
+ code = result.get("original_code" if original else "transformed_code", "")
208
+ label = "Original" if original else "Transformed"
209
+
210
+ console.print(
211
+ Panel(
212
+ f"[bold]{label} code for {relative_path}[/]",
213
+ title="File Content",
214
+ )
215
+ )
216
+
217
+ syntax = Syntax(code, "python", theme="monokai", line_numbers=True)
218
+ console.print(syntax)
219
+ return
220
+
221
+ console.print(f"[yellow]File not found in pending changes: {file_path}[/]")
@@ -0,0 +1,368 @@
1
+ """Scan command for discovering all possible migrations in a project."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any, cast
6
+
7
+ import click
8
+ import httpx
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn
12
+ from rich.table import Table
13
+
14
+ from codeshift.knowledge import (
15
+ generate_knowledge_base_sync,
16
+ is_tier_1_library,
17
+ )
18
+ from codeshift.scanner import DependencyParser
19
+ from codeshift.utils.config import ProjectConfig
20
+
21
+ console = Console()
22
+
23
+
24
+ def get_latest_version(package: str) -> str | None:
25
+ """Fetch the latest version of a package from PyPI.
26
+
27
+ Args:
28
+ package: Package name.
29
+
30
+ Returns:
31
+ Latest version string or None.
32
+ """
33
+ try:
34
+ response = httpx.get(f"https://pypi.org/pypi/{package}/json", timeout=10.0)
35
+ if response.status_code == 200:
36
+ return cast(str | None, response.json().get("info", {}).get("version"))
37
+ except Exception:
38
+ pass
39
+ return None
40
+
41
+
42
+ def parse_version(version_spec: str) -> str | None:
43
+ """Extract a version number from a version spec.
44
+
45
+ Args:
46
+ version_spec: Version specification (e.g., ">=1.0,<2.0").
47
+
48
+ Returns:
49
+ Extracted version or None.
50
+ """
51
+ import re
52
+
53
+ match = re.search(r"(\d+\.\d+(?:\.\d+)?)", version_spec)
54
+ if match:
55
+ return match.group(1)
56
+ return None
57
+
58
+
59
+ def compare_versions(current: str, latest: str) -> bool:
60
+ """Check if latest version is newer than current.
61
+
62
+ Args:
63
+ current: Current version.
64
+ latest: Latest version.
65
+
66
+ Returns:
67
+ True if latest > current.
68
+ """
69
+ from packaging.version import Version
70
+
71
+ try:
72
+ return bool(Version(latest) > Version(current))
73
+ except Exception:
74
+ return False
75
+
76
+
77
+ def is_major_upgrade(current: str, latest: str) -> bool:
78
+ """Check if this is a major version upgrade.
79
+
80
+ Args:
81
+ current: Current version.
82
+ latest: Latest version.
83
+
84
+ Returns:
85
+ True if major version changed.
86
+ """
87
+ try:
88
+ current_major = int(current.split(".")[0])
89
+ latest_major = int(latest.split(".")[0])
90
+ return latest_major > current_major
91
+ except Exception:
92
+ return False
93
+
94
+
95
+ @click.command()
96
+ @click.option(
97
+ "--path",
98
+ "-p",
99
+ type=click.Path(exists=True),
100
+ default=".",
101
+ help="Path to the project to scan",
102
+ )
103
+ @click.option(
104
+ "--fetch-changes",
105
+ is_flag=True,
106
+ help="Fetch changelogs and detect breaking changes (slower but more detailed)",
107
+ )
108
+ @click.option(
109
+ "--major-only",
110
+ is_flag=True,
111
+ help="Only show major version upgrades",
112
+ )
113
+ @click.option(
114
+ "--json-output",
115
+ is_flag=True,
116
+ help="Output results as JSON",
117
+ )
118
+ @click.option(
119
+ "--verbose",
120
+ "-v",
121
+ is_flag=True,
122
+ help="Show detailed output",
123
+ )
124
+ def scan(
125
+ path: str,
126
+ fetch_changes: bool,
127
+ major_only: bool,
128
+ json_output: bool,
129
+ verbose: bool,
130
+ ) -> None:
131
+ """Scan your project for possible dependency migrations.
132
+
133
+ This command analyzes your project dependencies, checks for newer versions,
134
+ and suggests which libraries could be upgraded.
135
+
136
+ \b
137
+ Examples:
138
+ codeshift scan
139
+ codeshift scan --fetch-changes
140
+ codeshift scan --major-only
141
+ codeshift scan --json-output
142
+ """
143
+ project_path = Path(path).resolve()
144
+ # Load project config (currently unused, reserved for future use)
145
+ _ = ProjectConfig.from_pyproject(project_path)
146
+
147
+ if not json_output:
148
+ console.print(
149
+ Panel(
150
+ "[bold]Scanning project for possible migrations[/]\n\n" f"Path: {project_path}",
151
+ title="PyResolve Scan",
152
+ )
153
+ )
154
+
155
+ # Parse dependencies
156
+ dep_parser = DependencyParser(project_path)
157
+ dependencies = dep_parser.parse_all()
158
+
159
+ if not dependencies:
160
+ if not json_output:
161
+ console.print("[yellow]No dependencies found in project.[/]")
162
+ return
163
+
164
+ if not json_output:
165
+ console.print(f"\nFound [cyan]{len(dependencies)}[/] dependencies")
166
+
167
+ # Check for updates
168
+ outdated = []
169
+
170
+ with Progress(
171
+ SpinnerColumn(),
172
+ TextColumn("[progress.description]{task.description}"),
173
+ BarColumn(),
174
+ TaskProgressColumn(),
175
+ console=console,
176
+ disable=json_output,
177
+ ) as progress:
178
+ task = progress.add_task("Checking for updates...", total=len(dependencies))
179
+
180
+ for dep in dependencies:
181
+ progress.update(task, description=f"Checking {dep.name}...")
182
+
183
+ current_version = parse_version(dep.version_spec) if dep.version_spec else None
184
+ latest_version = get_latest_version(dep.name)
185
+
186
+ if latest_version and current_version:
187
+ if compare_versions(current_version, latest_version):
188
+ is_major = is_major_upgrade(current_version, latest_version)
189
+
190
+ if major_only and not is_major:
191
+ progress.advance(task)
192
+ continue
193
+
194
+ outdated.append(
195
+ {
196
+ "name": dep.name,
197
+ "current": current_version,
198
+ "latest": latest_version,
199
+ "is_major": is_major,
200
+ "is_tier1": is_tier_1_library(dep.name),
201
+ }
202
+ )
203
+
204
+ progress.advance(task)
205
+
206
+ if not outdated:
207
+ if not json_output:
208
+ console.print("\n[green]All dependencies are up to date![/]")
209
+ else:
210
+ print(json.dumps({"outdated": [], "migrations": []}))
211
+ return
212
+
213
+ # Fetch breaking changes if requested
214
+ migrations = []
215
+
216
+ if fetch_changes:
217
+ if not json_output:
218
+ console.print(
219
+ f"\n[bold]Fetching changelogs for {len(outdated)} outdated packages...[/]\n"
220
+ )
221
+
222
+ with Progress(
223
+ SpinnerColumn(),
224
+ TextColumn("[progress.description]{task.description}"),
225
+ BarColumn(),
226
+ TaskProgressColumn(),
227
+ console=console,
228
+ disable=json_output,
229
+ ) as progress:
230
+ task = progress.add_task("Fetching changelogs...", total=len(outdated))
231
+
232
+ for pkg in outdated:
233
+ progress.update(task, description=f"Analyzing {pkg['name']}...")
234
+
235
+ try:
236
+ kb = generate_knowledge_base_sync(
237
+ package=str(pkg["name"]),
238
+ old_version=str(pkg["current"]),
239
+ new_version=str(pkg["latest"]),
240
+ )
241
+
242
+ pkg["breaking_changes"] = len(kb.breaking_changes)
243
+ pkg["confidence"] = kb.overall_confidence.value
244
+ pkg["changes"] = [
245
+ {
246
+ "old_api": c.old_api,
247
+ "new_api": c.new_api,
248
+ "description": c.description,
249
+ "confidence": c.confidence.value,
250
+ }
251
+ for c in kb.breaking_changes[:5] # Limit to 5 changes
252
+ ]
253
+
254
+ if kb.has_changes or pkg["is_tier1"]:
255
+ migrations.append(pkg)
256
+
257
+ except Exception as e:
258
+ if verbose and not json_output:
259
+ console.print(f" [dim]Could not fetch changelog for {pkg['name']}: {e}[/]")
260
+ pkg["breaking_changes"] = None
261
+ pkg["confidence"] = "unknown"
262
+
263
+ progress.advance(task)
264
+ else:
265
+ # Without fetch_changes, suggest all tier1 and major upgrades
266
+ for pkg in outdated:
267
+ if pkg["is_tier1"] or pkg["is_major"]:
268
+ migrations.append(pkg)
269
+
270
+ # Output results
271
+ if json_output:
272
+ print(
273
+ json.dumps(
274
+ {
275
+ "outdated": outdated,
276
+ "migrations": migrations,
277
+ },
278
+ indent=2,
279
+ )
280
+ )
281
+ return
282
+
283
+ # Display results
284
+ console.print(f"\n[bold]Outdated Dependencies ({len(outdated)})[/]\n")
285
+
286
+ table = Table()
287
+ table.add_column("Package", style="cyan")
288
+ table.add_column("Current", justify="right")
289
+ table.add_column("Latest", justify="right")
290
+ table.add_column("Type", justify="center")
291
+ table.add_column("Tier", justify="center")
292
+
293
+ if fetch_changes:
294
+ table.add_column("Breaking Changes", justify="right")
295
+ table.add_column("Confidence", justify="center")
296
+
297
+ for pkg in outdated:
298
+ type_str = "[red]Major[/]" if pkg["is_major"] else "[yellow]Minor/Patch[/]"
299
+ tier_str = "[green]Tier 1[/]" if pkg["is_tier1"] else "[dim]Tier 2/3[/]"
300
+
301
+ row = [
302
+ pkg["name"],
303
+ pkg["current"],
304
+ pkg["latest"],
305
+ type_str,
306
+ tier_str,
307
+ ]
308
+
309
+ if fetch_changes:
310
+ changes = pkg.get("breaking_changes")
311
+ if changes is not None:
312
+ row.append(str(changes))
313
+ confidence = str(pkg.get("confidence", "unknown"))
314
+ conf_style = {
315
+ "high": "[green]HIGH[/]",
316
+ "medium": "[yellow]MEDIUM[/]",
317
+ "low": "[dim]LOW[/]",
318
+ }.get(confidence, "[dim]?[/]")
319
+ row.append(conf_style)
320
+ else:
321
+ row.append("[dim]?[/]")
322
+ row.append("[dim]?[/]")
323
+
324
+ table.add_row(*[str(item) for item in row])
325
+
326
+ console.print(table)
327
+
328
+ # Show suggested migrations
329
+ if migrations:
330
+ console.print(f"\n[bold]Suggested Migrations ({len(migrations)})[/]\n")
331
+
332
+ for pkg in migrations:
333
+ tier_label = (
334
+ "[green](Tier 1 - deterministic)[/]"
335
+ if pkg["is_tier1"]
336
+ else "[yellow](Tier 2/3 - LLM-assisted)[/]"
337
+ )
338
+ console.print(
339
+ f" [cyan]{pkg['name']}[/] {pkg['current']} → {pkg['latest']} {tier_label}"
340
+ )
341
+
342
+ if fetch_changes and pkg.get("changes"):
343
+ console.print(" Breaking changes:")
344
+ changes_list = cast(list[dict[str, Any]], pkg["changes"])
345
+ for change in changes_list[:3]:
346
+ if change["new_api"]:
347
+ console.print(
348
+ f" [dim]├──[/] {change['old_api']} → {change['new_api']}"
349
+ )
350
+ else:
351
+ console.print(f" [dim]├──[/] {change['old_api']} [red](removed)[/]")
352
+ pkg_changes = cast(list[Any], pkg.get("changes", []))
353
+ if len(pkg_changes) > 3:
354
+ console.print(f" [dim]└── ... and {len(pkg_changes) - 3} more[/]")
355
+
356
+ console.print()
357
+
358
+ console.print("[bold]To migrate a package, run:[/]")
359
+ console.print(" [cyan]codeshift upgrade <package> --target <version>[/]\n")
360
+
361
+ # Show quick commands
362
+ console.print("[bold]Quick commands:[/]")
363
+ for pkg in migrations[:3]:
364
+ console.print(f" [dim]codeshift upgrade {pkg['name']} --target {pkg['latest']}[/]")
365
+ else:
366
+ console.print(
367
+ "\n[dim]No migrations suggested. Use --fetch-changes for detailed analysis.[/]"
368
+ )