pull-request-fixer 0.1.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.
@@ -0,0 +1,483 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: 2025 The Linux Foundation
3
+
4
+ from contextlib import suppress
5
+ from datetime import datetime, timedelta
6
+ from typing import Any
7
+
8
+ try:
9
+ from rich.console import Console
10
+ from rich.live import Live
11
+ from rich.text import Text
12
+
13
+ RICH_AVAILABLE = True
14
+ except ImportError:
15
+ RICH_AVAILABLE = False
16
+
17
+ # Fallback classes for when Rich is not available
18
+ class Live: # type: ignore
19
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
20
+ pass
21
+
22
+ def start(self) -> None:
23
+ pass
24
+
25
+ def stop(self) -> None:
26
+ pass
27
+
28
+ def update(self, *args: Any) -> None:
29
+ pass
30
+
31
+ class Text: # type: ignore
32
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
33
+ pass
34
+
35
+ def append(self, *args: Any, **kwargs: Any) -> None:
36
+ pass
37
+
38
+ class Console: # type: ignore
39
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
40
+ pass
41
+
42
+
43
+ class ProgressTracker:
44
+ """Real-time progress tracker for organization blocked PR checking operations."""
45
+
46
+ def __init__(self, organization: str, show_pr_stats: bool = True):
47
+ """Initialize progress tracker for an organization blocked PR check.
48
+
49
+ Args:
50
+ organization: Name of the GitHub organization being checked
51
+ show_pr_stats: Whether to show PR analysis statistics (default True)
52
+ """
53
+ self.organization = organization
54
+ self.start_time = datetime.now()
55
+ self.console = Console() if RICH_AVAILABLE else None
56
+
57
+ # Progress counters
58
+ self.total_repositories = 0
59
+ self.completed_repositories = 0
60
+ self.current_repository = ""
61
+ self.total_prs_analyzed = 0
62
+ self.unmergeable_prs_found = 0
63
+ self.current_operation = "Initializing..."
64
+ self.errors_count = 0
65
+
66
+ # Configuration
67
+ self.show_pr_stats = show_pr_stats
68
+
69
+ # Rate limiting tracking
70
+ self.rate_limited = False
71
+ self.rate_limit_reset_time: datetime | None = None
72
+
73
+ # Rich Live display
74
+ self.live: Live | None = None
75
+ self.rich_available = RICH_AVAILABLE
76
+ self.paused = False
77
+ # Metrics (optional; displayed when provided)
78
+ self.metrics_concurrency: int | None = None
79
+ self.metrics_rps: float | None = None
80
+
81
+ # Fallback for when Rich is not available
82
+ self._last_display = ""
83
+
84
+ def start(self) -> None:
85
+ """Start the live progress display."""
86
+ if not self.rich_available:
87
+ return
88
+
89
+ try:
90
+ self.live = Live(
91
+ self._generate_display_text(),
92
+ console=self.console,
93
+ refresh_per_second=2,
94
+ transient=False,
95
+ )
96
+ if self.live:
97
+ self.live.start()
98
+ except Exception:
99
+ # Fallback if Rich display fails (e.g., unsupported terminal)
100
+ self.rich_available = False
101
+ self.live = None
102
+
103
+ def stop(self) -> None:
104
+ """Stop the live progress display."""
105
+ if self.live:
106
+ with suppress(Exception):
107
+ self.live.stop()
108
+ # Ensure paused state is cleared when fully stopped
109
+ self.paused = False
110
+
111
+ def suspend(self) -> None:
112
+ """Temporarily pause the live display to allow clean printing elsewhere."""
113
+ if self.live and self.rich_available and not self.paused:
114
+ with suppress(Exception):
115
+ self.live.stop()
116
+ self.paused = True
117
+
118
+ def resume(self) -> None:
119
+ """Resume the live display after it was suspended."""
120
+ if self.rich_available and self.paused:
121
+ with suppress(Exception):
122
+ self.live = Live(
123
+ self._generate_display_text(),
124
+ console=self.console,
125
+ refresh_per_second=2,
126
+ transient=False,
127
+ )
128
+ if self.live:
129
+ self.live.start()
130
+ self.paused = False
131
+
132
+ def update_metrics(self, concurrency: int, rps: float) -> None:
133
+ """Update concurrency and RPS metrics (no-op since metrics are not displayed)."""
134
+ pass
135
+
136
+ def clear_metrics(self) -> None:
137
+ """Clear the concurrency and RPS metrics (no-op since metrics are not displayed)."""
138
+ pass
139
+
140
+ def update_total_repositories(self, total: int) -> None:
141
+ """Update the total number of repositories to process."""
142
+ self.total_repositories = total
143
+ self._refresh_display()
144
+
145
+ def start_repository(self, repo_name: str) -> None:
146
+ """Mark the start of processing a repository."""
147
+ self.current_repository = repo_name
148
+ self.current_operation = f"Getting PRs from {repo_name}"
149
+ self._refresh_display()
150
+
151
+ def complete_repository(self, unmergeable_count: int = 0) -> None:
152
+ """Mark completion of a repository."""
153
+ self.completed_repositories += 1
154
+ self.unmergeable_prs_found += unmergeable_count
155
+ self.current_operation = "Moving to next repository..."
156
+ self._refresh_display()
157
+
158
+ def update_operation(self, operation: str) -> None:
159
+ """Update the current operation description."""
160
+ self.current_operation = operation
161
+ self._refresh_display()
162
+
163
+ def analyze_pr(self, pr_number: int, repo_name: str) -> None:
164
+ """Mark the start of analyzing a specific PR."""
165
+ self.total_prs_analyzed += 1
166
+ self.current_operation = f"Analyzing PR #{pr_number} in {repo_name}"
167
+ self._refresh_display()
168
+
169
+ def add_error(self) -> None:
170
+ """Increment the error count."""
171
+ self.errors_count += 1
172
+ self._refresh_display()
173
+
174
+ def set_rate_limited(self, reset_time: datetime) -> None:
175
+ """Mark that we're rate limited and show countdown."""
176
+ self.rate_limited = True
177
+ self.rate_limit_reset_time = reset_time
178
+ self._refresh_display()
179
+
180
+ def clear_rate_limited(self) -> None:
181
+ """Clear rate limiting status."""
182
+ self.rate_limited = False
183
+ self.rate_limit_reset_time = None
184
+ self._refresh_display()
185
+
186
+ def _refresh_display(self) -> None:
187
+ """Refresh the live display if it's active."""
188
+ if self.live and self.rich_available and not self.paused:
189
+ with suppress(Exception):
190
+ self.live.update(self._generate_display_text())
191
+ elif not self.rich_available:
192
+ self._fallback_display()
193
+
194
+ def _generate_display_text(self) -> Any:
195
+ """Generate the current progress display text."""
196
+ if not self.rich_available:
197
+ return Text()
198
+
199
+ text = Text()
200
+
201
+ # Main progress line
202
+ if self.total_repositories > 0:
203
+ progress_pct = (
204
+ self.completed_repositories / self.total_repositories
205
+ ) * 100
206
+ text.append("šŸ” Checking ", style="bold blue")
207
+ text.append(f"{self.organization} ", style="bold cyan")
208
+ text.append(
209
+ f"({self.completed_repositories}/{self.total_repositories} repos, ",
210
+ style="white",
211
+ )
212
+ text.append(f"{progress_pct:.0f}%", style="green")
213
+ text.append(") | ", style="white")
214
+ else:
215
+ text.append("šŸ” Checking organization ", style="bold blue")
216
+ text.append(f"{self.organization}", style="bold cyan")
217
+ text.append(" (counting repositories...)", style="white")
218
+
219
+ # Stats (only when repo count is known and PR stats are enabled)
220
+ if self.total_repositories > 0 and self.show_pr_stats:
221
+ text.append(
222
+ f"{self.total_prs_analyzed} PRs analyzed | ", style="white"
223
+ )
224
+
225
+ if self.unmergeable_prs_found > 0:
226
+ text.append(
227
+ f"{self.unmergeable_prs_found} unmergeable", style="red"
228
+ )
229
+ else:
230
+ text.append(
231
+ f"{self.unmergeable_prs_found} unmergeable", style="green"
232
+ )
233
+
234
+ if self.errors_count > 0:
235
+ text.append(f" | {self.errors_count} errors", style="yellow")
236
+ elif self.total_repositories > 0 and not self.show_pr_stats:
237
+ # Show errors even when PR stats are disabled
238
+ if self.errors_count > 0:
239
+ text.append(f"{self.errors_count} errors", style="yellow")
240
+
241
+ text.append("\n")
242
+
243
+ # Current operation line
244
+ if self.rate_limited and self.rate_limit_reset_time:
245
+ remaining = self.rate_limit_reset_time - datetime.now()
246
+ if remaining.total_seconds() > 0:
247
+ text.append(
248
+ f"ā³ Rate limited - waiting {remaining.seconds}s",
249
+ style="yellow",
250
+ )
251
+ else:
252
+ text.append("⚔ Rate limit reset - resuming...", style="green")
253
+ else:
254
+ text.append(f"šŸ“‹ {self.current_operation}", style="dim white")
255
+
256
+ # Elapsed time
257
+ elapsed = datetime.now() - self.start_time
258
+ text.append(
259
+ f"\nā±ļø Elapsed: {self._format_duration(elapsed)}", style="dim blue"
260
+ )
261
+
262
+ return text
263
+
264
+ def _fallback_display(self) -> None:
265
+ """Fallback display method for when Rich is not available."""
266
+ # Generate simple text display
267
+ if self.total_repositories > 0:
268
+ progress_pct = (
269
+ self.completed_repositories / self.total_repositories
270
+ ) * 100
271
+ if self.show_pr_stats:
272
+ progress_line = f"šŸ” Checking {self.organization} ({self.completed_repositories}/{self.total_repositories} repos, {progress_pct:.0f}%) | {self.total_prs_analyzed} PRs analyzed | {self.unmergeable_prs_found} unmergeable"
273
+ else:
274
+ progress_line = f"šŸ” Checking {self.organization} ({self.completed_repositories}/{self.total_repositories} repos, {progress_pct:.0f}%)"
275
+ if self.errors_count > 0:
276
+ progress_line += f" | {self.errors_count} errors"
277
+ else:
278
+ progress_line = f"šŸ” Checking organization {self.organization} (counting repositories...)"
279
+
280
+ operation_line = f"šŸ“‹ {self.current_operation}"
281
+ elapsed = datetime.now() - self.start_time
282
+ time_line = f"ā±ļø Elapsed: {self._format_duration(elapsed)}"
283
+
284
+ current_display = f"{progress_line}\n{operation_line}\n{time_line}"
285
+
286
+ # Only print if display has changed to avoid spam
287
+ if current_display != self._last_display:
288
+ print(f"\r{current_display}\n", end="", flush=True)
289
+ self._last_display = current_display
290
+
291
+ def _format_duration(self, duration: timedelta) -> str:
292
+ """Format a duration for display."""
293
+ total_seconds = int(duration.total_seconds())
294
+ minutes = total_seconds // 60
295
+ seconds = total_seconds % 60
296
+
297
+ if minutes > 0:
298
+ return f"{minutes}m {seconds}s"
299
+ else:
300
+ return f"{seconds}s"
301
+
302
+ def get_summary(self) -> dict[str, Any]:
303
+ """Get a summary of the checking progress."""
304
+ elapsed = datetime.now() - self.start_time
305
+
306
+ return {
307
+ "organization": self.organization,
308
+ "total_repositories": self.total_repositories,
309
+ "completed_repositories": self.completed_repositories,
310
+ "total_prs_analyzed": self.total_prs_analyzed,
311
+ "unmergeable_prs_found": self.unmergeable_prs_found,
312
+ "errors_count": self.errors_count,
313
+ "elapsed_time": self._format_duration(elapsed),
314
+ "rate_limited": self.rate_limited,
315
+ }
316
+
317
+
318
+ class MergeProgressTracker(ProgressTracker):
319
+ """Specialized progress tracker for merge operations."""
320
+
321
+ def __init__(self, organization: str, is_close_operation: bool = False):
322
+ super().__init__(organization)
323
+ self.similar_prs_found = 0
324
+ self.prs_merged = 0
325
+ self.merge_failures = 0
326
+ self.prs_closed = 0
327
+ self.is_close_operation = is_close_operation
328
+
329
+ def add_similar_pr(self) -> None:
330
+ """Mark that a similar PR was found."""
331
+ self.similar_prs_found += 1
332
+ self._refresh_display()
333
+
334
+ def merge_success(self) -> None:
335
+ """Mark a successful merge."""
336
+ self.prs_merged += 1
337
+ self._refresh_display()
338
+
339
+ def merge_failure(self) -> None:
340
+ """Mark a failed merge."""
341
+ self.merge_failures += 1
342
+ self._refresh_display()
343
+
344
+ def increment_closed(self) -> None:
345
+ """Mark a successful close."""
346
+ self.prs_closed += 1
347
+ self._refresh_display()
348
+
349
+ def _generate_display_text(self) -> Any:
350
+ """Generate merge-specific display text."""
351
+ if not self.rich_available:
352
+ return Text()
353
+
354
+ text = Text()
355
+
356
+ # Main progress line for merge/close operations
357
+ if self.total_repositories > 0:
358
+ progress_pct = (
359
+ self.completed_repositories / self.total_repositories
360
+ ) * 100
361
+ operation_icon = "🚪" if self.is_close_operation else "šŸ”€"
362
+ operation_text = (
363
+ "Searching for similar PRs"
364
+ if not self.is_close_operation
365
+ else "Searching for similar PRs"
366
+ )
367
+ text.append(
368
+ f"{operation_icon} {operation_text} ", style="bold blue"
369
+ )
370
+ text.append(
371
+ f"({self.completed_repositories}/{self.total_repositories} repos, ",
372
+ style="white",
373
+ )
374
+ text.append(f"{progress_pct:.0f}%", style="green")
375
+ text.append(") | ", style="white")
376
+ # Special heading when examining the source PR before repo enumeration
377
+ elif "Getting source PR details" in (self.current_operation or ""):
378
+ text.append(
379
+ "šŸ” Examining source pull request in ", style="bold blue"
380
+ )
381
+ text.append(f"{self.organization}", style="bold cyan")
382
+ else:
383
+ operation_icon = "🚪" if self.is_close_operation else "šŸ”€"
384
+ text.append(
385
+ f"{operation_icon} Analyzing PRs in ", style="bold blue"
386
+ )
387
+ text.append(f"{self.organization}", style="bold cyan")
388
+
389
+ # Stats for merge operations (only when repo count is known)
390
+ if self.total_repositories > 0:
391
+ text.append(
392
+ f"{self.total_prs_analyzed} PRs analyzed", style="white"
393
+ )
394
+ if self.errors_count > 0:
395
+ text.append(f" | {self.errors_count} errors", style="yellow")
396
+
397
+ text.append("\n")
398
+
399
+ # Current operation line
400
+ if self.rate_limited and self.rate_limit_reset_time:
401
+ remaining = self.rate_limit_reset_time - datetime.now()
402
+ if remaining.total_seconds() > 0:
403
+ text.append(
404
+ f"ā³ Rate limited - waiting {remaining.seconds}s",
405
+ style="yellow",
406
+ )
407
+ else:
408
+ text.append("⚔ Rate limit reset - resuming...", style="green")
409
+ else:
410
+ text.append(f"šŸ“‹ {self.current_operation}", style="dim white")
411
+
412
+ # Elapsed time
413
+ elapsed = datetime.now() - self.start_time
414
+ text.append(
415
+ f"\nā±ļø Elapsed: {self._format_duration(elapsed)}", style="dim blue"
416
+ )
417
+
418
+ return text
419
+
420
+ def get_summary(self) -> dict[str, Any]:
421
+ """Get merge-specific summary."""
422
+ summary = super().get_summary()
423
+ summary.update(
424
+ {
425
+ "similar_prs_found": self.similar_prs_found,
426
+ "prs_merged": self.prs_merged,
427
+ "merge_failures": self.merge_failures,
428
+ "prs_closed": self.prs_closed,
429
+ }
430
+ )
431
+ return summary
432
+
433
+
434
+ class DummyProgressTracker:
435
+ """A no-op progress tracker for when progress display is disabled."""
436
+
437
+ def __init__(self, organization: str):
438
+ self.organization = organization
439
+
440
+ def start(self) -> None:
441
+ pass
442
+
443
+ def stop(self) -> None:
444
+ pass
445
+
446
+ def update_total_repositories(self, total: int) -> None:
447
+ pass
448
+
449
+ def start_repository(self, repo_name: str) -> None:
450
+ pass
451
+
452
+ def complete_repository(self, unmergeable_count: int = 0) -> None:
453
+ pass
454
+
455
+ def update_operation(self, operation: str) -> None:
456
+ pass
457
+
458
+ def analyze_pr(self, pr_number: int, repo_name: str) -> None:
459
+ pass
460
+
461
+ def add_error(self) -> None:
462
+ pass
463
+
464
+ def set_rate_limited(self, reset_time: datetime) -> None:
465
+ pass
466
+
467
+ def clear_rate_limited(self) -> None:
468
+ pass
469
+
470
+ def found_similar_pr(self) -> None:
471
+ pass
472
+
473
+ def merge_success(self) -> None:
474
+ pass
475
+
476
+ def merge_failure(self) -> None:
477
+ pass
478
+
479
+ def increment_closed(self) -> None:
480
+ pass
481
+
482
+ def get_summary(self) -> dict[str, Any]:
483
+ return {"organization": self.organization}
File without changes