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,352 @@
1
+ """Code scanner for finding library imports and usage."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+
6
+ import libcst as cst
7
+ from libcst.metadata import MetadataWrapper, PositionProvider
8
+
9
+
10
+ @dataclass
11
+ class ImportInfo:
12
+ """Information about an import statement."""
13
+
14
+ module: str # e.g., "pydantic"
15
+ names: list[str] # e.g., ["BaseModel", "Field"]
16
+ alias: str | None = None # e.g., "pd" for "import pandas as pd"
17
+ file_path: Path = field(default_factory=Path)
18
+ line_number: int = 0
19
+ is_from_import: bool = False # True for "from x import y"
20
+
21
+ @property
22
+ def full_import(self) -> str:
23
+ """Get the full import string."""
24
+ if self.is_from_import:
25
+ names_str = ", ".join(self.names)
26
+ return f"from {self.module} import {names_str}"
27
+ if self.alias:
28
+ return f"import {self.module} as {self.alias}"
29
+ return f"import {self.module}"
30
+
31
+
32
+ @dataclass
33
+ class UsageInfo:
34
+ """Information about a symbol usage."""
35
+
36
+ symbol: str # e.g., "BaseModel", "user.dict()"
37
+ usage_type: str # "class", "function", "method_call", "attribute", "decorator"
38
+ file_path: Path = field(default_factory=Path)
39
+ line_number: int = 0
40
+ column: int = 0
41
+ context: str = "" # Surrounding code for context
42
+
43
+ @property
44
+ def location(self) -> str:
45
+ """Get a human-readable location string."""
46
+ return f"{self.file_path}:{self.line_number}:{self.column}"
47
+
48
+
49
+ class ImportVisitor(cst.CSTVisitor):
50
+ """Visitor to collect import statements for a specific library."""
51
+
52
+ METADATA_DEPENDENCIES = (PositionProvider,)
53
+
54
+ def __init__(self, target_library: str):
55
+ self.target_library = target_library
56
+ self.imports: list[ImportInfo] = []
57
+ self._imported_names: set[str] = set()
58
+
59
+ def visit_Import(self, node: cst.Import) -> None:
60
+ """Visit import statements like 'import pydantic'."""
61
+ for name in node.names if isinstance(node.names, tuple) else []:
62
+ if isinstance(name, cst.ImportAlias):
63
+ module_name = self._get_name_value(name.name)
64
+ if module_name and module_name.startswith(self.target_library):
65
+ alias = None
66
+ if name.asname and isinstance(name.asname, cst.AsName):
67
+ alias = self._get_name_value(name.asname.name)
68
+
69
+ pos = self.get_metadata(PositionProvider, node)
70
+ self.imports.append(
71
+ ImportInfo(
72
+ module=module_name,
73
+ names=[module_name],
74
+ alias=alias,
75
+ line_number=pos.start.line if pos else 0,
76
+ is_from_import=False,
77
+ )
78
+ )
79
+ self._imported_names.add(alias or module_name)
80
+
81
+ def visit_ImportFrom(self, node: cst.ImportFrom) -> None:
82
+ """Visit from-import statements like 'from pydantic import BaseModel'."""
83
+ if node.module is None:
84
+ return
85
+
86
+ module_name = self._get_name_value(node.module)
87
+ if not module_name or not module_name.startswith(self.target_library):
88
+ return
89
+
90
+ names = []
91
+ if isinstance(node.names, cst.ImportStar):
92
+ names = ["*"]
93
+ self._imported_names.add("*")
94
+ elif isinstance(node.names, tuple):
95
+ for name in node.names:
96
+ if isinstance(name, cst.ImportAlias):
97
+ imported_name = self._get_name_value(name.name)
98
+ if imported_name:
99
+ names.append(imported_name)
100
+ if name.asname and isinstance(name.asname, cst.AsName):
101
+ alias = self._get_name_value(name.asname.name)
102
+ self._imported_names.add(alias or imported_name)
103
+ else:
104
+ self._imported_names.add(imported_name)
105
+
106
+ if names:
107
+ pos = self.get_metadata(PositionProvider, node)
108
+ self.imports.append(
109
+ ImportInfo(
110
+ module=module_name,
111
+ names=names,
112
+ line_number=pos.start.line if pos else 0,
113
+ is_from_import=True,
114
+ )
115
+ )
116
+
117
+ def _get_name_value(self, node: cst.BaseExpression) -> str | None:
118
+ """Extract the string value from a Name or Attribute node."""
119
+ if isinstance(node, cst.Name):
120
+ return str(node.value)
121
+ if isinstance(node, cst.Attribute):
122
+ base = self._get_name_value(node.value)
123
+ if base:
124
+ return f"{base}.{node.attr.value}"
125
+ return None
126
+
127
+ @property
128
+ def imported_names(self) -> set[str]:
129
+ """Get all imported names from the target library."""
130
+ return self._imported_names
131
+
132
+
133
+ class UsageVisitor(cst.CSTVisitor):
134
+ """Visitor to collect usage of imported symbols."""
135
+
136
+ METADATA_DEPENDENCIES = (PositionProvider,)
137
+
138
+ def __init__(self, imported_names: set[str], target_library: str):
139
+ self.imported_names = imported_names
140
+ self.target_library = target_library
141
+ self.usages: list[UsageInfo] = []
142
+
143
+ def visit_ClassDef(self, node: cst.ClassDef) -> None:
144
+ """Visit class definitions to find BaseModel subclasses."""
145
+ for base in node.bases:
146
+ base_name = self._get_name_value(base.value)
147
+ if base_name and base_name in self.imported_names:
148
+ pos = self.get_metadata(PositionProvider, node)
149
+ self.usages.append(
150
+ UsageInfo(
151
+ symbol=base_name,
152
+ usage_type="class_inheritance",
153
+ line_number=pos.start.line if pos else 0,
154
+ context=f"class {node.name.value}({base_name})",
155
+ )
156
+ )
157
+
158
+ def visit_Decorator(self, node: cst.Decorator) -> None:
159
+ """Visit decorators like @validator."""
160
+ decorator_name = self._get_name_value(node.decorator)
161
+ if not decorator_name:
162
+ # Handle decorator calls like @validator("field")
163
+ if isinstance(node.decorator, cst.Call):
164
+ decorator_name = self._get_name_value(node.decorator.func)
165
+
166
+ if decorator_name and decorator_name in self.imported_names:
167
+ pos = self.get_metadata(PositionProvider, node)
168
+ self.usages.append(
169
+ UsageInfo(
170
+ symbol=decorator_name,
171
+ usage_type="decorator",
172
+ line_number=pos.start.line if pos else 0,
173
+ context=f"@{decorator_name}",
174
+ )
175
+ )
176
+
177
+ def visit_Call(self, node: cst.Call) -> None:
178
+ """Visit function/method calls."""
179
+ # Handle method calls like .dict(), .json(), etc.
180
+ if isinstance(node.func, cst.Attribute):
181
+ method_name = node.func.attr.value
182
+ if method_name in {
183
+ "dict",
184
+ "json",
185
+ "schema",
186
+ "schema_json",
187
+ "parse_obj",
188
+ "parse_raw",
189
+ "parse_file",
190
+ "copy",
191
+ "update_forward_refs",
192
+ }:
193
+ pos = self.get_metadata(PositionProvider, node)
194
+ self.usages.append(
195
+ UsageInfo(
196
+ symbol=f".{method_name}()",
197
+ usage_type="method_call",
198
+ line_number=pos.start.line if pos else 0,
199
+ context=f".{method_name}()",
200
+ )
201
+ )
202
+
203
+ # Handle direct function calls like Field()
204
+ func_name = self._get_name_value(node.func)
205
+ if func_name and func_name in self.imported_names:
206
+ pos = self.get_metadata(PositionProvider, node)
207
+ self.usages.append(
208
+ UsageInfo(
209
+ symbol=func_name,
210
+ usage_type="function_call",
211
+ line_number=pos.start.line if pos else 0,
212
+ context=f"{func_name}()",
213
+ )
214
+ )
215
+
216
+ def visit_Attribute(self, node: cst.Attribute) -> None:
217
+ """Visit attribute access like __fields__."""
218
+ attr_name = node.attr.value
219
+ if attr_name in {"__fields__", "__validators__", "model_fields"}:
220
+ pos = self.get_metadata(PositionProvider, node)
221
+ self.usages.append(
222
+ UsageInfo(
223
+ symbol=attr_name,
224
+ usage_type="attribute",
225
+ line_number=pos.start.line if pos else 0,
226
+ context=attr_name,
227
+ )
228
+ )
229
+
230
+ def _get_name_value(self, node: cst.BaseExpression) -> str | None:
231
+ """Extract the string value from a Name or Attribute node."""
232
+ if isinstance(node, cst.Name):
233
+ return str(node.value)
234
+ if isinstance(node, cst.Attribute):
235
+ base = self._get_name_value(node.value)
236
+ if base:
237
+ return f"{base}.{node.attr.value}"
238
+ return None
239
+
240
+
241
+ @dataclass
242
+ class ScanResult:
243
+ """Result of scanning a codebase."""
244
+
245
+ imports: list[ImportInfo] = field(default_factory=list)
246
+ usages: list[UsageInfo] = field(default_factory=list)
247
+ files_scanned: int = 0
248
+ files_with_imports: int = 0
249
+ errors: list[tuple[Path, str]] = field(default_factory=list)
250
+
251
+ @property
252
+ def has_library_usage(self) -> bool:
253
+ """Check if the library is used in the codebase."""
254
+ return len(self.imports) > 0
255
+
256
+
257
+ class CodeScanner:
258
+ """Scanner for finding library usage in Python code."""
259
+
260
+ def __init__(self, target_library: str, exclude_patterns: list[str] | None = None):
261
+ """Initialize the scanner.
262
+
263
+ Args:
264
+ target_library: Name of the library to scan for (e.g., "pydantic")
265
+ exclude_patterns: Glob patterns for paths to exclude
266
+ """
267
+ self.target_library = target_library
268
+ self.exclude_patterns = exclude_patterns or []
269
+
270
+ def scan_file(self, file_path: Path) -> tuple[list[ImportInfo], list[UsageInfo]]:
271
+ """Scan a single file for library usage.
272
+
273
+ Args:
274
+ file_path: Path to the Python file
275
+
276
+ Returns:
277
+ Tuple of (imports, usages) found in the file
278
+
279
+ Raises:
280
+ SyntaxError: If the file has invalid Python syntax
281
+ """
282
+ source_code = file_path.read_text()
283
+
284
+ try:
285
+ tree = cst.parse_module(source_code)
286
+ except cst.ParserSyntaxError as e:
287
+ raise SyntaxError(f"Invalid Python syntax in {file_path}: {e}") from e
288
+
289
+ wrapper = MetadataWrapper(tree)
290
+
291
+ # First pass: find imports
292
+ import_visitor = ImportVisitor(self.target_library)
293
+ wrapper.visit(import_visitor)
294
+
295
+ # Update file paths in imports
296
+ for imp in import_visitor.imports:
297
+ imp.file_path = file_path
298
+
299
+ # Second pass: find usages (only if we have imports)
300
+ usages = []
301
+ if import_visitor.imported_names:
302
+ usage_visitor = UsageVisitor(import_visitor.imported_names, self.target_library)
303
+ wrapper.visit(usage_visitor)
304
+ for usage in usage_visitor.usages:
305
+ usage.file_path = file_path
306
+ usages = usage_visitor.usages
307
+
308
+ return import_visitor.imports, usages
309
+
310
+ def scan_directory(self, directory: Path) -> ScanResult:
311
+ """Scan a directory for library usage.
312
+
313
+ Args:
314
+ directory: Path to the directory to scan
315
+
316
+ Returns:
317
+ ScanResult containing all imports and usages found
318
+ """
319
+ result = ScanResult()
320
+
321
+ # Find all Python files
322
+ python_files = list(directory.rglob("*.py"))
323
+
324
+ for file_path in python_files:
325
+ # Check exclude patterns
326
+ relative_path = str(file_path.relative_to(directory))
327
+ if self._should_exclude(relative_path):
328
+ continue
329
+
330
+ result.files_scanned += 1
331
+
332
+ try:
333
+ imports, usages = self.scan_file(file_path)
334
+ if imports:
335
+ result.files_with_imports += 1
336
+ result.imports.extend(imports)
337
+ result.usages.extend(usages)
338
+ except SyntaxError as e:
339
+ result.errors.append((file_path, str(e)))
340
+ except Exception as e:
341
+ result.errors.append((file_path, f"Error scanning file: {e}"))
342
+
343
+ return result
344
+
345
+ def _should_exclude(self, path: str) -> bool:
346
+ """Check if a path should be excluded based on patterns."""
347
+ import fnmatch
348
+
349
+ for pattern in self.exclude_patterns:
350
+ if fnmatch.fnmatch(path, pattern):
351
+ return True
352
+ return False