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,436 @@
1
+ """Upgrade command for analyzing and preparing migrations."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any, cast
6
+
7
+ import click
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.progress import Progress, SpinnerColumn, TextColumn
11
+ from rich.table import Table
12
+
13
+ from codeshift.cli.quota import (
14
+ QuotaError,
15
+ check_quota,
16
+ record_usage,
17
+ show_quota_exceeded_message,
18
+ )
19
+ from codeshift.knowledge import (
20
+ Confidence,
21
+ GeneratedKnowledgeBase,
22
+ generate_knowledge_base_sync,
23
+ is_tier_1_library,
24
+ )
25
+ from codeshift.knowledge_base import KnowledgeBaseLoader
26
+ from codeshift.migrator.ast_transforms import TransformChange, TransformResult, TransformStatus
27
+ from codeshift.migrator.transforms.fastapi_transformer import transform_fastapi
28
+ from codeshift.migrator.transforms.pandas_transformer import transform_pandas
29
+ from codeshift.migrator.transforms.pydantic_v1_to_v2 import transform_pydantic_v1_to_v2
30
+ from codeshift.migrator.transforms.requests_transformer import transform_requests
31
+ from codeshift.migrator.transforms.sqlalchemy_transformer import transform_sqlalchemy
32
+ from codeshift.scanner import CodeScanner, DependencyParser
33
+ from codeshift.utils.config import ProjectConfig
34
+
35
+ console = Console()
36
+
37
+
38
+ def load_state(project_path: Path) -> dict[str, Any] | None:
39
+ """Load the current migration state if it exists."""
40
+ state_file = project_path / ".codeshift" / "state.json"
41
+ if state_file.exists():
42
+ try:
43
+ return cast(dict[str, Any], json.loads(state_file.read_text()))
44
+ except Exception:
45
+ return None
46
+ return None
47
+
48
+
49
+ def save_state(project_path: Path, state: dict) -> None:
50
+ """Save the migration state."""
51
+ state_dir = project_path / ".codeshift"
52
+ state_dir.mkdir(parents=True, exist_ok=True)
53
+ state_file = state_dir / "state.json"
54
+ state_file.write_text(json.dumps(state, indent=2, default=str))
55
+
56
+
57
+ @click.command()
58
+ @click.argument("library")
59
+ @click.option(
60
+ "--target",
61
+ "-t",
62
+ required=True,
63
+ help="Target version to upgrade to (e.g., 2.5.0)",
64
+ )
65
+ @click.option(
66
+ "--path",
67
+ "-p",
68
+ type=click.Path(exists=True),
69
+ default=".",
70
+ help="Path to the project to analyze",
71
+ )
72
+ @click.option(
73
+ "--file",
74
+ "-f",
75
+ type=click.Path(exists=True),
76
+ help="Analyze a single file instead of the entire project",
77
+ )
78
+ @click.option(
79
+ "--dry-run",
80
+ is_flag=True,
81
+ help="Show what would be changed without saving state",
82
+ )
83
+ @click.option(
84
+ "--verbose",
85
+ "-v",
86
+ is_flag=True,
87
+ help="Show detailed output",
88
+ )
89
+ def upgrade(
90
+ library: str,
91
+ target: str,
92
+ path: str,
93
+ file: str | None,
94
+ dry_run: bool,
95
+ verbose: bool,
96
+ ) -> None:
97
+ """Analyze your codebase and propose changes for a library upgrade.
98
+
99
+ \b
100
+ Examples:
101
+ codeshift upgrade pydantic --target 2.5.0
102
+ codeshift upgrade pydantic -t 2.0 --file models.py
103
+ codeshift upgrade pydantic -t 2.0 --dry-run
104
+ """
105
+ project_path = Path(path).resolve()
106
+ project_config = ProjectConfig.from_pyproject(project_path)
107
+
108
+ # Check quota before starting (allow offline for Tier 1 libraries)
109
+ is_tier1 = is_tier_1_library(library)
110
+ try:
111
+ check_quota("file_migrated", quantity=1, allow_offline=is_tier1)
112
+ except QuotaError as e:
113
+ show_quota_exceeded_message(e)
114
+ raise SystemExit(1) from e
115
+
116
+ # Load knowledge base
117
+ loader = KnowledgeBaseLoader()
118
+
119
+ try:
120
+ knowledge = loader.load(library)
121
+ except FileNotFoundError as e:
122
+ console.print(f"[red]Error:[/] {e}")
123
+ console.print(f"\nSupported libraries: {', '.join(loader.get_supported_libraries())}")
124
+ raise SystemExit(1) from e
125
+
126
+ # Check if migration is supported
127
+ # For now, we'll allow any version since we're doing a general migration
128
+ console.print(
129
+ Panel(
130
+ f"[bold]Upgrading {knowledge.display_name}[/] to version [cyan]{target}[/]\n\n"
131
+ f"{knowledge.description}\n"
132
+ f"Migration guide: {knowledge.migration_guide_url or 'N/A'}",
133
+ title="PyResolve Migration",
134
+ )
135
+ )
136
+
137
+ # Step 1: Parse dependencies
138
+ with Progress(
139
+ SpinnerColumn(),
140
+ TextColumn("[progress.description]{task.description}"),
141
+ console=console,
142
+ ) as progress:
143
+ task = progress.add_task("Checking project dependencies...", total=None)
144
+
145
+ dep_parser = DependencyParser(project_path)
146
+ current_dep = dep_parser.get_dependency(library)
147
+
148
+ current_version = None
149
+ if current_dep:
150
+ console.print(
151
+ f"Found [cyan]{library}[/] in project dependencies: {current_dep.version_spec or 'any version'}"
152
+ )
153
+ # Extract version number from spec (e.g., ">=1.0,<2.0" -> "1.0")
154
+ if current_dep.version_spec:
155
+ import re
156
+
157
+ version_match = re.search(r"(\d+\.\d+(?:\.\d+)?)", current_dep.version_spec)
158
+ if version_match:
159
+ current_version = version_match.group(1)
160
+ else:
161
+ console.print(f"[yellow]Warning:[/] {library} not found in project dependencies")
162
+
163
+ progress.update(task, completed=True)
164
+
165
+ # Step 2: Fetch knowledge sources from GitHub
166
+ generated_kb: GeneratedKnowledgeBase | None = None
167
+
168
+ with Progress(
169
+ SpinnerColumn(),
170
+ TextColumn("[progress.description]{task.description}"),
171
+ console=console,
172
+ ) as progress:
173
+ task = progress.add_task("Fetching knowledge sources...", total=None)
174
+
175
+ def progress_callback(msg: str) -> None:
176
+ progress.update(task, description=msg)
177
+
178
+ try:
179
+ generated_kb = generate_knowledge_base_sync(
180
+ package=library,
181
+ old_version=current_version or "1.0",
182
+ new_version=target,
183
+ progress_callback=progress_callback,
184
+ )
185
+ except Exception as e:
186
+ if verbose:
187
+ console.print(f"[yellow]Warning:[/] Could not fetch knowledge sources: {e}")
188
+
189
+ progress.update(task, completed=True)
190
+
191
+ # Display detected breaking changes
192
+ if generated_kb and generated_kb.has_changes:
193
+ console.print("\n[bold]Breaking changes detected:[/]\n")
194
+
195
+ # Group by confidence
196
+ high_confidence = generated_kb.get_changes_by_confidence(Confidence.HIGH)
197
+ medium_confidence = [
198
+ c for c in generated_kb.breaking_changes if c.confidence == Confidence.MEDIUM
199
+ ]
200
+ low_confidence = [
201
+ c for c in generated_kb.breaking_changes if c.confidence == Confidence.LOW
202
+ ]
203
+
204
+ if high_confidence:
205
+ console.print(" [green]HIGH CONFIDENCE:[/]")
206
+ for change in high_confidence:
207
+ if change.new_api:
208
+ console.print(f" [dim]├──[/] {change.old_api} [dim]→[/] {change.new_api}")
209
+ else:
210
+ console.print(f" [dim]├──[/] {change.old_api} [red](removed)[/]")
211
+
212
+ if medium_confidence:
213
+ console.print("\n [yellow]MEDIUM CONFIDENCE:[/]")
214
+ for change in medium_confidence:
215
+ if change.new_api:
216
+ console.print(f" [dim]├──[/] {change.old_api} [dim]→[/] {change.new_api}")
217
+ else:
218
+ console.print(f" [dim]├──[/] {change.old_api} [red](removed)[/]")
219
+
220
+ if low_confidence and verbose:
221
+ console.print("\n [dim]LOW CONFIDENCE:[/]")
222
+ for change in low_confidence:
223
+ if change.new_api:
224
+ console.print(f" [dim]├──[/] {change.old_api} [dim]→[/] {change.new_api}")
225
+ else:
226
+ console.print(f" [dim]├──[/] {change.old_api} [red](removed)[/]")
227
+
228
+ if generated_kb.sources:
229
+ console.print(
230
+ f"\n [dim]Sources: {', '.join(generated_kb.sources[:2])}{'...' if len(generated_kb.sources) > 2 else ''}[/]"
231
+ )
232
+
233
+ elif generated_kb:
234
+ console.print("\n[dim]No breaking changes detected from changelog sources.[/]")
235
+
236
+ # Step 3: Scan for library usage
237
+ console.print("") # Add spacing
238
+ with Progress(
239
+ SpinnerColumn(),
240
+ TextColumn("[progress.description]{task.description}"),
241
+ console=console,
242
+ ) as progress:
243
+ task = progress.add_task("Scanning for library usage...", total=None)
244
+
245
+ scanner = CodeScanner(library, exclude_patterns=project_config.exclude)
246
+
247
+ if file:
248
+ # Single file mode
249
+ file_path = Path(file).resolve()
250
+ imports, usages = scanner.scan_file(file_path)
251
+ scan_result_files = 1
252
+ scan_result_imports = imports
253
+ scan_result_usages = usages
254
+ scan_result_errors = []
255
+ else:
256
+ # Full project scan
257
+ scan_result = scanner.scan_directory(project_path)
258
+ scan_result_files = scan_result.files_scanned
259
+ scan_result_imports = scan_result.imports
260
+ scan_result_usages = scan_result.usages
261
+ scan_result_errors = scan_result.errors
262
+
263
+ progress.update(task, completed=True)
264
+
265
+ console.print(f"\nScanned [cyan]{scan_result_files}[/] files")
266
+ console.print(f"Found [cyan]{len(scan_result_imports)}[/] imports from {library}")
267
+ console.print(f"Found [cyan]{len(scan_result_usages)}[/] usages of {library} symbols")
268
+
269
+ if scan_result_errors:
270
+ console.print(f"[yellow]Warnings:[/] {len(scan_result_errors)} files could not be parsed")
271
+ if verbose:
272
+ for file_path, error in scan_result_errors:
273
+ console.print(f" - {file_path}: {error}")
274
+
275
+ if not scan_result_imports:
276
+ console.print(f"\n[yellow]No {library} imports found in the codebase.[/]")
277
+ return
278
+
279
+ # Step 4: Apply transforms
280
+ with Progress(
281
+ SpinnerColumn(),
282
+ TextColumn("[progress.description]{task.description}"),
283
+ console=console,
284
+ ) as progress:
285
+ task = progress.add_task("Analyzing code and proposing changes...", total=None)
286
+
287
+ # Get unique files with imports
288
+ files_to_transform = set()
289
+ for imp in scan_result_imports:
290
+ files_to_transform.add(imp.file_path)
291
+
292
+ results: list[TransformResult] = []
293
+
294
+ for file_path in files_to_transform:
295
+ try:
296
+ source_code = file_path.read_text()
297
+
298
+ # Select transformer based on library
299
+ transform_func = {
300
+ "pydantic": transform_pydantic_v1_to_v2,
301
+ "fastapi": transform_fastapi,
302
+ "sqlalchemy": transform_sqlalchemy,
303
+ "pandas": transform_pandas,
304
+ "requests": transform_requests,
305
+ }.get(library)
306
+
307
+ if transform_func:
308
+ transformed_code, changes = transform_func(source_code)
309
+ # Create TransformResult from the function output
310
+ result = TransformResult(
311
+ file_path=file_path,
312
+ status=TransformStatus.SUCCESS if changes else TransformStatus.NO_CHANGES,
313
+ original_code=source_code,
314
+ transformed_code=transformed_code,
315
+ changes=[
316
+ TransformChange(
317
+ description=c.description,
318
+ line_number=c.line_number,
319
+ original=c.original,
320
+ replacement=c.replacement,
321
+ transform_name=c.transform_name,
322
+ confidence=getattr(c, "confidence", 1.0),
323
+ )
324
+ for c in changes
325
+ ],
326
+ )
327
+ else:
328
+ console.print(f"[yellow]Warning:[/] No transformer available for {library}")
329
+ continue
330
+
331
+ if result.has_changes:
332
+ results.append(result)
333
+
334
+ except Exception as e:
335
+ console.print(f"[red]Error processing {file_path}:[/] {e}")
336
+
337
+ progress.update(task, completed=True)
338
+
339
+ # Step 5: Show results
340
+ if not results:
341
+ console.print(
342
+ f"\n[green]No changes needed![/] Your code appears to be compatible with {library} v{target}."
343
+ )
344
+ return
345
+
346
+ console.print("\n[bold]Proposed Changes[/]")
347
+
348
+ table = Table()
349
+ table.add_column("File", style="cyan")
350
+ table.add_column("Changes", justify="right")
351
+ table.add_column("Status", justify="center")
352
+
353
+ total_changes = 0
354
+ for result in results:
355
+ status_style = {
356
+ TransformStatus.SUCCESS: "[green]Ready[/]",
357
+ TransformStatus.PARTIAL: "[yellow]Partial[/]",
358
+ TransformStatus.FAILED: "[red]Failed[/]",
359
+ TransformStatus.NO_CHANGES: "[dim]No changes[/]",
360
+ }
361
+ # Handle files outside project path
362
+ try:
363
+ display_path = str(result.file_path.relative_to(project_path))
364
+ except ValueError:
365
+ display_path = str(result.file_path)
366
+ table.add_row(
367
+ display_path,
368
+ str(result.change_count),
369
+ status_style.get(result.status, "[dim]Unknown[/]"),
370
+ )
371
+ total_changes += result.change_count
372
+
373
+ console.print(table)
374
+ console.print(f"\nTotal: [cyan]{total_changes}[/] changes across [cyan]{len(results)}[/] files")
375
+
376
+ # Show detailed changes if verbose
377
+ if verbose:
378
+ console.print("\n[bold]Change Details[/]")
379
+ for result in results:
380
+ try:
381
+ display_path = str(result.file_path.relative_to(project_path))
382
+ except ValueError:
383
+ display_path = str(result.file_path)
384
+ console.print(f"\n[cyan]{display_path}[/]:")
385
+ for transform_change in result.changes:
386
+ console.print(f" • {transform_change.description}")
387
+ console.print(f" [red]- {transform_change.original}[/]")
388
+ console.print(f" [green]+ {transform_change.replacement}[/]")
389
+
390
+ # Save state
391
+ if not dry_run:
392
+ state = {
393
+ "library": library,
394
+ "target_version": target,
395
+ "project_path": str(project_path),
396
+ "results": [
397
+ {
398
+ "file_path": str(r.file_path),
399
+ "original_code": r.original_code,
400
+ "transformed_code": r.transformed_code,
401
+ "change_count": r.change_count,
402
+ "status": r.status.value,
403
+ "changes": [
404
+ {
405
+ "description": c.description,
406
+ "line_number": c.line_number,
407
+ "original": c.original,
408
+ "replacement": c.replacement,
409
+ "transform_name": c.transform_name,
410
+ }
411
+ for c in r.changes
412
+ ],
413
+ }
414
+ for r in results
415
+ ],
416
+ }
417
+ save_state(project_path, state)
418
+
419
+ # Record usage event
420
+ record_usage(
421
+ event_type="scan",
422
+ library=library,
423
+ quantity=1,
424
+ metadata={
425
+ "files_analyzed": len(files_to_transform),
426
+ "changes_proposed": total_changes,
427
+ "target_version": target,
428
+ },
429
+ )
430
+
431
+ console.print("\n[dim]State saved to .codeshift/state.json[/]")
432
+ console.print("\nNext steps:")
433
+ console.print(" [cyan]codeshift diff[/] - View detailed diff of proposed changes")
434
+ console.print(" [cyan]codeshift apply[/] - Apply changes to your files")
435
+ else:
436
+ console.print("\n[dim]Dry run mode - no state saved[/]")