provide-foundation 0.0.0.dev0__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.
- provide/__init__.py +15 -0
- provide/foundation/__init__.py +155 -0
- provide/foundation/_version.py +58 -0
- provide/foundation/cli/__init__.py +67 -0
- provide/foundation/cli/commands/__init__.py +3 -0
- provide/foundation/cli/commands/deps.py +71 -0
- provide/foundation/cli/commands/logs/__init__.py +63 -0
- provide/foundation/cli/commands/logs/generate.py +357 -0
- provide/foundation/cli/commands/logs/generate_old.py +569 -0
- provide/foundation/cli/commands/logs/query.py +174 -0
- provide/foundation/cli/commands/logs/send.py +166 -0
- provide/foundation/cli/commands/logs/tail.py +112 -0
- provide/foundation/cli/decorators.py +262 -0
- provide/foundation/cli/main.py +65 -0
- provide/foundation/cli/testing.py +220 -0
- provide/foundation/cli/utils.py +210 -0
- provide/foundation/config/__init__.py +106 -0
- provide/foundation/config/base.py +295 -0
- provide/foundation/config/env.py +369 -0
- provide/foundation/config/loader.py +311 -0
- provide/foundation/config/manager.py +387 -0
- provide/foundation/config/schema.py +284 -0
- provide/foundation/config/sync.py +281 -0
- provide/foundation/config/types.py +78 -0
- provide/foundation/config/validators.py +80 -0
- provide/foundation/console/__init__.py +29 -0
- provide/foundation/console/input.py +364 -0
- provide/foundation/console/output.py +178 -0
- provide/foundation/context/__init__.py +12 -0
- provide/foundation/context/core.py +356 -0
- provide/foundation/core.py +20 -0
- provide/foundation/crypto/__init__.py +182 -0
- provide/foundation/crypto/algorithms.py +111 -0
- provide/foundation/crypto/certificates.py +896 -0
- provide/foundation/crypto/checksums.py +301 -0
- provide/foundation/crypto/constants.py +57 -0
- provide/foundation/crypto/hashing.py +265 -0
- provide/foundation/crypto/keys.py +188 -0
- provide/foundation/crypto/signatures.py +144 -0
- provide/foundation/crypto/utils.py +164 -0
- provide/foundation/errors/__init__.py +96 -0
- provide/foundation/errors/auth.py +73 -0
- provide/foundation/errors/base.py +81 -0
- provide/foundation/errors/config.py +103 -0
- provide/foundation/errors/context.py +299 -0
- provide/foundation/errors/decorators.py +484 -0
- provide/foundation/errors/handlers.py +360 -0
- provide/foundation/errors/integration.py +105 -0
- provide/foundation/errors/platform.py +37 -0
- provide/foundation/errors/process.py +140 -0
- provide/foundation/errors/resources.py +133 -0
- provide/foundation/errors/runtime.py +160 -0
- provide/foundation/errors/safe_decorators.py +133 -0
- provide/foundation/errors/types.py +276 -0
- provide/foundation/file/__init__.py +79 -0
- provide/foundation/file/atomic.py +157 -0
- provide/foundation/file/directory.py +134 -0
- provide/foundation/file/formats.py +236 -0
- provide/foundation/file/lock.py +175 -0
- provide/foundation/file/safe.py +179 -0
- provide/foundation/file/utils.py +170 -0
- provide/foundation/hub/__init__.py +88 -0
- provide/foundation/hub/click_builder.py +310 -0
- provide/foundation/hub/commands.py +42 -0
- provide/foundation/hub/components.py +640 -0
- provide/foundation/hub/decorators.py +244 -0
- provide/foundation/hub/info.py +32 -0
- provide/foundation/hub/manager.py +446 -0
- provide/foundation/hub/registry.py +279 -0
- provide/foundation/hub/type_mapping.py +54 -0
- provide/foundation/hub/types.py +28 -0
- provide/foundation/logger/__init__.py +41 -0
- provide/foundation/logger/base.py +22 -0
- provide/foundation/logger/config/__init__.py +16 -0
- provide/foundation/logger/config/base.py +40 -0
- provide/foundation/logger/config/logging.py +394 -0
- provide/foundation/logger/config/telemetry.py +188 -0
- provide/foundation/logger/core.py +239 -0
- provide/foundation/logger/custom_processors.py +172 -0
- provide/foundation/logger/emoji/__init__.py +44 -0
- provide/foundation/logger/emoji/matrix.py +209 -0
- provide/foundation/logger/emoji/sets.py +458 -0
- provide/foundation/logger/emoji/types.py +56 -0
- provide/foundation/logger/factories.py +56 -0
- provide/foundation/logger/processors/__init__.py +13 -0
- provide/foundation/logger/processors/main.py +254 -0
- provide/foundation/logger/processors/trace.py +113 -0
- provide/foundation/logger/ratelimit/__init__.py +31 -0
- provide/foundation/logger/ratelimit/limiters.py +294 -0
- provide/foundation/logger/ratelimit/processor.py +203 -0
- provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
- provide/foundation/logger/setup/__init__.py +29 -0
- provide/foundation/logger/setup/coordinator.py +138 -0
- provide/foundation/logger/setup/emoji_resolver.py +64 -0
- provide/foundation/logger/setup/processors.py +85 -0
- provide/foundation/logger/setup/testing.py +39 -0
- provide/foundation/logger/trace.py +38 -0
- provide/foundation/metrics/__init__.py +119 -0
- provide/foundation/metrics/otel.py +122 -0
- provide/foundation/metrics/simple.py +165 -0
- provide/foundation/observability/__init__.py +53 -0
- provide/foundation/observability/openobserve/__init__.py +79 -0
- provide/foundation/observability/openobserve/auth.py +72 -0
- provide/foundation/observability/openobserve/client.py +307 -0
- provide/foundation/observability/openobserve/commands.py +357 -0
- provide/foundation/observability/openobserve/exceptions.py +41 -0
- provide/foundation/observability/openobserve/formatters.py +298 -0
- provide/foundation/observability/openobserve/models.py +134 -0
- provide/foundation/observability/openobserve/otlp.py +320 -0
- provide/foundation/observability/openobserve/search.py +222 -0
- provide/foundation/observability/openobserve/streaming.py +235 -0
- provide/foundation/platform/__init__.py +44 -0
- provide/foundation/platform/detection.py +193 -0
- provide/foundation/platform/info.py +157 -0
- provide/foundation/process/__init__.py +39 -0
- provide/foundation/process/async_runner.py +373 -0
- provide/foundation/process/lifecycle.py +406 -0
- provide/foundation/process/runner.py +390 -0
- provide/foundation/setup/__init__.py +101 -0
- provide/foundation/streams/__init__.py +44 -0
- provide/foundation/streams/console.py +57 -0
- provide/foundation/streams/core.py +65 -0
- provide/foundation/streams/file.py +104 -0
- provide/foundation/testing/__init__.py +166 -0
- provide/foundation/testing/cli.py +227 -0
- provide/foundation/testing/crypto.py +163 -0
- provide/foundation/testing/fixtures.py +49 -0
- provide/foundation/testing/hub.py +23 -0
- provide/foundation/testing/logger.py +106 -0
- provide/foundation/testing/streams.py +54 -0
- provide/foundation/tracer/__init__.py +49 -0
- provide/foundation/tracer/context.py +115 -0
- provide/foundation/tracer/otel.py +135 -0
- provide/foundation/tracer/spans.py +174 -0
- provide/foundation/types.py +32 -0
- provide/foundation/utils/__init__.py +97 -0
- provide/foundation/utils/deps.py +195 -0
- provide/foundation/utils/env.py +491 -0
- provide/foundation/utils/formatting.py +483 -0
- provide/foundation/utils/parsing.py +235 -0
- provide/foundation/utils/rate_limiting.py +112 -0
- provide/foundation/utils/streams.py +67 -0
- provide/foundation/utils/timing.py +93 -0
- provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
- provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
- provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
- provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
- provide_foundation-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,483 @@
|
|
1
|
+
"""String formatting and text utilities.
|
2
|
+
|
3
|
+
Provides utilities for human-readable formatting of sizes, durations,
|
4
|
+
and other common string operations.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import Any
|
8
|
+
|
9
|
+
|
10
|
+
def format_size(size_bytes: int | float, precision: int = 1) -> str:
|
11
|
+
"""Format bytes as human-readable size.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
size_bytes: Size in bytes
|
15
|
+
precision: Decimal places for display
|
16
|
+
|
17
|
+
Returns:
|
18
|
+
Human-readable size string
|
19
|
+
|
20
|
+
Examples:
|
21
|
+
>>> format_size(1024)
|
22
|
+
'1.0 KB'
|
23
|
+
>>> format_size(1536)
|
24
|
+
'1.5 KB'
|
25
|
+
>>> format_size(1073741824)
|
26
|
+
'1.0 GB'
|
27
|
+
>>> format_size(0)
|
28
|
+
'0 B'
|
29
|
+
"""
|
30
|
+
if size_bytes == 0:
|
31
|
+
return "0 B"
|
32
|
+
|
33
|
+
# Handle negative sizes
|
34
|
+
negative = size_bytes < 0
|
35
|
+
size_bytes = abs(size_bytes)
|
36
|
+
|
37
|
+
units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"]
|
38
|
+
unit_index = 0
|
39
|
+
|
40
|
+
while size_bytes >= 1024.0 and unit_index < len(units) - 1:
|
41
|
+
size_bytes /= 1024.0
|
42
|
+
unit_index += 1
|
43
|
+
|
44
|
+
# Format with specified precision
|
45
|
+
if unit_index == 0:
|
46
|
+
# Bytes - no decimal places
|
47
|
+
formatted = f"{int(size_bytes)} {units[unit_index]}"
|
48
|
+
else:
|
49
|
+
formatted = f"{size_bytes:.{precision}f} {units[unit_index]}"
|
50
|
+
|
51
|
+
return f"-{formatted}" if negative else formatted
|
52
|
+
|
53
|
+
|
54
|
+
def format_duration(seconds: int | float, short: bool = False) -> str:
|
55
|
+
"""Format seconds as human-readable duration.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
seconds: Duration in seconds
|
59
|
+
short: Use short format (1h30m vs 1 hour 30 minutes)
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
Human-readable duration string
|
63
|
+
|
64
|
+
Examples:
|
65
|
+
>>> format_duration(90)
|
66
|
+
'1 minute 30 seconds'
|
67
|
+
>>> format_duration(90, short=True)
|
68
|
+
'1m30s'
|
69
|
+
>>> format_duration(3661)
|
70
|
+
'1 hour 1 minute 1 second'
|
71
|
+
>>> format_duration(3661, short=True)
|
72
|
+
'1h1m1s'
|
73
|
+
"""
|
74
|
+
if seconds < 0:
|
75
|
+
return f"-{format_duration(abs(seconds), short)}"
|
76
|
+
|
77
|
+
if seconds == 0:
|
78
|
+
return "0s" if short else "0 seconds"
|
79
|
+
|
80
|
+
# Calculate components
|
81
|
+
days = int(seconds // 86400)
|
82
|
+
hours = int((seconds % 86400) // 3600)
|
83
|
+
minutes = int((seconds % 3600) // 60)
|
84
|
+
secs = int(seconds % 60)
|
85
|
+
|
86
|
+
parts = []
|
87
|
+
|
88
|
+
if short:
|
89
|
+
if days > 0:
|
90
|
+
parts.append(f"{days}d")
|
91
|
+
if hours > 0:
|
92
|
+
parts.append(f"{hours}h")
|
93
|
+
if minutes > 0:
|
94
|
+
parts.append(f"{minutes}m")
|
95
|
+
if secs > 0 or not parts:
|
96
|
+
parts.append(f"{secs}s")
|
97
|
+
return "".join(parts)
|
98
|
+
else:
|
99
|
+
if days > 0:
|
100
|
+
parts.append(f"{days} day{'s' if days != 1 else ''}")
|
101
|
+
if hours > 0:
|
102
|
+
parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
|
103
|
+
if minutes > 0:
|
104
|
+
parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
|
105
|
+
if secs > 0 or not parts:
|
106
|
+
parts.append(f"{secs} second{'s' if secs != 1 else ''}")
|
107
|
+
return " ".join(parts)
|
108
|
+
|
109
|
+
|
110
|
+
def format_number(num: int | float, precision: int | None = None) -> str:
|
111
|
+
"""Format number with thousands separators.
|
112
|
+
|
113
|
+
Args:
|
114
|
+
num: Number to format
|
115
|
+
precision: Decimal places (None for automatic)
|
116
|
+
|
117
|
+
Returns:
|
118
|
+
Formatted number string
|
119
|
+
|
120
|
+
Examples:
|
121
|
+
>>> format_number(1234567)
|
122
|
+
'1,234,567'
|
123
|
+
>>> format_number(1234.5678, precision=2)
|
124
|
+
'1,234.57'
|
125
|
+
"""
|
126
|
+
if precision is None:
|
127
|
+
if isinstance(num, int):
|
128
|
+
return f"{num:,}"
|
129
|
+
else:
|
130
|
+
# Auto precision for floats
|
131
|
+
return f"{num:,.6f}".rstrip("0").rstrip(".")
|
132
|
+
else:
|
133
|
+
return f"{num:,.{precision}f}"
|
134
|
+
|
135
|
+
|
136
|
+
def format_percentage(
|
137
|
+
value: float, precision: int = 1, include_sign: bool = False
|
138
|
+
) -> str:
|
139
|
+
"""Format value as percentage.
|
140
|
+
|
141
|
+
Args:
|
142
|
+
value: Value to format (0.5 = 50%)
|
143
|
+
precision: Decimal places
|
144
|
+
include_sign: Include + sign for positive values
|
145
|
+
|
146
|
+
Returns:
|
147
|
+
Formatted percentage string
|
148
|
+
|
149
|
+
Examples:
|
150
|
+
>>> format_percentage(0.5)
|
151
|
+
'50.0%'
|
152
|
+
>>> format_percentage(0.1234, precision=2)
|
153
|
+
'12.34%'
|
154
|
+
>>> format_percentage(0.05, include_sign=True)
|
155
|
+
'+5.0%'
|
156
|
+
"""
|
157
|
+
percentage = value * 100
|
158
|
+
formatted = f"{percentage:.{precision}f}%"
|
159
|
+
|
160
|
+
if include_sign and value > 0:
|
161
|
+
formatted = f"+{formatted}"
|
162
|
+
|
163
|
+
return formatted
|
164
|
+
|
165
|
+
|
166
|
+
def truncate(
|
167
|
+
text: str, max_length: int, suffix: str = "...", whole_words: bool = True
|
168
|
+
) -> str:
|
169
|
+
"""Truncate text to maximum length.
|
170
|
+
|
171
|
+
Args:
|
172
|
+
text: Text to truncate
|
173
|
+
max_length: Maximum length including suffix
|
174
|
+
suffix: Suffix to append when truncated
|
175
|
+
whole_words: Truncate at word boundaries
|
176
|
+
|
177
|
+
Returns:
|
178
|
+
Truncated text
|
179
|
+
|
180
|
+
Examples:
|
181
|
+
>>> truncate("Hello world", 8)
|
182
|
+
'Hello...'
|
183
|
+
>>> truncate("Hello world", 8, whole_words=False)
|
184
|
+
'Hello...'
|
185
|
+
"""
|
186
|
+
if len(text) <= max_length:
|
187
|
+
return text
|
188
|
+
|
189
|
+
if max_length <= len(suffix):
|
190
|
+
return suffix[:max_length]
|
191
|
+
|
192
|
+
truncate_at = max_length - len(suffix)
|
193
|
+
|
194
|
+
if whole_words:
|
195
|
+
# Find last space before truncate point
|
196
|
+
space_pos = text.rfind(" ", 0, truncate_at)
|
197
|
+
if space_pos > 0:
|
198
|
+
truncate_at = space_pos
|
199
|
+
|
200
|
+
return text[:truncate_at] + suffix
|
201
|
+
|
202
|
+
|
203
|
+
def pluralize(count: int, singular: str, plural: str | None = None) -> str:
|
204
|
+
"""Get singular or plural form based on count.
|
205
|
+
|
206
|
+
Args:
|
207
|
+
count: Item count
|
208
|
+
singular: Singular form
|
209
|
+
plural: Plural form (default: singular + 's')
|
210
|
+
|
211
|
+
Returns:
|
212
|
+
Appropriate singular/plural form with count
|
213
|
+
|
214
|
+
Examples:
|
215
|
+
>>> pluralize(1, "file")
|
216
|
+
'1 file'
|
217
|
+
>>> pluralize(5, "file")
|
218
|
+
'5 files'
|
219
|
+
>>> pluralize(2, "child", "children")
|
220
|
+
'2 children'
|
221
|
+
"""
|
222
|
+
if plural is None:
|
223
|
+
plural = f"{singular}s"
|
224
|
+
|
225
|
+
word = singular if count == 1 else plural
|
226
|
+
return f"{count} {word}"
|
227
|
+
|
228
|
+
|
229
|
+
def indent(text: str, spaces: int = 2, first_line: bool = True) -> str:
|
230
|
+
"""Indent text lines.
|
231
|
+
|
232
|
+
Args:
|
233
|
+
text: Text to indent
|
234
|
+
spaces: Number of spaces to indent
|
235
|
+
first_line: Whether to indent the first line
|
236
|
+
|
237
|
+
Returns:
|
238
|
+
Indented text
|
239
|
+
|
240
|
+
Examples:
|
241
|
+
>>> indent("line1\\nline2", 4)
|
242
|
+
' line1\\n line2'
|
243
|
+
"""
|
244
|
+
indent_str = " " * spaces
|
245
|
+
lines = text.splitlines()
|
246
|
+
|
247
|
+
if not lines:
|
248
|
+
return text
|
249
|
+
|
250
|
+
result = []
|
251
|
+
for i, line in enumerate(lines):
|
252
|
+
if i == 0 and not first_line:
|
253
|
+
result.append(line)
|
254
|
+
else:
|
255
|
+
result.append(indent_str + line if line else "")
|
256
|
+
|
257
|
+
return "\n".join(result)
|
258
|
+
|
259
|
+
|
260
|
+
def wrap_text(
|
261
|
+
text: str, width: int = 80, indent_first: int = 0, indent_rest: int = 0
|
262
|
+
) -> str:
|
263
|
+
"""Wrap text to specified width.
|
264
|
+
|
265
|
+
Args:
|
266
|
+
text: Text to wrap
|
267
|
+
width: Maximum line width
|
268
|
+
indent_first: Spaces to indent first line
|
269
|
+
indent_rest: Spaces to indent remaining lines
|
270
|
+
|
271
|
+
Returns:
|
272
|
+
Wrapped text
|
273
|
+
"""
|
274
|
+
import textwrap
|
275
|
+
|
276
|
+
wrapper = textwrap.TextWrapper(
|
277
|
+
width=width,
|
278
|
+
initial_indent=" " * indent_first,
|
279
|
+
subsequent_indent=" " * indent_rest,
|
280
|
+
break_long_words=False,
|
281
|
+
break_on_hyphens=False,
|
282
|
+
)
|
283
|
+
|
284
|
+
return wrapper.fill(text)
|
285
|
+
|
286
|
+
|
287
|
+
def strip_ansi(text: str) -> str:
|
288
|
+
"""Strip ANSI color codes from text.
|
289
|
+
|
290
|
+
Args:
|
291
|
+
text: Text with potential ANSI codes
|
292
|
+
|
293
|
+
Returns:
|
294
|
+
Text without ANSI codes
|
295
|
+
"""
|
296
|
+
import re
|
297
|
+
|
298
|
+
ansi_pattern = re.compile(r"\x1b\[[0-9;]*m")
|
299
|
+
return ansi_pattern.sub("", text)
|
300
|
+
|
301
|
+
|
302
|
+
def to_snake_case(text: str) -> str:
|
303
|
+
"""Convert text to snake_case.
|
304
|
+
|
305
|
+
Args:
|
306
|
+
text: Text to convert
|
307
|
+
|
308
|
+
Returns:
|
309
|
+
snake_case text
|
310
|
+
|
311
|
+
Examples:
|
312
|
+
>>> to_snake_case("HelloWorld")
|
313
|
+
'hello_world'
|
314
|
+
>>> to_snake_case("some-kebab-case")
|
315
|
+
'some_kebab_case'
|
316
|
+
"""
|
317
|
+
import re
|
318
|
+
|
319
|
+
# Replace hyphens with underscores
|
320
|
+
text = text.replace("-", "_")
|
321
|
+
|
322
|
+
# Insert underscore before uppercase letters
|
323
|
+
text = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", text)
|
324
|
+
|
325
|
+
# Convert to lowercase
|
326
|
+
return text.lower()
|
327
|
+
|
328
|
+
|
329
|
+
def to_kebab_case(text: str) -> str:
|
330
|
+
"""Convert text to kebab-case.
|
331
|
+
|
332
|
+
Args:
|
333
|
+
text: Text to convert
|
334
|
+
|
335
|
+
Returns:
|
336
|
+
kebab-case text
|
337
|
+
|
338
|
+
Examples:
|
339
|
+
>>> to_kebab_case("HelloWorld")
|
340
|
+
'hello-world'
|
341
|
+
>>> to_kebab_case("some_snake_case")
|
342
|
+
'some-snake-case'
|
343
|
+
"""
|
344
|
+
import re
|
345
|
+
|
346
|
+
# Replace underscores with hyphens
|
347
|
+
text = text.replace("_", "-")
|
348
|
+
|
349
|
+
# Insert hyphen before uppercase letters
|
350
|
+
text = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", text)
|
351
|
+
|
352
|
+
# Convert to lowercase
|
353
|
+
return text.lower()
|
354
|
+
|
355
|
+
|
356
|
+
def to_camel_case(text: str, upper_first: bool = False) -> str:
|
357
|
+
"""Convert text to camelCase or PascalCase.
|
358
|
+
|
359
|
+
Args:
|
360
|
+
text: Text to convert
|
361
|
+
upper_first: Use PascalCase instead of camelCase
|
362
|
+
|
363
|
+
Returns:
|
364
|
+
camelCase or PascalCase text
|
365
|
+
|
366
|
+
Examples:
|
367
|
+
>>> to_camel_case("hello_world")
|
368
|
+
'helloWorld'
|
369
|
+
>>> to_camel_case("hello-world", upper_first=True)
|
370
|
+
'HelloWorld'
|
371
|
+
"""
|
372
|
+
import re
|
373
|
+
|
374
|
+
# Split on underscores, hyphens, and spaces
|
375
|
+
parts = re.split(r"[-_\s]+", text)
|
376
|
+
|
377
|
+
if not parts:
|
378
|
+
return text
|
379
|
+
|
380
|
+
# Capitalize each part except possibly the first
|
381
|
+
result = []
|
382
|
+
for i, part in enumerate(parts):
|
383
|
+
if i == 0 and not upper_first:
|
384
|
+
result.append(part.lower())
|
385
|
+
else:
|
386
|
+
result.append(part.capitalize())
|
387
|
+
|
388
|
+
return "".join(result)
|
389
|
+
|
390
|
+
|
391
|
+
def format_table(
|
392
|
+
headers: list[str], rows: list[list[Any]], alignment: list[str] | None = None
|
393
|
+
) -> str:
|
394
|
+
"""Format data as ASCII table.
|
395
|
+
|
396
|
+
Args:
|
397
|
+
headers: Column headers
|
398
|
+
rows: Data rows
|
399
|
+
alignment: Column alignments ('l', 'r', 'c')
|
400
|
+
|
401
|
+
Returns:
|
402
|
+
Formatted table string
|
403
|
+
|
404
|
+
Examples:
|
405
|
+
>>> headers = ['Name', 'Age']
|
406
|
+
>>> rows = [['Alice', 30], ['Bob', 25]]
|
407
|
+
>>> print(format_table(headers, rows))
|
408
|
+
Name | Age
|
409
|
+
------|----
|
410
|
+
Alice | 30
|
411
|
+
Bob | 25
|
412
|
+
"""
|
413
|
+
if not headers and not rows:
|
414
|
+
return ""
|
415
|
+
|
416
|
+
# Convert all cells to strings
|
417
|
+
str_headers = [str(h) for h in headers]
|
418
|
+
str_rows = [[str(cell) for cell in row] for row in rows]
|
419
|
+
|
420
|
+
# Calculate column widths
|
421
|
+
widths = [len(h) for h in str_headers]
|
422
|
+
for row in str_rows:
|
423
|
+
for i, cell in enumerate(row):
|
424
|
+
if i < len(widths):
|
425
|
+
widths[i] = max(widths[i], len(cell))
|
426
|
+
|
427
|
+
# Default alignment
|
428
|
+
if alignment is None:
|
429
|
+
alignment = ["l"] * len(headers)
|
430
|
+
|
431
|
+
# Format header
|
432
|
+
header_parts = []
|
433
|
+
separator_parts = []
|
434
|
+
|
435
|
+
for i, (header, width) in enumerate(zip(str_headers, widths, strict=False)):
|
436
|
+
align = alignment[i] if i < len(alignment) else "l"
|
437
|
+
|
438
|
+
if align == "r":
|
439
|
+
header_parts.append(header.rjust(width))
|
440
|
+
elif align == "c":
|
441
|
+
header_parts.append(header.center(width))
|
442
|
+
else:
|
443
|
+
header_parts.append(header.ljust(width))
|
444
|
+
|
445
|
+
separator_parts.append("-" * width)
|
446
|
+
|
447
|
+
lines = [" | ".join(header_parts), "-|-".join(separator_parts)]
|
448
|
+
|
449
|
+
# Format rows
|
450
|
+
for row in str_rows:
|
451
|
+
row_parts = []
|
452
|
+
for i, cell in enumerate(row):
|
453
|
+
if i < len(widths):
|
454
|
+
width = widths[i]
|
455
|
+
align = alignment[i] if i < len(alignment) else "l"
|
456
|
+
|
457
|
+
if align == "r":
|
458
|
+
row_parts.append(cell.rjust(width))
|
459
|
+
elif align == "c":
|
460
|
+
row_parts.append(cell.center(width))
|
461
|
+
else:
|
462
|
+
row_parts.append(cell.ljust(width))
|
463
|
+
|
464
|
+
lines.append(" | ".join(row_parts))
|
465
|
+
|
466
|
+
return "\n".join(lines)
|
467
|
+
|
468
|
+
|
469
|
+
__all__ = [
|
470
|
+
"format_duration",
|
471
|
+
"format_number",
|
472
|
+
"format_percentage",
|
473
|
+
"format_size",
|
474
|
+
"format_table",
|
475
|
+
"indent",
|
476
|
+
"pluralize",
|
477
|
+
"strip_ansi",
|
478
|
+
"to_camel_case",
|
479
|
+
"to_kebab_case",
|
480
|
+
"to_snake_case",
|
481
|
+
"truncate",
|
482
|
+
"wrap_text",
|
483
|
+
]
|
@@ -0,0 +1,235 @@
|
|
1
|
+
"""
|
2
|
+
Type parsing and conversion utilities.
|
3
|
+
|
4
|
+
Provides utilities for converting string values (from environment variables,
|
5
|
+
config files, CLI args, etc.) to proper Python types based on type hints.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
from typing import Any, TypeVar, get_args, get_origin
|
11
|
+
|
12
|
+
T = TypeVar("T")
|
13
|
+
|
14
|
+
|
15
|
+
def parse_bool(value: Any, strict: bool = False) -> bool:
|
16
|
+
"""
|
17
|
+
Parse a boolean value from string or other types.
|
18
|
+
|
19
|
+
Accepts: true/false, yes/no, 1/0, on/off, enabled/disabled (case-insensitive)
|
20
|
+
|
21
|
+
Args:
|
22
|
+
value: Value to parse as boolean
|
23
|
+
strict: If True, only accept bool or string types (raise TypeError otherwise)
|
24
|
+
|
25
|
+
Returns:
|
26
|
+
Boolean value
|
27
|
+
|
28
|
+
Raises:
|
29
|
+
TypeError: If strict=True and value is not bool or string
|
30
|
+
ValueError: If value cannot be parsed as boolean
|
31
|
+
"""
|
32
|
+
if isinstance(value, bool):
|
33
|
+
return value
|
34
|
+
|
35
|
+
if strict and not isinstance(value, str):
|
36
|
+
raise TypeError(f"Cannot convert {type(value).__name__} to bool: {value!r}")
|
37
|
+
|
38
|
+
str_value = str(value).lower().strip()
|
39
|
+
|
40
|
+
if str_value in ("true", "yes", "1", "on", "enabled"):
|
41
|
+
return True
|
42
|
+
elif str_value in ("false", "no", "0", "off", "disabled", ""):
|
43
|
+
return False
|
44
|
+
else:
|
45
|
+
raise ValueError(f"Cannot parse '{value}' as boolean")
|
46
|
+
|
47
|
+
|
48
|
+
def parse_list(
|
49
|
+
value: str | list,
|
50
|
+
separator: str = ",",
|
51
|
+
strip: bool = True,
|
52
|
+
) -> list[str]:
|
53
|
+
"""
|
54
|
+
Parse a list from a string.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
value: String or list to parse
|
58
|
+
separator: Separator character
|
59
|
+
strip: Whether to strip whitespace from items
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
List of strings
|
63
|
+
"""
|
64
|
+
if isinstance(value, list):
|
65
|
+
return value
|
66
|
+
|
67
|
+
if not value:
|
68
|
+
return []
|
69
|
+
|
70
|
+
items = value.split(separator)
|
71
|
+
|
72
|
+
if strip:
|
73
|
+
items = [item.strip() for item in items]
|
74
|
+
|
75
|
+
return items
|
76
|
+
|
77
|
+
|
78
|
+
def parse_dict(
|
79
|
+
value: str | dict,
|
80
|
+
item_separator: str = ",",
|
81
|
+
key_separator: str = "=",
|
82
|
+
strip: bool = True,
|
83
|
+
) -> dict[str, str]:
|
84
|
+
"""
|
85
|
+
Parse a dictionary from a string.
|
86
|
+
|
87
|
+
Format: "key1=value1,key2=value2"
|
88
|
+
|
89
|
+
Args:
|
90
|
+
value: String or dict to parse
|
91
|
+
item_separator: Separator between items
|
92
|
+
key_separator: Separator between key and value
|
93
|
+
strip: Whether to strip whitespace
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
Dictionary of string keys and values
|
97
|
+
|
98
|
+
Raises:
|
99
|
+
ValueError: If format is invalid
|
100
|
+
"""
|
101
|
+
if isinstance(value, dict):
|
102
|
+
return value
|
103
|
+
|
104
|
+
if not value:
|
105
|
+
return {}
|
106
|
+
|
107
|
+
result = {}
|
108
|
+
items = value.split(item_separator)
|
109
|
+
|
110
|
+
for item in items:
|
111
|
+
if not item:
|
112
|
+
continue
|
113
|
+
|
114
|
+
if key_separator not in item:
|
115
|
+
raise ValueError(f"Invalid dict format: '{item}' missing '{key_separator}'")
|
116
|
+
|
117
|
+
key, val = item.split(key_separator, 1)
|
118
|
+
|
119
|
+
if strip:
|
120
|
+
key = key.strip()
|
121
|
+
val = val.strip()
|
122
|
+
|
123
|
+
result[key] = val
|
124
|
+
|
125
|
+
return result
|
126
|
+
|
127
|
+
|
128
|
+
def parse_typed_value(value: str, target_type: type) -> Any:
|
129
|
+
"""
|
130
|
+
Parse a string value to a specific type.
|
131
|
+
|
132
|
+
Handles basic types (int, float, bool, str) and generic types (list, dict).
|
133
|
+
For attrs fields, pass field.type as target_type.
|
134
|
+
|
135
|
+
Args:
|
136
|
+
value: String value to parse
|
137
|
+
target_type: Target type to convert to
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
Parsed value of the target type
|
141
|
+
|
142
|
+
Examples:
|
143
|
+
>>> parse_typed_value("42", int)
|
144
|
+
42
|
145
|
+
>>> parse_typed_value("true", bool)
|
146
|
+
True
|
147
|
+
>>> parse_typed_value("a,b,c", list)
|
148
|
+
['a', 'b', 'c']
|
149
|
+
"""
|
150
|
+
if value is None:
|
151
|
+
return None
|
152
|
+
|
153
|
+
# Handle basic types
|
154
|
+
if target_type == bool:
|
155
|
+
return parse_bool(value)
|
156
|
+
elif target_type == int:
|
157
|
+
return int(value)
|
158
|
+
elif target_type == float:
|
159
|
+
return float(value)
|
160
|
+
elif target_type == str:
|
161
|
+
return value
|
162
|
+
|
163
|
+
# Handle generic types using typing module
|
164
|
+
origin = get_origin(target_type)
|
165
|
+
|
166
|
+
if origin == list:
|
167
|
+
# Handle list[T] - convert each item to the specified type
|
168
|
+
args = get_args(target_type)
|
169
|
+
if args and len(args) > 0:
|
170
|
+
item_type = args[0]
|
171
|
+
str_list = parse_list(value)
|
172
|
+
try:
|
173
|
+
# Convert each item to the target type
|
174
|
+
return [parse_typed_value(item, item_type) for item in str_list]
|
175
|
+
except (ValueError, TypeError) as e:
|
176
|
+
raise ValueError(
|
177
|
+
f"Cannot convert list items to {item_type.__name__}: {e}"
|
178
|
+
)
|
179
|
+
else:
|
180
|
+
# list without type parameter, return as list[str]
|
181
|
+
return parse_list(value)
|
182
|
+
elif origin == dict:
|
183
|
+
return parse_dict(value)
|
184
|
+
elif origin is None:
|
185
|
+
# Not a generic type, try direct conversion
|
186
|
+
if target_type == list:
|
187
|
+
return parse_list(value)
|
188
|
+
elif target_type == dict:
|
189
|
+
return parse_dict(value)
|
190
|
+
|
191
|
+
# Default to string
|
192
|
+
return value
|
193
|
+
|
194
|
+
|
195
|
+
def auto_parse(attr: Any, value: str) -> Any:
|
196
|
+
"""
|
197
|
+
Automatically parse value based on an attrs field's type.
|
198
|
+
|
199
|
+
This is a convenience wrapper for parse_typed_value that extracts
|
200
|
+
the type from an attrs field.
|
201
|
+
|
202
|
+
Args:
|
203
|
+
attr: attrs field (from fields(Class))
|
204
|
+
value: String value to parse
|
205
|
+
|
206
|
+
Returns:
|
207
|
+
Parsed value based on field type
|
208
|
+
"""
|
209
|
+
# Get type hint from attrs field
|
210
|
+
if hasattr(attr, "type") and attr.type is not None:
|
211
|
+
field_type = attr.type
|
212
|
+
|
213
|
+
# Handle string type annotations (e.g., 'int', 'str', 'bool')
|
214
|
+
# This happens when attrs processes classes defined inside functions
|
215
|
+
if isinstance(field_type, str):
|
216
|
+
# Map common string type names to actual types
|
217
|
+
type_map = {
|
218
|
+
"int": int,
|
219
|
+
"float": float,
|
220
|
+
"str": str,
|
221
|
+
"bool": bool,
|
222
|
+
"list": list,
|
223
|
+
"dict": dict,
|
224
|
+
}
|
225
|
+
# Try to get the actual type from the map
|
226
|
+
field_type = type_map.get(field_type, field_type)
|
227
|
+
|
228
|
+
# If we still have a string, we can't parse it
|
229
|
+
if isinstance(field_type, str):
|
230
|
+
return value
|
231
|
+
|
232
|
+
return parse_typed_value(value, field_type)
|
233
|
+
|
234
|
+
# No type info, return as string
|
235
|
+
return value
|