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.
Files changed (149) hide show
  1. provide/__init__.py +15 -0
  2. provide/foundation/__init__.py +155 -0
  3. provide/foundation/_version.py +58 -0
  4. provide/foundation/cli/__init__.py +67 -0
  5. provide/foundation/cli/commands/__init__.py +3 -0
  6. provide/foundation/cli/commands/deps.py +71 -0
  7. provide/foundation/cli/commands/logs/__init__.py +63 -0
  8. provide/foundation/cli/commands/logs/generate.py +357 -0
  9. provide/foundation/cli/commands/logs/generate_old.py +569 -0
  10. provide/foundation/cli/commands/logs/query.py +174 -0
  11. provide/foundation/cli/commands/logs/send.py +166 -0
  12. provide/foundation/cli/commands/logs/tail.py +112 -0
  13. provide/foundation/cli/decorators.py +262 -0
  14. provide/foundation/cli/main.py +65 -0
  15. provide/foundation/cli/testing.py +220 -0
  16. provide/foundation/cli/utils.py +210 -0
  17. provide/foundation/config/__init__.py +106 -0
  18. provide/foundation/config/base.py +295 -0
  19. provide/foundation/config/env.py +369 -0
  20. provide/foundation/config/loader.py +311 -0
  21. provide/foundation/config/manager.py +387 -0
  22. provide/foundation/config/schema.py +284 -0
  23. provide/foundation/config/sync.py +281 -0
  24. provide/foundation/config/types.py +78 -0
  25. provide/foundation/config/validators.py +80 -0
  26. provide/foundation/console/__init__.py +29 -0
  27. provide/foundation/console/input.py +364 -0
  28. provide/foundation/console/output.py +178 -0
  29. provide/foundation/context/__init__.py +12 -0
  30. provide/foundation/context/core.py +356 -0
  31. provide/foundation/core.py +20 -0
  32. provide/foundation/crypto/__init__.py +182 -0
  33. provide/foundation/crypto/algorithms.py +111 -0
  34. provide/foundation/crypto/certificates.py +896 -0
  35. provide/foundation/crypto/checksums.py +301 -0
  36. provide/foundation/crypto/constants.py +57 -0
  37. provide/foundation/crypto/hashing.py +265 -0
  38. provide/foundation/crypto/keys.py +188 -0
  39. provide/foundation/crypto/signatures.py +144 -0
  40. provide/foundation/crypto/utils.py +164 -0
  41. provide/foundation/errors/__init__.py +96 -0
  42. provide/foundation/errors/auth.py +73 -0
  43. provide/foundation/errors/base.py +81 -0
  44. provide/foundation/errors/config.py +103 -0
  45. provide/foundation/errors/context.py +299 -0
  46. provide/foundation/errors/decorators.py +484 -0
  47. provide/foundation/errors/handlers.py +360 -0
  48. provide/foundation/errors/integration.py +105 -0
  49. provide/foundation/errors/platform.py +37 -0
  50. provide/foundation/errors/process.py +140 -0
  51. provide/foundation/errors/resources.py +133 -0
  52. provide/foundation/errors/runtime.py +160 -0
  53. provide/foundation/errors/safe_decorators.py +133 -0
  54. provide/foundation/errors/types.py +276 -0
  55. provide/foundation/file/__init__.py +79 -0
  56. provide/foundation/file/atomic.py +157 -0
  57. provide/foundation/file/directory.py +134 -0
  58. provide/foundation/file/formats.py +236 -0
  59. provide/foundation/file/lock.py +175 -0
  60. provide/foundation/file/safe.py +179 -0
  61. provide/foundation/file/utils.py +170 -0
  62. provide/foundation/hub/__init__.py +88 -0
  63. provide/foundation/hub/click_builder.py +310 -0
  64. provide/foundation/hub/commands.py +42 -0
  65. provide/foundation/hub/components.py +640 -0
  66. provide/foundation/hub/decorators.py +244 -0
  67. provide/foundation/hub/info.py +32 -0
  68. provide/foundation/hub/manager.py +446 -0
  69. provide/foundation/hub/registry.py +279 -0
  70. provide/foundation/hub/type_mapping.py +54 -0
  71. provide/foundation/hub/types.py +28 -0
  72. provide/foundation/logger/__init__.py +41 -0
  73. provide/foundation/logger/base.py +22 -0
  74. provide/foundation/logger/config/__init__.py +16 -0
  75. provide/foundation/logger/config/base.py +40 -0
  76. provide/foundation/logger/config/logging.py +394 -0
  77. provide/foundation/logger/config/telemetry.py +188 -0
  78. provide/foundation/logger/core.py +239 -0
  79. provide/foundation/logger/custom_processors.py +172 -0
  80. provide/foundation/logger/emoji/__init__.py +44 -0
  81. provide/foundation/logger/emoji/matrix.py +209 -0
  82. provide/foundation/logger/emoji/sets.py +458 -0
  83. provide/foundation/logger/emoji/types.py +56 -0
  84. provide/foundation/logger/factories.py +56 -0
  85. provide/foundation/logger/processors/__init__.py +13 -0
  86. provide/foundation/logger/processors/main.py +254 -0
  87. provide/foundation/logger/processors/trace.py +113 -0
  88. provide/foundation/logger/ratelimit/__init__.py +31 -0
  89. provide/foundation/logger/ratelimit/limiters.py +294 -0
  90. provide/foundation/logger/ratelimit/processor.py +203 -0
  91. provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
  92. provide/foundation/logger/setup/__init__.py +29 -0
  93. provide/foundation/logger/setup/coordinator.py +138 -0
  94. provide/foundation/logger/setup/emoji_resolver.py +64 -0
  95. provide/foundation/logger/setup/processors.py +85 -0
  96. provide/foundation/logger/setup/testing.py +39 -0
  97. provide/foundation/logger/trace.py +38 -0
  98. provide/foundation/metrics/__init__.py +119 -0
  99. provide/foundation/metrics/otel.py +122 -0
  100. provide/foundation/metrics/simple.py +165 -0
  101. provide/foundation/observability/__init__.py +53 -0
  102. provide/foundation/observability/openobserve/__init__.py +79 -0
  103. provide/foundation/observability/openobserve/auth.py +72 -0
  104. provide/foundation/observability/openobserve/client.py +307 -0
  105. provide/foundation/observability/openobserve/commands.py +357 -0
  106. provide/foundation/observability/openobserve/exceptions.py +41 -0
  107. provide/foundation/observability/openobserve/formatters.py +298 -0
  108. provide/foundation/observability/openobserve/models.py +134 -0
  109. provide/foundation/observability/openobserve/otlp.py +320 -0
  110. provide/foundation/observability/openobserve/search.py +222 -0
  111. provide/foundation/observability/openobserve/streaming.py +235 -0
  112. provide/foundation/platform/__init__.py +44 -0
  113. provide/foundation/platform/detection.py +193 -0
  114. provide/foundation/platform/info.py +157 -0
  115. provide/foundation/process/__init__.py +39 -0
  116. provide/foundation/process/async_runner.py +373 -0
  117. provide/foundation/process/lifecycle.py +406 -0
  118. provide/foundation/process/runner.py +390 -0
  119. provide/foundation/setup/__init__.py +101 -0
  120. provide/foundation/streams/__init__.py +44 -0
  121. provide/foundation/streams/console.py +57 -0
  122. provide/foundation/streams/core.py +65 -0
  123. provide/foundation/streams/file.py +104 -0
  124. provide/foundation/testing/__init__.py +166 -0
  125. provide/foundation/testing/cli.py +227 -0
  126. provide/foundation/testing/crypto.py +163 -0
  127. provide/foundation/testing/fixtures.py +49 -0
  128. provide/foundation/testing/hub.py +23 -0
  129. provide/foundation/testing/logger.py +106 -0
  130. provide/foundation/testing/streams.py +54 -0
  131. provide/foundation/tracer/__init__.py +49 -0
  132. provide/foundation/tracer/context.py +115 -0
  133. provide/foundation/tracer/otel.py +135 -0
  134. provide/foundation/tracer/spans.py +174 -0
  135. provide/foundation/types.py +32 -0
  136. provide/foundation/utils/__init__.py +97 -0
  137. provide/foundation/utils/deps.py +195 -0
  138. provide/foundation/utils/env.py +491 -0
  139. provide/foundation/utils/formatting.py +483 -0
  140. provide/foundation/utils/parsing.py +235 -0
  141. provide/foundation/utils/rate_limiting.py +112 -0
  142. provide/foundation/utils/streams.py +67 -0
  143. provide/foundation/utils/timing.py +93 -0
  144. provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
  145. provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
  146. provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
  147. provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  148. provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
  149. 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