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,388 @@
1
+ """Source fetchers for changelog and migration guide discovery."""
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from urllib.parse import urlparse
6
+
7
+ import httpx
8
+
9
+ from codeshift.knowledge.models import ChangelogSource
10
+
11
+
12
+ @dataclass
13
+ class PackageInfo:
14
+ """Package information from PyPI."""
15
+
16
+ name: str
17
+ version: str
18
+ home_page: str | None = None
19
+ project_url: str | None = None
20
+ repository_url: str | None = None
21
+ documentation_url: str | None = None
22
+
23
+ @property
24
+ def github_url(self) -> str | None:
25
+ """Extract GitHub repository URL."""
26
+ for url in [self.repository_url, self.project_url, self.home_page]:
27
+ if url and "github.com" in url:
28
+ # Normalize to https://github.com/owner/repo format
29
+ parsed = urlparse(url)
30
+ if parsed.netloc == "github.com":
31
+ path_parts = parsed.path.strip("/").split("/")
32
+ if len(path_parts) >= 2:
33
+ return f"https://github.com/{path_parts[0]}/{path_parts[1]}"
34
+ return None
35
+
36
+
37
+ class SourceFetcher:
38
+ """Fetches changelog and migration guide sources from various locations."""
39
+
40
+ CHANGELOG_FILENAMES = [
41
+ "CHANGELOG.md",
42
+ "CHANGELOG.rst",
43
+ "CHANGELOG.txt",
44
+ "CHANGELOG",
45
+ "CHANGES.md",
46
+ "CHANGES.rst",
47
+ "CHANGES.txt",
48
+ "CHANGES",
49
+ "HISTORY.md",
50
+ "HISTORY.rst",
51
+ "NEWS.md",
52
+ "NEWS.rst",
53
+ "NEWS",
54
+ ]
55
+
56
+ MIGRATION_GUIDE_PATTERNS = [
57
+ "docs/migration",
58
+ "docs/upgrading",
59
+ "docs/upgrade",
60
+ "migration",
61
+ "MIGRATION.md",
62
+ "UPGRADING.md",
63
+ ]
64
+
65
+ def __init__(self, timeout: float = 30.0):
66
+ """Initialize the fetcher.
67
+
68
+ Args:
69
+ timeout: HTTP request timeout in seconds.
70
+ """
71
+ self.timeout = timeout
72
+ self._client: httpx.Client | None = None
73
+
74
+ @property
75
+ def client(self) -> httpx.Client:
76
+ """Get or create HTTP client."""
77
+ if self._client is None:
78
+ self._client = httpx.Client(
79
+ timeout=self.timeout,
80
+ follow_redirects=True,
81
+ headers={"Accept": "application/vnd.github.v3.raw"},
82
+ )
83
+ return self._client
84
+
85
+ def close(self) -> None:
86
+ """Close the HTTP client."""
87
+ if self._client:
88
+ self._client.close()
89
+ self._client = None
90
+
91
+ def get_package_info(self, package: str) -> PackageInfo | None:
92
+ """Fetch package information from PyPI.
93
+
94
+ Args:
95
+ package: Package name.
96
+
97
+ Returns:
98
+ PackageInfo or None if not found.
99
+ """
100
+ try:
101
+ response = self.client.get(f"https://pypi.org/pypi/{package}/json")
102
+ response.raise_for_status()
103
+ data = response.json()
104
+
105
+ info = data.get("info", {})
106
+ project_urls = info.get("project_urls") or {}
107
+
108
+ return PackageInfo(
109
+ name=info.get("name", package),
110
+ version=info.get("version", ""),
111
+ home_page=info.get("home_page"),
112
+ project_url=info.get("project_url"),
113
+ repository_url=project_urls.get("Repository")
114
+ or project_urls.get("Source")
115
+ or project_urls.get("GitHub"),
116
+ documentation_url=project_urls.get("Documentation") or project_urls.get("Docs"),
117
+ )
118
+ except Exception:
119
+ return None
120
+
121
+ def fetch_github_file(self, repo_url: str, file_path: str, branch: str = "main") -> str | None:
122
+ """Fetch a file from a GitHub repository.
123
+
124
+ Args:
125
+ repo_url: GitHub repository URL (https://github.com/owner/repo).
126
+ file_path: Path to the file within the repo.
127
+ branch: Branch to fetch from.
128
+
129
+ Returns:
130
+ File content or None if not found.
131
+ """
132
+ # Extract owner/repo from URL
133
+ parsed = urlparse(repo_url)
134
+ path_parts = parsed.path.strip("/").split("/")
135
+ if len(path_parts) < 2:
136
+ return None
137
+
138
+ owner, repo = path_parts[0], path_parts[1]
139
+
140
+ # Try raw GitHub content URL
141
+ raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{file_path}"
142
+
143
+ try:
144
+ response = self.client.get(raw_url)
145
+ if response.status_code == 200:
146
+ return str(response.text)
147
+ except Exception:
148
+ pass
149
+
150
+ # Try master branch if main failed
151
+ if branch == "main":
152
+ return self.fetch_github_file(repo_url, file_path, branch="master")
153
+
154
+ return None
155
+
156
+ def fetch_changelog(self, repo_url: str) -> ChangelogSource | None:
157
+ """Fetch changelog from a GitHub repository.
158
+
159
+ Args:
160
+ repo_url: GitHub repository URL.
161
+
162
+ Returns:
163
+ ChangelogSource or None if not found.
164
+ """
165
+ for filename in self.CHANGELOG_FILENAMES:
166
+ content = self.fetch_github_file(repo_url, filename)
167
+ if content:
168
+ return ChangelogSource(
169
+ url=f"{repo_url}/blob/main/{filename}",
170
+ source_type="changelog",
171
+ content=content,
172
+ )
173
+ return None
174
+
175
+ def fetch_migration_guide(self, repo_url: str) -> ChangelogSource | None:
176
+ """Fetch migration guide from a GitHub repository.
177
+
178
+ Args:
179
+ repo_url: GitHub repository URL.
180
+
181
+ Returns:
182
+ ChangelogSource or None if not found.
183
+ """
184
+ for pattern in self.MIGRATION_GUIDE_PATTERNS:
185
+ # Try common file extensions
186
+ for ext in [".md", ".rst", ".txt", ""]:
187
+ path = (
188
+ f"{pattern}{ext}" if not pattern.endswith((".md", ".rst", ".txt")) else pattern
189
+ )
190
+ content = self.fetch_github_file(repo_url, path)
191
+ if content:
192
+ return ChangelogSource(
193
+ url=f"{repo_url}/blob/main/{path}",
194
+ source_type="migration_guide",
195
+ content=content,
196
+ )
197
+ return None
198
+
199
+ def fetch_release_notes(self, repo_url: str, version: str) -> ChangelogSource | None:
200
+ """Fetch release notes for a specific version from GitHub releases.
201
+
202
+ Args:
203
+ repo_url: GitHub repository URL.
204
+ version: Version to fetch release notes for.
205
+
206
+ Returns:
207
+ ChangelogSource or None if not found.
208
+ """
209
+ parsed = urlparse(repo_url)
210
+ path_parts = parsed.path.strip("/").split("/")
211
+ if len(path_parts) < 2:
212
+ return None
213
+
214
+ owner, repo = path_parts[0], path_parts[1]
215
+
216
+ # Try GitHub API for releases
217
+ api_url = f"https://api.github.com/repos/{owner}/{repo}/releases"
218
+
219
+ try:
220
+ response = self.client.get(api_url)
221
+ if response.status_code != 200:
222
+ return None
223
+
224
+ releases = response.json()
225
+
226
+ # Find matching release
227
+ version_patterns = [
228
+ f"v{version}",
229
+ version,
230
+ f"release-{version}",
231
+ ]
232
+
233
+ for release in releases:
234
+ tag = release.get("tag_name", "")
235
+ for pattern in version_patterns:
236
+ if tag == pattern or tag.startswith(pattern):
237
+ body = release.get("body", "")
238
+ if body:
239
+ return ChangelogSource(
240
+ url=release.get("html_url", repo_url),
241
+ source_type="release_notes",
242
+ content=body,
243
+ version_range=(version, version),
244
+ )
245
+ break
246
+
247
+ except Exception:
248
+ pass
249
+
250
+ return None
251
+
252
+ async def discover_sources(
253
+ self, package: str, target_version: str | None = None
254
+ ) -> list[ChangelogSource]:
255
+ """Discover all available changelog sources for a package.
256
+
257
+ Args:
258
+ package: Package name.
259
+ target_version: Optional target version for release notes.
260
+
261
+ Returns:
262
+ List of discovered ChangelogSources.
263
+ """
264
+ # This is a sync wrapper - the async signature matches the architecture doc
265
+ return self.discover_sources_sync(package, target_version)
266
+
267
+ def discover_sources_sync(
268
+ self, package: str, target_version: str | None = None
269
+ ) -> list[ChangelogSource]:
270
+ """Synchronously discover all available changelog sources for a package.
271
+
272
+ Args:
273
+ package: Package name.
274
+ target_version: Optional target version for release notes.
275
+
276
+ Returns:
277
+ List of discovered ChangelogSources.
278
+ """
279
+ sources: list[ChangelogSource] = []
280
+
281
+ # Get package info from PyPI
282
+ pkg_info = self.get_package_info(package)
283
+ if not pkg_info:
284
+ return sources
285
+
286
+ github_url = pkg_info.github_url
287
+ if not github_url:
288
+ return sources
289
+
290
+ # Fetch changelog
291
+ changelog = self.fetch_changelog(github_url)
292
+ if changelog:
293
+ sources.append(changelog)
294
+
295
+ # Fetch migration guide
296
+ migration_guide = self.fetch_migration_guide(github_url)
297
+ if migration_guide:
298
+ sources.append(migration_guide)
299
+
300
+ # Fetch release notes for target version
301
+ if target_version:
302
+ release_notes = self.fetch_release_notes(github_url, target_version)
303
+ if release_notes:
304
+ sources.append(release_notes)
305
+
306
+ return sources
307
+
308
+ def extract_version_changelog(
309
+ self,
310
+ changelog_content: str,
311
+ from_version: str,
312
+ to_version: str,
313
+ ) -> str:
314
+ """Extract the relevant portion of a changelog between two versions.
315
+
316
+ Args:
317
+ changelog_content: Full changelog content.
318
+ from_version: Starting version.
319
+ to_version: Target version.
320
+
321
+ Returns:
322
+ Extracted changelog content for the version range.
323
+ """
324
+ lines = changelog_content.split("\n")
325
+ result_lines = []
326
+ in_range = False
327
+ found_start = False
328
+
329
+ # Common version header patterns
330
+ version_pattern = re.compile(
331
+ r"^#+\s*\[?v?(\d+\.\d+(?:\.\d+)?)\]?|" # ## [1.0.0] or ## v1.0.0
332
+ r"^v?(\d+\.\d+(?:\.\d+)?)\s*[-–—]|" # 1.0.0 - or v1.0.0 -
333
+ r"^v?(\d+\.\d+(?:\.\d+)?)\s*\(", # 1.0.0 (date)
334
+ re.IGNORECASE,
335
+ )
336
+
337
+ for line in lines:
338
+ match = version_pattern.match(line)
339
+ if match:
340
+ # Extract version number
341
+ version = match.group(1) or match.group(2) or match.group(3)
342
+ if version:
343
+ # Check if this is our target version or later
344
+ if self._compare_versions(version, to_version) <= 0:
345
+ if not found_start:
346
+ in_range = True
347
+ found_start = True
348
+
349
+ # Check if we've gone past the from_version
350
+ if self._compare_versions(version, from_version) < 0:
351
+ in_range = False
352
+
353
+ if in_range:
354
+ result_lines.append(line)
355
+
356
+ return "\n".join(result_lines)
357
+
358
+ def _compare_versions(self, v1: str, v2: str) -> int:
359
+ """Compare two version strings.
360
+
361
+ Returns:
362
+ -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2.
363
+ """
364
+ from packaging.version import Version
365
+
366
+ try:
367
+ ver1 = Version(v1)
368
+ ver2 = Version(v2)
369
+ if ver1 < ver2:
370
+ return -1
371
+ elif ver1 > ver2:
372
+ return 1
373
+ return 0
374
+ except Exception:
375
+ # Fallback to string comparison
376
+ return (v1 > v2) - (v1 < v2)
377
+
378
+
379
+ # Singleton instance
380
+ _default_fetcher: SourceFetcher | None = None
381
+
382
+
383
+ def get_source_fetcher() -> SourceFetcher:
384
+ """Get the default source fetcher instance."""
385
+ global _default_fetcher
386
+ if _default_fetcher is None:
387
+ _default_fetcher = SourceFetcher()
388
+ return _default_fetcher
@@ -0,0 +1,17 @@
1
+ """Knowledge base module for breaking change definitions."""
2
+
3
+ from codeshift.knowledge_base.loader import KnowledgeBaseLoader
4
+ from codeshift.knowledge_base.models import (
5
+ BreakingChange,
6
+ ChangeType,
7
+ LibraryKnowledge,
8
+ Severity,
9
+ )
10
+
11
+ __all__ = [
12
+ "KnowledgeBaseLoader",
13
+ "BreakingChange",
14
+ "ChangeType",
15
+ "Severity",
16
+ "LibraryKnowledge",
17
+ ]
@@ -0,0 +1,102 @@
1
+ """Loader for the knowledge base YAML files."""
2
+
3
+ from pathlib import Path
4
+
5
+ import yaml
6
+
7
+ from codeshift.knowledge_base.models import LibraryKnowledge
8
+
9
+
10
+ class KnowledgeBaseLoader:
11
+ """Loads and manages library knowledge bases."""
12
+
13
+ def __init__(self, knowledge_base_dir: Path | None = None):
14
+ """Initialize the loader.
15
+
16
+ Args:
17
+ knowledge_base_dir: Directory containing the YAML files.
18
+ Defaults to the 'libraries' subdirectory.
19
+ """
20
+ if knowledge_base_dir is None:
21
+ knowledge_base_dir = Path(__file__).parent / "libraries"
22
+ self.knowledge_base_dir = knowledge_base_dir
23
+ self._cache: dict[str, LibraryKnowledge] = {}
24
+
25
+ def get_supported_libraries(self) -> list[str]:
26
+ """Get a list of all supported library names."""
27
+ libraries = []
28
+ if self.knowledge_base_dir.exists():
29
+ for yaml_file in self.knowledge_base_dir.glob("*.yaml"):
30
+ libraries.append(yaml_file.stem)
31
+ return sorted(libraries)
32
+
33
+ def load(self, library_name: str) -> LibraryKnowledge:
34
+ """Load the knowledge base for a specific library.
35
+
36
+ Args:
37
+ library_name: Name of the library (e.g., "pydantic")
38
+
39
+ Returns:
40
+ LibraryKnowledge object containing all breaking change info
41
+
42
+ Raises:
43
+ FileNotFoundError: If the knowledge base file doesn't exist
44
+ ValueError: If the YAML file is invalid
45
+ """
46
+ if library_name in self._cache:
47
+ return self._cache[library_name]
48
+
49
+ yaml_path = self.knowledge_base_dir / f"{library_name}.yaml"
50
+ if not yaml_path.exists():
51
+ raise FileNotFoundError(
52
+ f"No knowledge base found for library '{library_name}'. "
53
+ f"Available libraries: {', '.join(self.get_supported_libraries())}"
54
+ )
55
+
56
+ try:
57
+ with open(yaml_path) as f:
58
+ data = yaml.safe_load(f)
59
+ except yaml.YAMLError as e:
60
+ raise ValueError(f"Invalid YAML in knowledge base for '{library_name}': {e}") from e
61
+
62
+ if not isinstance(data, dict):
63
+ raise ValueError(f"Knowledge base for '{library_name}' must be a dictionary")
64
+
65
+ knowledge = LibraryKnowledge.from_dict(data)
66
+ self._cache[library_name] = knowledge
67
+ return knowledge
68
+
69
+ def is_migration_supported(self, library_name: str, from_version: str, to_version: str) -> bool:
70
+ """Check if a specific migration path is supported.
71
+
72
+ Args:
73
+ library_name: Name of the library
74
+ from_version: Starting version
75
+ to_version: Target version
76
+
77
+ Returns:
78
+ True if the migration path is supported
79
+ """
80
+ try:
81
+ knowledge = self.load(library_name)
82
+ except FileNotFoundError:
83
+ return False
84
+
85
+ from packaging.version import Version
86
+
87
+ from_v = Version(from_version)
88
+ to_v = Version(to_version)
89
+
90
+ for supported_from, supported_to in knowledge.supported_migrations:
91
+ supported_from_v = Version(supported_from)
92
+ supported_to_v = Version(supported_to)
93
+
94
+ # Check if requested migration falls within a supported range
95
+ if from_v >= supported_from_v and to_v <= supported_to_v:
96
+ return True
97
+
98
+ return False
99
+
100
+ def clear_cache(self) -> None:
101
+ """Clear the cached knowledge bases."""
102
+ self._cache.clear()
@@ -0,0 +1,110 @@
1
+ """Data models for the knowledge base."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+
6
+
7
+ class ChangeType(Enum):
8
+ """Types of breaking changes."""
9
+
10
+ RENAMED = "renamed"
11
+ REMOVED = "removed"
12
+ SIGNATURE_CHANGED = "signature_changed"
13
+ BEHAVIOR_CHANGED = "behavior_changed"
14
+ DEPRECATED = "deprecated"
15
+ TYPE_CHANGED = "type_changed"
16
+
17
+
18
+ class Severity(Enum):
19
+ """Severity levels for breaking changes."""
20
+
21
+ LOW = "low"
22
+ MEDIUM = "medium"
23
+ HIGH = "high"
24
+ CRITICAL = "critical"
25
+
26
+
27
+ @dataclass
28
+ class BreakingChange:
29
+ """Represents a single breaking change in a library."""
30
+
31
+ symbol: str # e.g., "BaseModel.Config", "@validator", ".dict()"
32
+ change_type: ChangeType
33
+ severity: Severity
34
+ from_version: str
35
+ to_version: str
36
+ description: str
37
+ replacement: str | None = None # e.g., "model_config = ConfigDict(...)"
38
+ has_deterministic_transform: bool = False
39
+ transform_name: str | None = None # e.g., "config_to_configdict"
40
+ migration_guide_url: str | None = None
41
+ notes: str | None = None
42
+
43
+ @classmethod
44
+ def from_dict(cls, data: dict) -> "BreakingChange":
45
+ """Create a BreakingChange from a dictionary."""
46
+ return cls(
47
+ symbol=data["symbol"],
48
+ change_type=ChangeType(data["change_type"]),
49
+ severity=Severity(data["severity"]),
50
+ from_version=data["from_version"],
51
+ to_version=data["to_version"],
52
+ description=data["description"],
53
+ replacement=data.get("replacement"),
54
+ has_deterministic_transform=data.get("has_deterministic_transform", False),
55
+ transform_name=data.get("transform_name"),
56
+ migration_guide_url=data.get("migration_guide_url"),
57
+ notes=data.get("notes"),
58
+ )
59
+
60
+
61
+ @dataclass
62
+ class LibraryKnowledge:
63
+ """Knowledge about a library's breaking changes."""
64
+
65
+ name: str
66
+ display_name: str
67
+ description: str
68
+ migration_guide_url: str | None
69
+ supported_migrations: list[tuple[str, str]] # List of (from_version, to_version)
70
+ breaking_changes: list[BreakingChange] = field(default_factory=list)
71
+
72
+ @classmethod
73
+ def from_dict(cls, data: dict) -> "LibraryKnowledge":
74
+ """Create a LibraryKnowledge from a dictionary."""
75
+ breaking_changes = [BreakingChange.from_dict(bc) for bc in data.get("breaking_changes", [])]
76
+ supported_migrations = [(m["from"], m["to"]) for m in data.get("supported_migrations", [])]
77
+
78
+ return cls(
79
+ name=data["name"],
80
+ display_name=data["display_name"],
81
+ description=data.get("description", ""),
82
+ migration_guide_url=data.get("migration_guide_url"),
83
+ supported_migrations=supported_migrations,
84
+ breaking_changes=breaking_changes,
85
+ )
86
+
87
+ def get_changes_for_migration(self, from_version: str, to_version: str) -> list[BreakingChange]:
88
+ """Get all breaking changes relevant to a specific migration."""
89
+ from packaging.version import Version
90
+
91
+ from_v = Version(from_version)
92
+ to_v = Version(to_version)
93
+
94
+ relevant = []
95
+ for change in self.breaking_changes:
96
+ change_from = Version(change.from_version)
97
+ change_to = Version(change.to_version)
98
+
99
+ # Include if the change affects versions between from and to
100
+ if change_from >= from_v and change_to <= to_v:
101
+ relevant.append(change)
102
+
103
+ return relevant
104
+
105
+ def get_deterministic_transforms(
106
+ self, from_version: str, to_version: str
107
+ ) -> list[BreakingChange]:
108
+ """Get all breaking changes that have deterministic transforms."""
109
+ changes = self.get_changes_for_migration(from_version, to_version)
110
+ return [c for c in changes if c.has_deterministic_transform]
@@ -0,0 +1,23 @@
1
+ """Migrator module for transforming code."""
2
+
3
+ from codeshift.migrator.ast_transforms import (
4
+ BaseTransformer,
5
+ TransformChange,
6
+ TransformResult,
7
+ TransformStatus,
8
+ )
9
+ from codeshift.migrator.engine import (
10
+ MigrationEngine,
11
+ get_migration_engine,
12
+ run_migration,
13
+ )
14
+
15
+ __all__ = [
16
+ "BaseTransformer",
17
+ "TransformChange",
18
+ "TransformResult",
19
+ "TransformStatus",
20
+ "MigrationEngine",
21
+ "get_migration_engine",
22
+ "run_migration",
23
+ ]