codeshift 0.4.0__py3-none-any.whl → 0.7.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 (32) hide show
  1. codeshift/__init__.py +1 -1
  2. codeshift/cli/commands/auth.py +41 -25
  3. codeshift/cli/commands/health.py +244 -0
  4. codeshift/cli/commands/upgrade.py +68 -55
  5. codeshift/cli/main.py +2 -0
  6. codeshift/health/__init__.py +50 -0
  7. codeshift/health/calculator.py +217 -0
  8. codeshift/health/metrics/__init__.py +63 -0
  9. codeshift/health/metrics/documentation.py +209 -0
  10. codeshift/health/metrics/freshness.py +180 -0
  11. codeshift/health/metrics/migration_readiness.py +142 -0
  12. codeshift/health/metrics/security.py +225 -0
  13. codeshift/health/metrics/test_coverage.py +191 -0
  14. codeshift/health/models.py +284 -0
  15. codeshift/health/report.py +310 -0
  16. codeshift/knowledge/generator.py +6 -0
  17. codeshift/knowledge_base/libraries/aiohttp.yaml +3 -3
  18. codeshift/knowledge_base/libraries/httpx.yaml +4 -4
  19. codeshift/knowledge_base/libraries/pytest.yaml +1 -1
  20. codeshift/knowledge_base/models.py +1 -0
  21. codeshift/migrator/transforms/marshmallow_transformer.py +50 -0
  22. codeshift/migrator/transforms/pydantic_v1_to_v2.py +191 -22
  23. codeshift/scanner/code_scanner.py +22 -2
  24. codeshift/utils/api_client.py +144 -4
  25. codeshift/utils/credential_store.py +393 -0
  26. codeshift/utils/llm_client.py +111 -9
  27. {codeshift-0.4.0.dist-info → codeshift-0.7.0.dist-info}/METADATA +4 -1
  28. {codeshift-0.4.0.dist-info → codeshift-0.7.0.dist-info}/RECORD +32 -20
  29. {codeshift-0.4.0.dist-info → codeshift-0.7.0.dist-info}/WHEEL +0 -0
  30. {codeshift-0.4.0.dist-info → codeshift-0.7.0.dist-info}/entry_points.txt +0 -0
  31. {codeshift-0.4.0.dist-info → codeshift-0.7.0.dist-info}/licenses/LICENSE +0 -0
  32. {codeshift-0.4.0.dist-info → codeshift-0.7.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,284 @@
1
+ """Data models for the health score feature."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ class MetricCategory(Enum):
11
+ """Categories of health metrics."""
12
+
13
+ FRESHNESS = "freshness"
14
+ SECURITY = "security"
15
+ MIGRATION_READINESS = "migration_readiness"
16
+ TEST_COVERAGE = "test_coverage"
17
+ DOCUMENTATION = "documentation"
18
+
19
+
20
+ class HealthGrade(Enum):
21
+ """Letter grade for overall health score."""
22
+
23
+ A = "A" # 90-100
24
+ B = "B" # 80-89
25
+ C = "C" # 70-79
26
+ D = "D" # 60-69
27
+ F = "F" # Below 60
28
+
29
+ @classmethod
30
+ def from_score(cls, score: float) -> "HealthGrade":
31
+ """Convert a numeric score to a letter grade.
32
+
33
+ Args:
34
+ score: Numeric score from 0-100
35
+
36
+ Returns:
37
+ Corresponding letter grade
38
+ """
39
+ if score >= 90:
40
+ return cls.A
41
+ elif score >= 80:
42
+ return cls.B
43
+ elif score >= 70:
44
+ return cls.C
45
+ elif score >= 60:
46
+ return cls.D
47
+ else:
48
+ return cls.F
49
+
50
+ @property
51
+ def color(self) -> str:
52
+ """Get the display color for this grade."""
53
+ colors = {
54
+ HealthGrade.A: "green",
55
+ HealthGrade.B: "cyan",
56
+ HealthGrade.C: "yellow",
57
+ HealthGrade.D: "orange1",
58
+ HealthGrade.F: "red",
59
+ }
60
+ return colors.get(self, "white")
61
+
62
+ @property
63
+ def emoji(self) -> str:
64
+ """Get the emoji for this grade."""
65
+ emojis = {
66
+ HealthGrade.A: "🟢",
67
+ HealthGrade.B: "🔵",
68
+ HealthGrade.C: "🟡",
69
+ HealthGrade.D: "🟠",
70
+ HealthGrade.F: "🔴",
71
+ }
72
+ return emojis.get(self, "⚪")
73
+
74
+
75
+ class VulnerabilitySeverity(Enum):
76
+ """Severity levels for security vulnerabilities."""
77
+
78
+ CRITICAL = "critical"
79
+ HIGH = "high"
80
+ MEDIUM = "medium"
81
+ LOW = "low"
82
+
83
+ @property
84
+ def penalty(self) -> int:
85
+ """Get the score penalty for this severity level."""
86
+ penalties = {
87
+ VulnerabilitySeverity.CRITICAL: 25,
88
+ VulnerabilitySeverity.HIGH: 15,
89
+ VulnerabilitySeverity.MEDIUM: 8,
90
+ VulnerabilitySeverity.LOW: 3,
91
+ }
92
+ return penalties.get(self, 0)
93
+
94
+
95
+ @dataclass
96
+ class SecurityVulnerability:
97
+ """Represents a security vulnerability in a dependency."""
98
+
99
+ package: str
100
+ vulnerability_id: str
101
+ severity: VulnerabilitySeverity
102
+ description: str
103
+ fixed_in: str | None = None
104
+ url: str | None = None
105
+
106
+ def to_dict(self) -> dict[str, Any]:
107
+ """Convert to dictionary."""
108
+ return {
109
+ "package": self.package,
110
+ "vulnerability_id": self.vulnerability_id,
111
+ "severity": self.severity.value,
112
+ "description": self.description,
113
+ "fixed_in": self.fixed_in,
114
+ "url": self.url,
115
+ }
116
+
117
+
118
+ @dataclass
119
+ class DependencyHealth:
120
+ """Health information for a single dependency."""
121
+
122
+ name: str
123
+ current_version: str | None
124
+ latest_version: str | None
125
+ is_outdated: bool
126
+ major_versions_behind: int = 0
127
+ minor_versions_behind: int = 0
128
+ has_tier1_support: bool = False
129
+ has_tier2_support: bool = False
130
+ vulnerabilities: list[SecurityVulnerability] = field(default_factory=list)
131
+
132
+ @property
133
+ def version_lag_penalty(self) -> int:
134
+ """Calculate the penalty for version lag."""
135
+ # Major version lag: -15 points each
136
+ # Minor version lag: -5 points each (max 3)
137
+ major_penalty = self.major_versions_behind * 15
138
+ minor_penalty = min(self.minor_versions_behind, 3) * 5
139
+ return major_penalty + minor_penalty
140
+
141
+ def to_dict(self) -> dict[str, Any]:
142
+ """Convert to dictionary."""
143
+ return {
144
+ "name": self.name,
145
+ "current_version": self.current_version,
146
+ "latest_version": self.latest_version,
147
+ "is_outdated": self.is_outdated,
148
+ "major_versions_behind": self.major_versions_behind,
149
+ "minor_versions_behind": self.minor_versions_behind,
150
+ "has_tier1_support": self.has_tier1_support,
151
+ "has_tier2_support": self.has_tier2_support,
152
+ "vulnerabilities": [v.to_dict() for v in self.vulnerabilities],
153
+ }
154
+
155
+
156
+ @dataclass
157
+ class MetricResult:
158
+ """Result from a single metric calculation."""
159
+
160
+ category: MetricCategory
161
+ score: float # 0-100
162
+ weight: float # 0.0-1.0
163
+ description: str
164
+ details: dict[str, Any] = field(default_factory=dict)
165
+ recommendations: list[str] = field(default_factory=list)
166
+
167
+ @property
168
+ def weighted_score(self) -> float:
169
+ """Calculate the weighted score contribution."""
170
+ return self.score * self.weight
171
+
172
+ def to_dict(self) -> dict[str, Any]:
173
+ """Convert to dictionary."""
174
+ return {
175
+ "category": self.category.value,
176
+ "score": self.score,
177
+ "weight": self.weight,
178
+ "weighted_score": self.weighted_score,
179
+ "description": self.description,
180
+ "details": self.details,
181
+ "recommendations": self.recommendations,
182
+ }
183
+
184
+
185
+ @dataclass
186
+ class HealthScore:
187
+ """Complete health score for a project."""
188
+
189
+ overall_score: float # 0-100
190
+ grade: HealthGrade
191
+ metrics: list[MetricResult] = field(default_factory=list)
192
+ dependencies: list[DependencyHealth] = field(default_factory=list)
193
+ vulnerabilities: list[SecurityVulnerability] = field(default_factory=list)
194
+ calculated_at: datetime = field(default_factory=datetime.now)
195
+ project_path: Path = field(default_factory=lambda: Path("."))
196
+
197
+ @property
198
+ def summary(self) -> str:
199
+ """Get a summary string of the health score."""
200
+ return f"{self.grade.emoji} Grade {self.grade.value} ({self.overall_score:.1f}/100)"
201
+
202
+ @property
203
+ def top_recommendations(self) -> list[str]:
204
+ """Get the top 5 recommendations across all metrics."""
205
+ all_recs: list[tuple[float, str]] = []
206
+ for metric in self.metrics:
207
+ # Weight recommendations by how much improvement they could provide
208
+ improvement_potential = 100 - metric.score
209
+ for rec in metric.recommendations:
210
+ all_recs.append((improvement_potential * metric.weight, rec))
211
+
212
+ # Sort by improvement potential and return top 5
213
+ all_recs.sort(key=lambda x: x[0], reverse=True)
214
+ seen: set[str] = set()
215
+ unique_recs: list[str] = []
216
+ for _, rec in all_recs:
217
+ if rec not in seen:
218
+ seen.add(rec)
219
+ unique_recs.append(rec)
220
+ if len(unique_recs) >= 5:
221
+ break
222
+ return unique_recs
223
+
224
+ def to_dict(self) -> dict[str, Any]:
225
+ """Convert to dictionary."""
226
+ return {
227
+ "overall_score": self.overall_score,
228
+ "grade": self.grade.value,
229
+ "metrics": [m.to_dict() for m in self.metrics],
230
+ "dependencies": [d.to_dict() for d in self.dependencies],
231
+ "vulnerabilities": [v.to_dict() for v in self.vulnerabilities],
232
+ "calculated_at": self.calculated_at.isoformat(),
233
+ "project_path": str(self.project_path),
234
+ "recommendations": self.top_recommendations,
235
+ }
236
+
237
+
238
+ @dataclass
239
+ class HealthReport:
240
+ """Health report comparing current score to previous."""
241
+
242
+ current: HealthScore
243
+ previous: HealthScore | None = None
244
+
245
+ @property
246
+ def trend(self) -> str:
247
+ """Get the trend direction."""
248
+ if self.previous is None:
249
+ return "new"
250
+
251
+ diff = self.current.overall_score - self.previous.overall_score
252
+ if diff > 2:
253
+ return "improving"
254
+ elif diff < -2:
255
+ return "declining"
256
+ else:
257
+ return "stable"
258
+
259
+ @property
260
+ def trend_emoji(self) -> str:
261
+ """Get the trend emoji."""
262
+ emojis = {
263
+ "improving": "📈",
264
+ "declining": "📉",
265
+ "stable": "➡️",
266
+ "new": "🆕",
267
+ }
268
+ return emojis.get(self.trend, "")
269
+
270
+ @property
271
+ def score_delta(self) -> float | None:
272
+ """Get the score change from previous."""
273
+ if self.previous is None:
274
+ return None
275
+ return self.current.overall_score - self.previous.overall_score
276
+
277
+ def to_dict(self) -> dict[str, Any]:
278
+ """Convert to dictionary."""
279
+ return {
280
+ "current": self.current.to_dict(),
281
+ "previous": self.previous.to_dict() if self.previous else None,
282
+ "trend": self.trend,
283
+ "score_delta": self.score_delta,
284
+ }
@@ -0,0 +1,310 @@
1
+ """Report generation for health scores (JSON and HTML)."""
2
+
3
+ import html
4
+ import json
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from codeshift.health.models import HealthReport, HealthScore
10
+
11
+
12
+ def generate_json_report(report: HealthReport | HealthScore, pretty: bool = True) -> str:
13
+ """Generate a JSON report.
14
+
15
+ Args:
16
+ report: HealthReport or HealthScore to serialize
17
+ pretty: Whether to pretty-print the JSON
18
+
19
+ Returns:
20
+ JSON string
21
+ """
22
+ if isinstance(report, HealthScore):
23
+ data = report.to_dict()
24
+ else:
25
+ data = report.to_dict()
26
+
27
+ if pretty:
28
+ return json.dumps(data, indent=2, default=_json_serializer)
29
+ return json.dumps(data, default=_json_serializer)
30
+
31
+
32
+ def save_json_report(report: HealthReport | HealthScore, output_path: Path) -> None:
33
+ """Save a JSON report to a file.
34
+
35
+ Args:
36
+ report: HealthReport or HealthScore to serialize
37
+ output_path: Path to save the report
38
+ """
39
+ json_content = generate_json_report(report)
40
+ output_path.write_text(json_content)
41
+
42
+
43
+ def generate_html_report(report: HealthReport | HealthScore) -> str:
44
+ """Generate an HTML report.
45
+
46
+ Args:
47
+ report: HealthReport or HealthScore to render
48
+
49
+ Returns:
50
+ HTML string
51
+ """
52
+ if isinstance(report, HealthScore):
53
+ score = report
54
+ trend_info = ""
55
+ else:
56
+ score = report.current
57
+ if report.previous:
58
+ delta = report.score_delta or 0
59
+ sign = "+" if delta >= 0 else ""
60
+ trend_info = (
61
+ f'<span class="trend {report.trend}">{report.trend_emoji} {sign}{delta:.1f}</span>'
62
+ )
63
+ else:
64
+ trend_info = '<span class="trend new">New baseline</span>'
65
+
66
+ # Build metrics rows
67
+ metrics_rows = ""
68
+ for metric in score.metrics:
69
+ grade_class = _get_score_class(metric.score)
70
+ metrics_rows += f"""
71
+ <tr>
72
+ <td>{_format_category(metric.category.value)}</td>
73
+ <td class="{grade_class}">{metric.score:.1f}</td>
74
+ <td>{metric.weight * 100:.0f}%</td>
75
+ <td>{html.escape(metric.description)}</td>
76
+ </tr>
77
+ """
78
+
79
+ # Build recommendations list
80
+ recs_html = ""
81
+ for rec in score.top_recommendations:
82
+ recs_html += f"<li>{html.escape(rec)}</li>\n"
83
+
84
+ # Build dependencies table
85
+ deps_rows = ""
86
+ for dep in score.dependencies:
87
+ status = "✓" if not dep.is_outdated else "↑"
88
+ status_class = "up-to-date" if not dep.is_outdated else "outdated"
89
+ tier = "Tier 1" if dep.has_tier1_support else ("Tier 2" if dep.has_tier2_support else "-")
90
+ vuln_count = len(dep.vulnerabilities)
91
+ vuln_class = "vuln-none" if vuln_count == 0 else "vuln-some"
92
+
93
+ deps_rows += f"""
94
+ <tr>
95
+ <td>{html.escape(dep.name)}</td>
96
+ <td>{html.escape(dep.current_version or 'unknown')}</td>
97
+ <td>{html.escape(dep.latest_version or 'unknown')}</td>
98
+ <td class="{status_class}">{status}</td>
99
+ <td>{tier}</td>
100
+ <td class="{vuln_class}">{vuln_count}</td>
101
+ </tr>
102
+ """
103
+
104
+ # Build vulnerabilities section
105
+ vulns_html = ""
106
+ if score.vulnerabilities:
107
+ vulns_rows = ""
108
+ for vuln in score.vulnerabilities:
109
+ vulns_rows += f"""
110
+ <tr class="severity-{vuln.severity.value}">
111
+ <td>{html.escape(vuln.package)}</td>
112
+ <td>{html.escape(vuln.vulnerability_id)}</td>
113
+ <td>{vuln.severity.value.upper()}</td>
114
+ <td>{html.escape(vuln.description[:100])}...</td>
115
+ <td>{html.escape(vuln.fixed_in or '-')}</td>
116
+ </tr>
117
+ """
118
+
119
+ vulns_html = f"""
120
+ <section class="vulnerabilities">
121
+ <h2>Security Vulnerabilities</h2>
122
+ <table>
123
+ <thead>
124
+ <tr>
125
+ <th>Package</th>
126
+ <th>ID</th>
127
+ <th>Severity</th>
128
+ <th>Description</th>
129
+ <th>Fixed In</th>
130
+ </tr>
131
+ </thead>
132
+ <tbody>
133
+ {vulns_rows}
134
+ </tbody>
135
+ </table>
136
+ </section>
137
+ """
138
+
139
+ grade_class = score.grade.value.lower()
140
+
141
+ return f"""<!DOCTYPE html>
142
+ <html lang="en">
143
+ <head>
144
+ <meta charset="UTF-8">
145
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
146
+ <title>Codeshift Health Report</title>
147
+ <style>
148
+ :root {{
149
+ --color-a: #22c55e;
150
+ --color-b: #06b6d4;
151
+ --color-c: #eab308;
152
+ --color-d: #f97316;
153
+ --color-f: #ef4444;
154
+ }}
155
+ * {{ box-sizing: border-box; margin: 0; padding: 0; }}
156
+ body {{
157
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
158
+ line-height: 1.6;
159
+ color: #1f2937;
160
+ background: #f9fafb;
161
+ padding: 2rem;
162
+ }}
163
+ .container {{ max-width: 1200px; margin: 0 auto; }}
164
+ h1 {{ margin-bottom: 0.5rem; }}
165
+ h2 {{ margin: 2rem 0 1rem; border-bottom: 2px solid #e5e7eb; padding-bottom: 0.5rem; }}
166
+ .header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; }}
167
+ .score-card {{
168
+ background: white;
169
+ border-radius: 12px;
170
+ padding: 2rem;
171
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
172
+ text-align: center;
173
+ }}
174
+ .grade {{ font-size: 4rem; font-weight: bold; }}
175
+ .grade.a {{ color: var(--color-a); }}
176
+ .grade.b {{ color: var(--color-b); }}
177
+ .grade.c {{ color: var(--color-c); }}
178
+ .grade.d {{ color: var(--color-d); }}
179
+ .grade.f {{ color: var(--color-f); }}
180
+ .overall-score {{ font-size: 1.5rem; color: #6b7280; }}
181
+ .trend {{ font-size: 1rem; margin-top: 0.5rem; display: block; }}
182
+ .trend.improving {{ color: var(--color-a); }}
183
+ .trend.declining {{ color: var(--color-f); }}
184
+ .trend.stable {{ color: #6b7280; }}
185
+ table {{ width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }}
186
+ th, td {{ padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid #e5e7eb; }}
187
+ th {{ background: #f3f4f6; font-weight: 600; }}
188
+ tr:last-child td {{ border-bottom: none; }}
189
+ .excellent {{ color: var(--color-a); font-weight: 600; }}
190
+ .good {{ color: var(--color-b); font-weight: 600; }}
191
+ .fair {{ color: var(--color-c); font-weight: 600; }}
192
+ .poor {{ color: var(--color-d); font-weight: 600; }}
193
+ .critical {{ color: var(--color-f); font-weight: 600; }}
194
+ .up-to-date {{ color: var(--color-a); }}
195
+ .outdated {{ color: var(--color-d); }}
196
+ .vuln-none {{ color: var(--color-a); }}
197
+ .vuln-some {{ color: var(--color-f); font-weight: 600; }}
198
+ .severity-critical td {{ background: #fef2f2; }}
199
+ .severity-high td {{ background: #fff7ed; }}
200
+ .recommendations {{ background: white; border-radius: 8px; padding: 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }}
201
+ .recommendations ul {{ padding-left: 1.5rem; }}
202
+ .recommendations li {{ margin: 0.5rem 0; }}
203
+ .meta {{ color: #6b7280; font-size: 0.875rem; margin-top: 2rem; }}
204
+ </style>
205
+ </head>
206
+ <body>
207
+ <div class="container">
208
+ <div class="header">
209
+ <div>
210
+ <h1>Codeshift Health Report</h1>
211
+ <p>{html.escape(str(score.project_path))}</p>
212
+ </div>
213
+ <div class="score-card">
214
+ <div class="grade {grade_class}">{score.grade.value}</div>
215
+ <div class="overall-score">{score.overall_score:.1f}/100</div>
216
+ {trend_info}
217
+ </div>
218
+ </div>
219
+
220
+ <section>
221
+ <h2>Metrics Breakdown</h2>
222
+ <table>
223
+ <thead>
224
+ <tr>
225
+ <th>Category</th>
226
+ <th>Score</th>
227
+ <th>Weight</th>
228
+ <th>Details</th>
229
+ </tr>
230
+ </thead>
231
+ <tbody>
232
+ {metrics_rows}
233
+ </tbody>
234
+ </table>
235
+ </section>
236
+
237
+ <section class="recommendations">
238
+ <h2>Recommendations</h2>
239
+ <ul>
240
+ {recs_html if recs_html else "<li>No recommendations - your project is in great shape!</li>"}
241
+ </ul>
242
+ </section>
243
+
244
+ <section>
245
+ <h2>Dependencies ({len(score.dependencies)})</h2>
246
+ <table>
247
+ <thead>
248
+ <tr>
249
+ <th>Package</th>
250
+ <th>Current</th>
251
+ <th>Latest</th>
252
+ <th>Status</th>
253
+ <th>Migration Support</th>
254
+ <th>Vulnerabilities</th>
255
+ </tr>
256
+ </thead>
257
+ <tbody>
258
+ {deps_rows if deps_rows else "<tr><td colspan='6'>No dependencies found</td></tr>"}
259
+ </tbody>
260
+ </table>
261
+ </section>
262
+
263
+ {vulns_html}
264
+
265
+ <p class="meta">
266
+ Generated by Codeshift on {score.calculated_at.strftime('%Y-%m-%d %H:%M:%S')}
267
+ </p>
268
+ </div>
269
+ </body>
270
+ </html>
271
+ """
272
+
273
+
274
+ def save_html_report(report: HealthReport | HealthScore, output_path: Path) -> None:
275
+ """Save an HTML report to a file.
276
+
277
+ Args:
278
+ report: HealthReport or HealthScore to render
279
+ output_path: Path to save the report
280
+ """
281
+ html_content = generate_html_report(report)
282
+ output_path.write_text(html_content)
283
+
284
+
285
+ def _json_serializer(obj: Any) -> Any:
286
+ """Custom JSON serializer for non-standard types."""
287
+ if isinstance(obj, datetime):
288
+ return obj.isoformat()
289
+ if isinstance(obj, Path):
290
+ return str(obj)
291
+ raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
292
+
293
+
294
+ def _get_score_class(score: float) -> str:
295
+ """Get CSS class based on score."""
296
+ if score >= 90:
297
+ return "excellent"
298
+ elif score >= 80:
299
+ return "good"
300
+ elif score >= 70:
301
+ return "fair"
302
+ elif score >= 60:
303
+ return "poor"
304
+ else:
305
+ return "critical"
306
+
307
+
308
+ def _format_category(category: str) -> str:
309
+ """Format category name for display."""
310
+ return category.replace("_", " ").title()
@@ -172,6 +172,12 @@ TIER_1_LIBRARIES = {
172
172
  "pytest",
173
173
  "marshmallow",
174
174
  "flask",
175
+ "celery",
176
+ "httpx",
177
+ "aiohttp",
178
+ "click",
179
+ "attrs",
180
+ "django",
175
181
  }
176
182
 
177
183
 
@@ -69,7 +69,7 @@ breaking_changes:
69
69
 
70
70
  # connector_owner default change
71
71
  - symbol: "ClientSession(connector=..., connector_owner=None)"
72
- change_type: behavior_change
72
+ change_type: behavior_changed
73
73
  severity: medium
74
74
  from_version: "3.7"
75
75
  to_version: "3.9"
@@ -133,7 +133,7 @@ breaking_changes:
133
133
 
134
134
  # Middleware signature changes (old-style to new-style)
135
135
  - symbol: "@middleware"
136
- change_type: signature_change
136
+ change_type: signature_changed
137
137
  severity: high
138
138
  from_version: "3.7"
139
139
  to_version: "3.9"
@@ -154,7 +154,7 @@ breaking_changes:
154
154
  transform_name: ws_connect_timeout_rename
155
155
 
156
156
  - symbol: "ws_connect(receive_timeout=...)"
157
- change_type: behavior_change
157
+ change_type: behavior_changed
158
158
  severity: low
159
159
  from_version: "3.7"
160
160
  to_version: "3.9"
@@ -119,7 +119,7 @@ breaking_changes:
119
119
 
120
120
  # Response.iter_lines() behavior change
121
121
  - symbol: "Response.iter_lines()"
122
- change_type: behavior_change
122
+ change_type: behavior_changed
123
123
  severity: medium
124
124
  from_version: "0.23"
125
125
  to_version: "0.24"
@@ -130,7 +130,7 @@ breaking_changes:
130
130
 
131
131
  # NetRC authentication change
132
132
  - symbol: "trust_env=True"
133
- change_type: behavior_change
133
+ change_type: behavior_changed
134
134
  severity: medium
135
135
  from_version: "0.23"
136
136
  to_version: "0.24"
@@ -141,7 +141,7 @@ breaking_changes:
141
141
 
142
142
  # Query parameter encoding change
143
143
  - symbol: "params encoding"
144
- change_type: behavior_change
144
+ change_type: behavior_changed
145
145
  severity: low
146
146
  from_version: "0.23"
147
147
  to_version: "0.24"
@@ -173,7 +173,7 @@ breaking_changes:
173
173
 
174
174
  # Follow redirects default change
175
175
  - symbol: "follow_redirects"
176
- change_type: behavior_change
176
+ change_type: behavior_changed
177
177
  severity: high
178
178
  from_version: "0.19"
179
179
  to_version: "0.20"
@@ -182,7 +182,7 @@ breaking_changes:
182
182
 
183
183
  # pytest.importorskip exc_type
184
184
  - symbol: "pytest.importorskip()"
185
- change_type: behavior_change
185
+ change_type: behavior_changed
186
186
  severity: low
187
187
  from_version: "8.0"
188
188
  to_version: "8.2"
@@ -9,6 +9,7 @@ class ChangeType(Enum):
9
9
 
10
10
  RENAMED = "renamed"
11
11
  REMOVED = "removed"
12
+ MOVED = "moved"
12
13
  SIGNATURE_CHANGED = "signature_changed"
13
14
  BEHAVIOR_CHANGED = "behavior_changed"
14
15
  DEPRECATED = "deprecated"