|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import math |
| 4 | +import re |
3 | 5 | from gettext import gettext as _ |
4 | 6 | from typing import Literal |
| 7 | +from datetime import datetime |
5 | 8 |
|
6 | 9 | import click |
7 | 10 |
|
| 11 | +from together.types.finetune import FinetuneResponse, COMPLETED_STATUSES |
| 12 | + |
| 13 | +_PROGRESS_BAR_WIDTH = 40 |
| 14 | + |
8 | 15 |
|
9 | 16 | class AutoIntParamType(click.ParamType): |
10 | 17 | name = "integer_or_max" |
@@ -49,3 +56,84 @@ def convert( |
49 | 56 |
|
50 | 57 | INT_WITH_MAX = AutoIntParamType() |
51 | 58 | BOOL_WITH_AUTO = BooleanWithAutoParamType() |
| 59 | + |
| 60 | + |
| 61 | +def _human_readable_time(timedelta: float) -> str: |
| 62 | + """Convert a timedelta to a compact human-readble string |
| 63 | + Examples: |
| 64 | + 00:00:10 -> 10s |
| 65 | + 01:23:45 -> 1h 23min 45s |
| 66 | + 1 Month 23 days 04:56:07 -> 1month 23d 4h 56min 7s |
| 67 | + Args: |
| 68 | + timedelta (float): The timedelta in seconds to convert. |
| 69 | + Returns: |
| 70 | + A string representing the timedelta in a human-readable format. |
| 71 | + """ |
| 72 | + units = [ |
| 73 | + (30 * 24 * 60 * 60, "month"), # 30 days |
| 74 | + (24 * 60 * 60, "d"), |
| 75 | + (60 * 60, "h"), |
| 76 | + (60, "min"), |
| 77 | + (1, "s"), |
| 78 | + ] |
| 79 | + |
| 80 | + total_seconds = int(timedelta) |
| 81 | + parts = [] |
| 82 | + |
| 83 | + for unit_seconds, unit_name in units: |
| 84 | + if total_seconds >= unit_seconds: |
| 85 | + value = total_seconds // unit_seconds |
| 86 | + total_seconds %= unit_seconds |
| 87 | + parts.append(f"{value}{unit_name}") |
| 88 | + |
| 89 | + return " ".join(parts) if parts else "0s" |
| 90 | + |
| 91 | + |
| 92 | +def generate_progress_bar( |
| 93 | + finetune_job: FinetuneResponse, current_time: datetime, use_rich: bool = False |
| 94 | +) -> str: |
| 95 | + """Generate a progress bar for a finetune job. |
| 96 | + Args: |
| 97 | + finetune_job: The finetune job to generate a progress bar for. |
| 98 | + current_time: The current time. |
| 99 | + use_rich: Whether to use rich formatting. |
| 100 | + Returns: |
| 101 | + A string representing the progress bar. |
| 102 | + """ |
| 103 | + progress = "Progress: [bold red]unavailable[/bold red]" |
| 104 | + if finetune_job.status in COMPLETED_STATUSES: |
| 105 | + progress = "Progress: [bold green]completed[/bold green]" |
| 106 | + elif finetune_job.updated_at is not None: |
| 107 | + # Replace 'Z' with '+00:00' for Python 3.10 compatibility |
| 108 | + updated_at_str = finetune_job.updated_at.replace("Z", "+00:00") |
| 109 | + update_at = datetime.fromisoformat(updated_at_str).astimezone() |
| 110 | + |
| 111 | + if finetune_job.progress is not None: |
| 112 | + if current_time < update_at: |
| 113 | + return progress |
| 114 | + |
| 115 | + if not finetune_job.progress.estimate_available: |
| 116 | + return progress |
| 117 | + |
| 118 | + if finetune_job.progress.seconds_remaining <= 0: |
| 119 | + return progress |
| 120 | + |
| 121 | + elapsed_time = (current_time - update_at).total_seconds() |
| 122 | + ratio_filled = min( |
| 123 | + elapsed_time / finetune_job.progress.seconds_remaining, 1.0 |
| 124 | + ) |
| 125 | + percentage = ratio_filled * 100 |
| 126 | + filled = math.ceil(ratio_filled * _PROGRESS_BAR_WIDTH) |
| 127 | + bar = "█" * filled + "░" * (_PROGRESS_BAR_WIDTH - filled) |
| 128 | + time_left = "N/A" |
| 129 | + if finetune_job.progress.seconds_remaining > elapsed_time: |
| 130 | + time_left = _human_readable_time( |
| 131 | + finetune_job.progress.seconds_remaining - elapsed_time |
| 132 | + ) |
| 133 | + time_text = f"{time_left} left" |
| 134 | + progress = f"Progress: {bar} [bold]{percentage:>3.0f}%[/bold] [yellow]{time_text}[/yellow]" |
| 135 | + |
| 136 | + if use_rich: |
| 137 | + return progress |
| 138 | + |
| 139 | + return re.sub(r"\[/?[^\]]+\]", "", progress) |
0 commit comments