guidellm 0.3.1__py3-none-any.whl → 0.6.0a5__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.
- guidellm/__init__.py +5 -2
- guidellm/__main__.py +524 -255
- guidellm/backends/__init__.py +33 -0
- guidellm/backends/backend.py +109 -0
- guidellm/backends/openai.py +340 -0
- guidellm/backends/response_handlers.py +428 -0
- guidellm/benchmark/__init__.py +69 -39
- guidellm/benchmark/benchmarker.py +160 -316
- guidellm/benchmark/entrypoints.py +560 -127
- guidellm/benchmark/outputs/__init__.py +24 -0
- guidellm/benchmark/outputs/console.py +633 -0
- guidellm/benchmark/outputs/csv.py +721 -0
- guidellm/benchmark/outputs/html.py +473 -0
- guidellm/benchmark/outputs/output.py +169 -0
- guidellm/benchmark/outputs/serialized.py +69 -0
- guidellm/benchmark/profiles.py +718 -0
- guidellm/benchmark/progress.py +553 -556
- guidellm/benchmark/scenarios/__init__.py +40 -0
- guidellm/benchmark/scenarios/chat.json +6 -0
- guidellm/benchmark/scenarios/rag.json +6 -0
- guidellm/benchmark/schemas/__init__.py +66 -0
- guidellm/benchmark/schemas/base.py +402 -0
- guidellm/benchmark/schemas/generative/__init__.py +55 -0
- guidellm/benchmark/schemas/generative/accumulator.py +841 -0
- guidellm/benchmark/schemas/generative/benchmark.py +163 -0
- guidellm/benchmark/schemas/generative/entrypoints.py +381 -0
- guidellm/benchmark/schemas/generative/metrics.py +927 -0
- guidellm/benchmark/schemas/generative/report.py +158 -0
- guidellm/data/__init__.py +34 -4
- guidellm/data/builders.py +541 -0
- guidellm/data/collators.py +16 -0
- guidellm/data/config.py +120 -0
- guidellm/data/deserializers/__init__.py +49 -0
- guidellm/data/deserializers/deserializer.py +141 -0
- guidellm/data/deserializers/file.py +223 -0
- guidellm/data/deserializers/huggingface.py +94 -0
- guidellm/data/deserializers/memory.py +194 -0
- guidellm/data/deserializers/synthetic.py +246 -0
- guidellm/data/entrypoints.py +52 -0
- guidellm/data/loaders.py +190 -0
- guidellm/data/preprocessors/__init__.py +27 -0
- guidellm/data/preprocessors/formatters.py +410 -0
- guidellm/data/preprocessors/mappers.py +196 -0
- guidellm/data/preprocessors/preprocessor.py +30 -0
- guidellm/data/processor.py +29 -0
- guidellm/data/schemas.py +175 -0
- guidellm/data/utils/__init__.py +6 -0
- guidellm/data/utils/dataset.py +94 -0
- guidellm/extras/__init__.py +4 -0
- guidellm/extras/audio.py +220 -0
- guidellm/extras/vision.py +242 -0
- guidellm/logger.py +2 -2
- guidellm/mock_server/__init__.py +8 -0
- guidellm/mock_server/config.py +84 -0
- guidellm/mock_server/handlers/__init__.py +17 -0
- guidellm/mock_server/handlers/chat_completions.py +280 -0
- guidellm/mock_server/handlers/completions.py +280 -0
- guidellm/mock_server/handlers/tokenizer.py +142 -0
- guidellm/mock_server/models.py +510 -0
- guidellm/mock_server/server.py +238 -0
- guidellm/mock_server/utils.py +302 -0
- guidellm/scheduler/__init__.py +69 -26
- guidellm/scheduler/constraints/__init__.py +49 -0
- guidellm/scheduler/constraints/constraint.py +325 -0
- guidellm/scheduler/constraints/error.py +411 -0
- guidellm/scheduler/constraints/factory.py +182 -0
- guidellm/scheduler/constraints/request.py +312 -0
- guidellm/scheduler/constraints/saturation.py +722 -0
- guidellm/scheduler/environments.py +252 -0
- guidellm/scheduler/scheduler.py +137 -368
- guidellm/scheduler/schemas.py +358 -0
- guidellm/scheduler/strategies.py +617 -0
- guidellm/scheduler/worker.py +413 -419
- guidellm/scheduler/worker_group.py +712 -0
- guidellm/schemas/__init__.py +65 -0
- guidellm/schemas/base.py +417 -0
- guidellm/schemas/info.py +188 -0
- guidellm/schemas/request.py +235 -0
- guidellm/schemas/request_stats.py +349 -0
- guidellm/schemas/response.py +124 -0
- guidellm/schemas/statistics.py +1018 -0
- guidellm/{config.py → settings.py} +31 -24
- guidellm/utils/__init__.py +71 -8
- guidellm/utils/auto_importer.py +98 -0
- guidellm/utils/cli.py +132 -5
- guidellm/utils/console.py +566 -0
- guidellm/utils/encoding.py +778 -0
- guidellm/utils/functions.py +159 -0
- guidellm/utils/hf_datasets.py +1 -2
- guidellm/utils/hf_transformers.py +4 -4
- guidellm/utils/imports.py +9 -0
- guidellm/utils/messaging.py +1118 -0
- guidellm/utils/mixins.py +115 -0
- guidellm/utils/random.py +3 -4
- guidellm/utils/registry.py +220 -0
- guidellm/utils/singleton.py +133 -0
- guidellm/utils/synchronous.py +159 -0
- guidellm/utils/text.py +163 -50
- guidellm/utils/typing.py +41 -0
- guidellm/version.py +2 -2
- guidellm-0.6.0a5.dist-info/METADATA +364 -0
- guidellm-0.6.0a5.dist-info/RECORD +109 -0
- guidellm/backend/__init__.py +0 -23
- guidellm/backend/backend.py +0 -259
- guidellm/backend/openai.py +0 -708
- guidellm/backend/response.py +0 -136
- guidellm/benchmark/aggregator.py +0 -760
- guidellm/benchmark/benchmark.py +0 -837
- guidellm/benchmark/output.py +0 -997
- guidellm/benchmark/profile.py +0 -409
- guidellm/benchmark/scenario.py +0 -104
- guidellm/data/prideandprejudice.txt.gz +0 -0
- guidellm/dataset/__init__.py +0 -22
- guidellm/dataset/creator.py +0 -213
- guidellm/dataset/entrypoints.py +0 -42
- guidellm/dataset/file.py +0 -92
- guidellm/dataset/hf_datasets.py +0 -62
- guidellm/dataset/in_memory.py +0 -132
- guidellm/dataset/synthetic.py +0 -287
- guidellm/objects/__init__.py +0 -18
- guidellm/objects/pydantic.py +0 -89
- guidellm/objects/statistics.py +0 -953
- guidellm/preprocess/__init__.py +0 -3
- guidellm/preprocess/dataset.py +0 -374
- guidellm/presentation/__init__.py +0 -28
- guidellm/presentation/builder.py +0 -27
- guidellm/presentation/data_models.py +0 -232
- guidellm/presentation/injector.py +0 -66
- guidellm/request/__init__.py +0 -18
- guidellm/request/loader.py +0 -284
- guidellm/request/request.py +0 -79
- guidellm/request/types.py +0 -10
- guidellm/scheduler/queues.py +0 -25
- guidellm/scheduler/result.py +0 -155
- guidellm/scheduler/strategy.py +0 -495
- guidellm-0.3.1.dist-info/METADATA +0 -329
- guidellm-0.3.1.dist-info/RECORD +0 -62
- {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/WHEEL +0 -0
- {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/entry_points.txt +0 -0
- {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/licenses/LICENSE +0 -0
- {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Console utilities for rich terminal output and status updates.
|
|
3
|
+
|
|
4
|
+
Provides an extended Rich console with custom formatting for status messages,
|
|
5
|
+
progress tracking, and tabular data display. Includes predefined color schemes,
|
|
6
|
+
status levels, icons, and styles for consistent terminal output across the
|
|
7
|
+
application. Supports multi-step operations with spinners and context managers
|
|
8
|
+
for clean progress reporting.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Mapping, Sequence
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Annotated, Any, Literal
|
|
16
|
+
|
|
17
|
+
from rich.console import Console as RichConsole
|
|
18
|
+
from rich.padding import Padding
|
|
19
|
+
from rich.status import Status
|
|
20
|
+
from rich.text import Text
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Colors",
|
|
24
|
+
"Console",
|
|
25
|
+
"ConsoleUpdateStep",
|
|
26
|
+
"StatusIcons",
|
|
27
|
+
"StatusLevel",
|
|
28
|
+
"StatusStyles",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
StatusLevel = Annotated[
|
|
32
|
+
Literal[
|
|
33
|
+
"debug",
|
|
34
|
+
"info",
|
|
35
|
+
"warning",
|
|
36
|
+
"error",
|
|
37
|
+
"critical",
|
|
38
|
+
"notset",
|
|
39
|
+
"success",
|
|
40
|
+
],
|
|
41
|
+
"Status level for console messages indicating severity or state",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Colors:
|
|
46
|
+
"""
|
|
47
|
+
Color constants for console styling.
|
|
48
|
+
|
|
49
|
+
Provides standardized color schemes for different message types and branding.
|
|
50
|
+
Colors are defined using Rich console color names or hex values.
|
|
51
|
+
|
|
52
|
+
:cvar info: Color for informational messages
|
|
53
|
+
:cvar progress: Color for progress indicators
|
|
54
|
+
:cvar success: Color for successful operations
|
|
55
|
+
:cvar warning: Color for warning messages
|
|
56
|
+
:cvar error: Color for error messages
|
|
57
|
+
:cvar primary: Primary brand color
|
|
58
|
+
:cvar secondary: Secondary brand color
|
|
59
|
+
:cvar tertiary: Tertiary brand color
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
# Core states
|
|
63
|
+
info: str = "light_steel_blue"
|
|
64
|
+
progress: str = "dark_slate_gray1"
|
|
65
|
+
success: str = "chartreuse1"
|
|
66
|
+
warning: str = "#FDB516"
|
|
67
|
+
error: str = "orange_red1"
|
|
68
|
+
|
|
69
|
+
# Branding
|
|
70
|
+
primary: str = "#30A2FF"
|
|
71
|
+
secondary: str = "#FDB516"
|
|
72
|
+
tertiary: str = "#008080"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
StatusIcons: Annotated[
|
|
76
|
+
Mapping[str, str],
|
|
77
|
+
"Mapping of status levels to unicode icon characters for visual indicators",
|
|
78
|
+
] = {
|
|
79
|
+
"debug": "…",
|
|
80
|
+
"info": "ℹ",
|
|
81
|
+
"warning": "⚠",
|
|
82
|
+
"error": "✖",
|
|
83
|
+
"critical": "‼",
|
|
84
|
+
"notset": "⟳",
|
|
85
|
+
"success": "✔",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
StatusStyles: Annotated[
|
|
89
|
+
Mapping[str, str],
|
|
90
|
+
"Mapping of status levels to Rich console style strings for colored output",
|
|
91
|
+
] = {
|
|
92
|
+
"debug": "dim",
|
|
93
|
+
"info": f"bold {Colors.info}",
|
|
94
|
+
"warning": f"bold {Colors.warning}",
|
|
95
|
+
"error": f"bold {Colors.error}",
|
|
96
|
+
"critical": "bold red reverse",
|
|
97
|
+
"notset": f"bold {Colors.progress}",
|
|
98
|
+
"success": f"bold {Colors.success}",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class ConsoleUpdateStep:
|
|
104
|
+
"""
|
|
105
|
+
Context manager for multi-step progress operations with spinner.
|
|
106
|
+
|
|
107
|
+
Displays animated spinner during operation execution and allows dynamic
|
|
108
|
+
status updates. Automatically stops spinner on exit and prints final
|
|
109
|
+
status message. Designed for use with Python's `with` statement.
|
|
110
|
+
|
|
111
|
+
Example:
|
|
112
|
+
::
|
|
113
|
+
console = Console()
|
|
114
|
+
with console.print_update_step("Processing data") as step:
|
|
115
|
+
step.update("Loading files", "info")
|
|
116
|
+
# ... do work ...
|
|
117
|
+
step.finish("Completed successfully", status_level="success")
|
|
118
|
+
|
|
119
|
+
:param console: The Console instance to use for output
|
|
120
|
+
:param title: Initial progress message to display
|
|
121
|
+
:param details: Optional additional details to show after completion
|
|
122
|
+
:param status_level: Initial status level determining style and icon
|
|
123
|
+
:param spinner: Spinner animation style name from Rich's spinner set
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
console: Console
|
|
127
|
+
title: str
|
|
128
|
+
details: Any | None = None
|
|
129
|
+
status_level: StatusLevel = "info"
|
|
130
|
+
spinner: str = "dots"
|
|
131
|
+
_status: Status | None = None
|
|
132
|
+
|
|
133
|
+
def __enter__(self) -> ConsoleUpdateStep:
|
|
134
|
+
if self.console.quiet:
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
style = StatusStyles.get(self.status_level, "bold")
|
|
138
|
+
self._status = self.console.status(
|
|
139
|
+
f"[{style}]{self.title}[/]",
|
|
140
|
+
spinner=self.spinner,
|
|
141
|
+
)
|
|
142
|
+
self._status.__enter__()
|
|
143
|
+
return self
|
|
144
|
+
|
|
145
|
+
def update(self, title: str, status_level: StatusLevel | None = None):
|
|
146
|
+
"""
|
|
147
|
+
Update the progress message and optionally the status level.
|
|
148
|
+
|
|
149
|
+
:param title: New progress message to display
|
|
150
|
+
:param status_level: Optional new status level to apply
|
|
151
|
+
"""
|
|
152
|
+
self.title = title
|
|
153
|
+
if status_level is not None:
|
|
154
|
+
self.status_level = status_level
|
|
155
|
+
|
|
156
|
+
if self._status:
|
|
157
|
+
style = StatusStyles.get(self.status_level, "bold")
|
|
158
|
+
self._status.update(status=f"[{style}]{title}[/]")
|
|
159
|
+
|
|
160
|
+
def finish(
|
|
161
|
+
self,
|
|
162
|
+
title: str,
|
|
163
|
+
details: Any | None = None,
|
|
164
|
+
status_level: StatusLevel = "info",
|
|
165
|
+
):
|
|
166
|
+
"""
|
|
167
|
+
Stop the spinner and print the final status message.
|
|
168
|
+
|
|
169
|
+
:param title: Final completion message to display
|
|
170
|
+
:param details: Optional additional information to show below message
|
|
171
|
+
:param status_level: Status level for final message styling
|
|
172
|
+
"""
|
|
173
|
+
self.title = title
|
|
174
|
+
self.status_level = status_level
|
|
175
|
+
|
|
176
|
+
if self._status:
|
|
177
|
+
self._status.stop()
|
|
178
|
+
|
|
179
|
+
self.console.print_update(title, details, status_level)
|
|
180
|
+
|
|
181
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
182
|
+
if self._status:
|
|
183
|
+
self._status.__exit__(exc_type, exc_val, exc_tb)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class Console(RichConsole):
|
|
187
|
+
"""
|
|
188
|
+
Extended Rich console with custom formatting and status reporting.
|
|
189
|
+
|
|
190
|
+
Enhances Rich's Console with specialized methods for status messages,
|
|
191
|
+
progress tracking with spinners, and formatted table output. Provides
|
|
192
|
+
consistent styling through predefined status levels, icons, and colors.
|
|
193
|
+
Supports quiet mode to suppress non-critical output.
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
::
|
|
197
|
+
console = Console()
|
|
198
|
+
console.print_update("Starting process", status="info")
|
|
199
|
+
with console.print_update_step("Loading data") as step:
|
|
200
|
+
step.update("Processing items")
|
|
201
|
+
step.finish("Complete", status_level="success")
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
def print_update(
|
|
205
|
+
self,
|
|
206
|
+
title: str,
|
|
207
|
+
details: Any | None = None,
|
|
208
|
+
status: StatusLevel = "info",
|
|
209
|
+
):
|
|
210
|
+
"""
|
|
211
|
+
Print a status message with icon and optional details.
|
|
212
|
+
|
|
213
|
+
:param title: Main status message to display
|
|
214
|
+
:param details: Optional additional details shown indented below message
|
|
215
|
+
:param status: Status level determining icon and styling
|
|
216
|
+
"""
|
|
217
|
+
icon = StatusIcons.get(status, "•")
|
|
218
|
+
style = StatusStyles.get(status, "bold")
|
|
219
|
+
line = Text.assemble(f"{icon} ", (title, style))
|
|
220
|
+
self.print(line)
|
|
221
|
+
self.print_update_details(details)
|
|
222
|
+
|
|
223
|
+
def print_update_details(self, details: Any | None):
|
|
224
|
+
"""
|
|
225
|
+
Print additional details indented below a status message.
|
|
226
|
+
|
|
227
|
+
:param details: Content to display, converted to string and styled dimly
|
|
228
|
+
"""
|
|
229
|
+
if details:
|
|
230
|
+
block = Padding(
|
|
231
|
+
Text.from_markup(str(details)),
|
|
232
|
+
(0, 0, 0, 2),
|
|
233
|
+
style=StatusStyles.get("debug", "dim"),
|
|
234
|
+
)
|
|
235
|
+
self.print(block)
|
|
236
|
+
|
|
237
|
+
def print_update_step(
|
|
238
|
+
self,
|
|
239
|
+
title: str,
|
|
240
|
+
status: StatusLevel = "info",
|
|
241
|
+
details: Any | None = None,
|
|
242
|
+
spinner: str = "dots",
|
|
243
|
+
) -> ConsoleUpdateStep:
|
|
244
|
+
"""
|
|
245
|
+
Create a context manager for multi-step progress with spinner.
|
|
246
|
+
|
|
247
|
+
:param title: Initial progress message to display
|
|
248
|
+
:param status: Initial status level for styling
|
|
249
|
+
:param details: Optional details to show after completion
|
|
250
|
+
:param spinner: Spinner animation style name
|
|
251
|
+
:return: ConsoleUpdateStep context manager for progress tracking
|
|
252
|
+
"""
|
|
253
|
+
return ConsoleUpdateStep(
|
|
254
|
+
console=self,
|
|
255
|
+
title=title,
|
|
256
|
+
details=details,
|
|
257
|
+
status_level=status,
|
|
258
|
+
spinner=spinner,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def print_tables(
|
|
262
|
+
self,
|
|
263
|
+
header_cols_groups: Sequence[Sequence[str | list[str]]],
|
|
264
|
+
value_cols_groups: Sequence[Sequence[str | list[str]]],
|
|
265
|
+
title: str | None = None,
|
|
266
|
+
widths: Sequence[int] | None = None,
|
|
267
|
+
):
|
|
268
|
+
"""
|
|
269
|
+
Print multiple tables with uniform column widths.
|
|
270
|
+
|
|
271
|
+
:param header_cols_groups: List of header column groups for each table
|
|
272
|
+
:param value_cols_groups: List of value column groups for each table
|
|
273
|
+
:param title: Optional title to display before tables
|
|
274
|
+
:param widths: Optional minimum column widths to enforce
|
|
275
|
+
"""
|
|
276
|
+
if title is not None:
|
|
277
|
+
self.print_update(title, None, "info")
|
|
278
|
+
|
|
279
|
+
# Format all groups to determine uniform widths
|
|
280
|
+
widths = widths or None
|
|
281
|
+
headers = []
|
|
282
|
+
values = []
|
|
283
|
+
|
|
284
|
+
# Process all tables to get consistent widths
|
|
285
|
+
for value_cols in value_cols_groups:
|
|
286
|
+
formatted, widths = self._format_table_columns(value_cols, widths)
|
|
287
|
+
values.append(formatted)
|
|
288
|
+
for header_cols in header_cols_groups:
|
|
289
|
+
formatted, widths = self._format_table_headers(header_cols, widths)
|
|
290
|
+
headers.append(formatted)
|
|
291
|
+
|
|
292
|
+
# Print each table
|
|
293
|
+
for ind, (header, value) in enumerate(zip(headers, values, strict=False)):
|
|
294
|
+
is_last = ind == len(headers) - 1
|
|
295
|
+
self.print_table(
|
|
296
|
+
header,
|
|
297
|
+
value,
|
|
298
|
+
widths=widths,
|
|
299
|
+
apply_formatting=False,
|
|
300
|
+
print_bottom_divider=is_last,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def print_table(
|
|
304
|
+
self,
|
|
305
|
+
header_cols: Sequence[str | list[str]],
|
|
306
|
+
value_cols: Sequence[str | list[str]],
|
|
307
|
+
title: str | None = None,
|
|
308
|
+
widths: Sequence[int] | None = None,
|
|
309
|
+
apply_formatting: bool = True,
|
|
310
|
+
print_bottom_divider: bool = True,
|
|
311
|
+
):
|
|
312
|
+
"""
|
|
313
|
+
Print a formatted table with headers and values.
|
|
314
|
+
|
|
315
|
+
:param header_cols: List of header columns, each string or list of strings
|
|
316
|
+
:param value_cols: List of value columns, each string or list of strings
|
|
317
|
+
:param title: Optional title to display before table
|
|
318
|
+
:param widths: Optional minimum column widths to enforce
|
|
319
|
+
:param apply_formatting: Whether to calculate widths and format columns
|
|
320
|
+
:param print_bottom_divider: Whether to print bottom border line
|
|
321
|
+
"""
|
|
322
|
+
if title is not None:
|
|
323
|
+
self.print_update(title, None, "info")
|
|
324
|
+
|
|
325
|
+
# Format data
|
|
326
|
+
values: list[list[str]]
|
|
327
|
+
headers: list[list[str]]
|
|
328
|
+
final_widths: list[int]
|
|
329
|
+
|
|
330
|
+
if apply_formatting:
|
|
331
|
+
values, final_widths = self._format_table_columns(value_cols, widths)
|
|
332
|
+
headers, final_widths = self._format_table_headers(
|
|
333
|
+
header_cols, final_widths
|
|
334
|
+
)
|
|
335
|
+
else:
|
|
336
|
+
values = [col if isinstance(col, list) else [col] for col in value_cols]
|
|
337
|
+
headers = [col if isinstance(col, list) else [col] for col in header_cols]
|
|
338
|
+
final_widths = list(widths) if widths else []
|
|
339
|
+
|
|
340
|
+
# Print table structure
|
|
341
|
+
self.print_table_divider(final_widths, "=")
|
|
342
|
+
self.print_table_headers(headers, final_widths)
|
|
343
|
+
self.print_table_divider(final_widths, "-")
|
|
344
|
+
self.print_table_values(values, final_widths)
|
|
345
|
+
|
|
346
|
+
if print_bottom_divider:
|
|
347
|
+
self.print_table_divider(final_widths, "=")
|
|
348
|
+
|
|
349
|
+
def print_table_divider(self, widths: Sequence[int], char: str):
|
|
350
|
+
"""
|
|
351
|
+
Print a horizontal divider line across table columns.
|
|
352
|
+
|
|
353
|
+
:param widths: Column widths for divider line
|
|
354
|
+
:param char: Character to use for divider line (e.g., '=', '-')
|
|
355
|
+
"""
|
|
356
|
+
self.print_table_row(
|
|
357
|
+
[""] * len(widths),
|
|
358
|
+
widths=widths,
|
|
359
|
+
spacer=char,
|
|
360
|
+
cell_style="bold",
|
|
361
|
+
divider_style="bold",
|
|
362
|
+
edge_style="bold",
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
def print_table_headers(self, headers: Sequence[list[str]], widths: Sequence[int]):
|
|
366
|
+
"""
|
|
367
|
+
Print header rows with support for column spanning.
|
|
368
|
+
|
|
369
|
+
:param headers: List of header columns, each containing header row values
|
|
370
|
+
:param widths: Column widths for proper alignment
|
|
371
|
+
"""
|
|
372
|
+
if not headers or not headers[0]:
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
for row_idx in range(len(headers[0])):
|
|
376
|
+
# Calculate widths for this header row, accounting for merged cells.
|
|
377
|
+
row_widths = list(widths)
|
|
378
|
+
for col_idx in range(len(headers)):
|
|
379
|
+
if not headers[col_idx][row_idx]:
|
|
380
|
+
continue
|
|
381
|
+
|
|
382
|
+
# Find span end
|
|
383
|
+
span_end = col_idx + 1
|
|
384
|
+
while span_end < len(headers) and not headers[span_end][row_idx]:
|
|
385
|
+
row_widths[span_end] = 0
|
|
386
|
+
span_end += 1
|
|
387
|
+
|
|
388
|
+
# Set combined width for the first cell in span
|
|
389
|
+
row_widths[col_idx] = sum(
|
|
390
|
+
widths[col] for col in range(col_idx, span_end)
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Print the header row
|
|
394
|
+
self.print_table_row(
|
|
395
|
+
values=[headers[col][row_idx] for col in range(len(headers))],
|
|
396
|
+
widths=row_widths,
|
|
397
|
+
cell_style="bold",
|
|
398
|
+
divider_style="bold",
|
|
399
|
+
edge_style="bold",
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
def print_table_values(self, values: Sequence[list[str]], widths: Sequence[int]):
|
|
403
|
+
"""
|
|
404
|
+
Print all data rows in the table.
|
|
405
|
+
|
|
406
|
+
:param values: List of value columns, each containing row values
|
|
407
|
+
:param widths: Column widths for proper alignment
|
|
408
|
+
"""
|
|
409
|
+
if not values:
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
for row_idx in range(len(values[0])):
|
|
413
|
+
# Print the value row
|
|
414
|
+
self.print_table_row(
|
|
415
|
+
values=[values[col][row_idx] for col in range(len(values))],
|
|
416
|
+
widths=widths,
|
|
417
|
+
divider="|",
|
|
418
|
+
edge_style="bold",
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
def print_table_row(
|
|
422
|
+
self,
|
|
423
|
+
values: Sequence[str],
|
|
424
|
+
widths: Sequence[int] | None = None,
|
|
425
|
+
spacer: str = " ",
|
|
426
|
+
divider: str = "|",
|
|
427
|
+
cell_style: str = "",
|
|
428
|
+
value_style: str = "",
|
|
429
|
+
divider_style: str = "",
|
|
430
|
+
edge_style: str = "",
|
|
431
|
+
):
|
|
432
|
+
"""
|
|
433
|
+
Print a single table row with custom styling.
|
|
434
|
+
|
|
435
|
+
:param values: Cell values for the row
|
|
436
|
+
:param widths: Column widths, defaults to value lengths
|
|
437
|
+
:param spacer: Character for padding cells
|
|
438
|
+
:param divider: Character separating columns
|
|
439
|
+
:param cell_style: Rich style string for entire cells
|
|
440
|
+
:param value_style: Rich style string for cell values only
|
|
441
|
+
:param divider_style: Rich style string for column dividers
|
|
442
|
+
:param edge_style: Rich style string for table edges
|
|
443
|
+
"""
|
|
444
|
+
widths = widths or [len(val) for val in values]
|
|
445
|
+
|
|
446
|
+
# Build styled cells
|
|
447
|
+
cells = []
|
|
448
|
+
for val, width in zip(values, widths, strict=True):
|
|
449
|
+
cell = val.ljust(width, spacer)
|
|
450
|
+
if value_style and val:
|
|
451
|
+
cell = cell.replace(val, f"[{value_style}]{val}[/{value_style}]")
|
|
452
|
+
if cell_style:
|
|
453
|
+
cell = f"[{cell_style}]{cell}[/{cell_style}]"
|
|
454
|
+
cells.append(cell)
|
|
455
|
+
|
|
456
|
+
# Build and print row
|
|
457
|
+
edge = f"[{edge_style}]{divider}[/{edge_style}]" if edge_style else divider
|
|
458
|
+
inner = (
|
|
459
|
+
f"[{divider_style}]{divider}[/{divider_style}]"
|
|
460
|
+
if divider_style
|
|
461
|
+
else divider
|
|
462
|
+
)
|
|
463
|
+
line = edge + inner.join(cells) + edge
|
|
464
|
+
self.print(line, overflow="ignore", crop=False)
|
|
465
|
+
|
|
466
|
+
def _format_table_headers(
|
|
467
|
+
self,
|
|
468
|
+
headers: Sequence[str | list[str]],
|
|
469
|
+
col_widths: Sequence[int] | None = None,
|
|
470
|
+
spacer: str = " ",
|
|
471
|
+
min_padding: int = 1,
|
|
472
|
+
) -> tuple[list[list[str]], list[int]]:
|
|
473
|
+
formatted, header_widths = self._format_table_columns(
|
|
474
|
+
headers, col_widths, spacer, min_padding
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
if not formatted or not formatted[0]:
|
|
478
|
+
return formatted, []
|
|
479
|
+
|
|
480
|
+
# Merge identical adjacent headers row by row
|
|
481
|
+
widths = list(col_widths) if col_widths else header_widths
|
|
482
|
+
for row_idx in range(len(formatted[0])):
|
|
483
|
+
last_value = None
|
|
484
|
+
start_col = -1
|
|
485
|
+
|
|
486
|
+
for col_idx in range(len(formatted) + 1):
|
|
487
|
+
cur_value = (
|
|
488
|
+
formatted[col_idx][row_idx] if col_idx < len(formatted) else None
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Check if we should continue merging
|
|
492
|
+
if (
|
|
493
|
+
col_idx < len(formatted)
|
|
494
|
+
and cur_value != ""
|
|
495
|
+
and cur_value == last_value
|
|
496
|
+
and (
|
|
497
|
+
row_idx == 0
|
|
498
|
+
or headers[start_col][row_idx - 1]
|
|
499
|
+
== headers[col_idx][row_idx - 1]
|
|
500
|
+
)
|
|
501
|
+
):
|
|
502
|
+
continue
|
|
503
|
+
|
|
504
|
+
# Finalize previous
|
|
505
|
+
if start_col >= 0:
|
|
506
|
+
# Clear merged cells to keep only the first
|
|
507
|
+
for col in range(start_col + 1, col_idx):
|
|
508
|
+
formatted[col][row_idx] = ""
|
|
509
|
+
|
|
510
|
+
# Adjust widths of columns in the merged span, if needed
|
|
511
|
+
if (required := len(formatted[start_col][row_idx])) > (
|
|
512
|
+
current := sum(widths[col] for col in range(start_col, col_idx))
|
|
513
|
+
):
|
|
514
|
+
diff = required - current
|
|
515
|
+
cols_count = col_idx - start_col
|
|
516
|
+
per_col = diff // cols_count
|
|
517
|
+
extra = diff % cols_count
|
|
518
|
+
|
|
519
|
+
for col in range(start_col, col_idx):
|
|
520
|
+
widths[col] += per_col
|
|
521
|
+
if extra > 0:
|
|
522
|
+
widths[col] += 1
|
|
523
|
+
extra -= 1
|
|
524
|
+
|
|
525
|
+
# Start new merge
|
|
526
|
+
last_value = cur_value
|
|
527
|
+
start_col = col_idx
|
|
528
|
+
|
|
529
|
+
return formatted, widths
|
|
530
|
+
|
|
531
|
+
def _format_table_columns(
|
|
532
|
+
self,
|
|
533
|
+
columns: Sequence[str | list[str]],
|
|
534
|
+
col_widths: Sequence[int] | None = None,
|
|
535
|
+
spacer: str = " ",
|
|
536
|
+
min_padding: int = 1,
|
|
537
|
+
) -> tuple[list[list[str]], list[int]]:
|
|
538
|
+
if not columns:
|
|
539
|
+
return [], []
|
|
540
|
+
|
|
541
|
+
# Normalize to list of lists
|
|
542
|
+
max_rows = max(len(col) if isinstance(col, list) else 1 for col in columns)
|
|
543
|
+
|
|
544
|
+
formatted = []
|
|
545
|
+
for col in columns:
|
|
546
|
+
col_list = col if isinstance(col, list) else [col]
|
|
547
|
+
# Pad to max height
|
|
548
|
+
col_list = col_list + [""] * (max_rows - len(col_list))
|
|
549
|
+
# Add cell padding
|
|
550
|
+
padding = spacer * min_padding
|
|
551
|
+
col_list = [
|
|
552
|
+
f"{padding}{item}{padding}" if item else "" for item in col_list
|
|
553
|
+
]
|
|
554
|
+
formatted.append(col_list)
|
|
555
|
+
|
|
556
|
+
# Calculate widths
|
|
557
|
+
widths = [max(len(row) for row in col) for col in formatted]
|
|
558
|
+
|
|
559
|
+
# Apply minimum widths if provided
|
|
560
|
+
if col_widths is not None:
|
|
561
|
+
widths = [
|
|
562
|
+
max(width, min_w)
|
|
563
|
+
for width, min_w in zip(widths, col_widths, strict=True)
|
|
564
|
+
]
|
|
565
|
+
|
|
566
|
+
return formatted, widths
|