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,491 @@
1
+ """Environment variable utilities with type coercion and prefix support.
2
+
3
+ Provides utilities for safely reading and parsing environment variables with
4
+ automatic type detection, prefix-based namespacing, and default value handling.
5
+ """
6
+
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Any, TypeVar, get_origin
10
+
11
+ from provide.foundation.errors.config import ValidationError
12
+ from provide.foundation.utils.parsing import parse_bool, parse_dict, parse_list
13
+
14
+
15
+ def _get_logger():
16
+ """Get logger instance lazily to avoid circular imports."""
17
+ from provide.foundation.logger import get_logger
18
+
19
+ return get_logger(__name__)
20
+
21
+
22
+ T = TypeVar("T")
23
+
24
+
25
+ def get_bool(name: str, default: bool | None = None) -> bool | None:
26
+ """Get boolean environment variable.
27
+
28
+ Args:
29
+ name: Environment variable name
30
+ default: Default value if not set
31
+
32
+ Returns:
33
+ Boolean value or default
34
+
35
+ Examples:
36
+ >>> os.environ['DEBUG'] = 'true'
37
+ >>> get_bool('DEBUG')
38
+ True
39
+ >>> get_bool('MISSING', False)
40
+ False
41
+ """
42
+ value = os.environ.get(name)
43
+ if value is None:
44
+ return default
45
+
46
+ try:
47
+ return parse_bool(value)
48
+ except ValueError as e:
49
+ raise ValidationError(
50
+ f"Invalid boolean value for {name}: {value}",
51
+ field=name,
52
+ value=value,
53
+ rule="boolean",
54
+ ) from e
55
+
56
+
57
+ def get_int(name: str, default: int | None = None) -> int | None:
58
+ """Get integer environment variable.
59
+
60
+ Args:
61
+ name: Environment variable name
62
+ default: Default value if not set
63
+
64
+ Returns:
65
+ Integer value or default
66
+
67
+ Raises:
68
+ ValidationError: If value cannot be parsed as integer
69
+ """
70
+ value = os.environ.get(name)
71
+ if value is None:
72
+ return default
73
+
74
+ try:
75
+ return int(value)
76
+ except ValueError as e:
77
+ raise ValidationError(
78
+ f"Invalid integer value for {name}: {value}",
79
+ field=name,
80
+ value=value,
81
+ rule="integer",
82
+ ) from e
83
+
84
+
85
+ def get_float(name: str, default: float | None = None) -> float | None:
86
+ """Get float environment variable.
87
+
88
+ Args:
89
+ name: Environment variable name
90
+ default: Default value if not set
91
+
92
+ Returns:
93
+ Float value or default
94
+
95
+ Raises:
96
+ ValidationError: If value cannot be parsed as float
97
+ """
98
+ value = os.environ.get(name)
99
+ if value is None:
100
+ return default
101
+
102
+ try:
103
+ return float(value)
104
+ except ValueError as e:
105
+ raise ValidationError(
106
+ f"Invalid float value for {name}: {value}",
107
+ field=name,
108
+ value=value,
109
+ rule="float",
110
+ ) from e
111
+
112
+
113
+ def get_str(name: str, default: str | None = None) -> str | None:
114
+ """Get string environment variable.
115
+
116
+ Args:
117
+ name: Environment variable name
118
+ default: Default value if not set
119
+
120
+ Returns:
121
+ String value or default
122
+ """
123
+ return os.environ.get(name, default)
124
+
125
+
126
+ def get_path(name: str, default: Path | str | None = None) -> Path | None:
127
+ """Get path environment variable.
128
+
129
+ Args:
130
+ name: Environment variable name
131
+ default: Default path if not set
132
+
133
+ Returns:
134
+ Path object or None
135
+ """
136
+ value = os.environ.get(name)
137
+ if value is None:
138
+ if default is None:
139
+ return None
140
+ return Path(default) if not isinstance(default, Path) else default
141
+
142
+ # Expand user and environment variables
143
+ expanded = os.path.expanduser(os.path.expandvars(value))
144
+ return Path(expanded)
145
+
146
+
147
+ def get_list(
148
+ name: str, default: list[str] | None = None, separator: str = ","
149
+ ) -> list[str]:
150
+ """Get list from environment variable.
151
+
152
+ Args:
153
+ name: Environment variable name
154
+ default: Default list if not set
155
+ separator: String separator (default: comma)
156
+
157
+ Returns:
158
+ List of strings
159
+
160
+ Examples:
161
+ >>> os.environ['ITEMS'] = 'a,b,c'
162
+ >>> get_list('ITEMS')
163
+ ['a', 'b', 'c']
164
+ """
165
+ value = os.environ.get(name)
166
+ if value is None:
167
+ return default or []
168
+
169
+ # Use existing parse_list which handles empty strings and stripping
170
+ items = parse_list(value, separator=separator, strip=True)
171
+ # Filter empty strings (parse_list doesn't do this by default)
172
+ return [item for item in items if item]
173
+
174
+
175
+ def get_dict(
176
+ name: str,
177
+ default: dict[str, str] | None = None,
178
+ item_separator: str = ",",
179
+ key_value_separator: str = "=",
180
+ ) -> dict[str, str]:
181
+ """Get dictionary from environment variable.
182
+
183
+ Args:
184
+ name: Environment variable name
185
+ default: Default dict if not set
186
+ item_separator: Separator between items
187
+ key_value_separator: Separator between key and value
188
+
189
+ Returns:
190
+ Dictionary of string key-value pairs
191
+
192
+ Examples:
193
+ >>> os.environ['CONFIG'] = 'key1=val1,key2=val2'
194
+ >>> get_dict('CONFIG')
195
+ {'key1': 'val1', 'key2': 'val2'}
196
+ """
197
+ value = os.environ.get(name)
198
+ if value is None:
199
+ return default or {}
200
+
201
+ try:
202
+ return parse_dict(
203
+ value,
204
+ item_separator=item_separator,
205
+ key_separator=key_value_separator,
206
+ strip=True,
207
+ )
208
+ except ValueError as e:
209
+ # parse_dict raises on invalid format, log warning and return partial result
210
+ _get_logger().warning(
211
+ "Invalid dictionary format in environment variable",
212
+ var=name,
213
+ value=value,
214
+ error=str(e),
215
+ )
216
+ # Try to parse what we can, skipping invalid items
217
+ result = {}
218
+ items = value.split(item_separator)
219
+ for item in items:
220
+ item = item.strip()
221
+ if not item:
222
+ continue
223
+ if key_value_separator not in item:
224
+ continue
225
+ key, val = item.split(key_value_separator, 1)
226
+ result[key.strip()] = val.strip()
227
+ return result
228
+
229
+
230
+ def require(name: str, type_hint: type[T] | None = None) -> Any:
231
+ """Require an environment variable to be set.
232
+
233
+ Args:
234
+ name: Environment variable name
235
+ type_hint: Optional type hint for parsing
236
+
237
+ Returns:
238
+ Parsed value
239
+
240
+ Raises:
241
+ ValidationError: If variable is not set
242
+ """
243
+ if name not in os.environ:
244
+ raise ValidationError(
245
+ f"Required environment variable not set: {name}",
246
+ field=name,
247
+ rule="required",
248
+ )
249
+
250
+ if type_hint is None:
251
+ return os.environ[name]
252
+
253
+ # Parse based on type hint
254
+ origin = get_origin(type_hint)
255
+ if origin is None:
256
+ # Simple type
257
+ if type_hint is bool:
258
+ return get_bool(name)
259
+ elif type_hint is int:
260
+ return get_int(name)
261
+ elif type_hint is float:
262
+ return get_float(name)
263
+ elif type_hint is str:
264
+ return get_str(name)
265
+ elif type_hint is Path:
266
+ return get_path(name)
267
+ elif origin is list:
268
+ return get_list(name)
269
+ elif origin is dict:
270
+ return get_dict(name)
271
+
272
+ # Fallback to string
273
+ return os.environ[name]
274
+
275
+
276
+ class EnvPrefix:
277
+ """Environment variable reader with prefix support.
278
+
279
+ Provides convenient access to environment variables with a common prefix,
280
+ useful for application-specific configuration namespacing.
281
+
282
+ Examples:
283
+ >>> app_env = EnvPrefix('MYAPP')
284
+ >>> app_env.get_bool('DEBUG') # Reads MYAPP_DEBUG
285
+ >>> app_env['database_url'] # Reads MYAPP_DATABASE_URL
286
+ """
287
+
288
+ def __init__(self, prefix: str, separator: str = "_") -> None:
289
+ """Initialize with prefix.
290
+
291
+ Args:
292
+ prefix: Prefix for all environment variables
293
+ separator: Separator between prefix and variable name
294
+ """
295
+ self.prefix = prefix.upper()
296
+ self.separator = separator
297
+
298
+ def _make_name(self, name: str) -> str:
299
+ """Create full environment variable name."""
300
+ # Convert to uppercase and replace common separators
301
+ name = name.upper().replace("-", "_").replace(".", "_")
302
+ return f"{self.prefix}{self.separator}{name}"
303
+
304
+ def get_bool(self, name: str, default: bool | None = None) -> bool | None:
305
+ """Get boolean with prefix."""
306
+ return get_bool(self._make_name(name), default)
307
+
308
+ def get_int(self, name: str, default: int | None = None) -> int | None:
309
+ """Get integer with prefix."""
310
+ return get_int(self._make_name(name), default)
311
+
312
+ def get_float(self, name: str, default: float | None = None) -> float | None:
313
+ """Get float with prefix."""
314
+ return get_float(self._make_name(name), default)
315
+
316
+ def get_str(self, name: str, default: str | None = None) -> str | None:
317
+ """Get string with prefix."""
318
+ return get_str(self._make_name(name), default)
319
+
320
+ def get_path(self, name: str, default: Path | str | None = None) -> Path | None:
321
+ """Get path with prefix."""
322
+ return get_path(self._make_name(name), default)
323
+
324
+ def get_list(
325
+ self, name: str, default: list[str] | None = None, separator: str = ","
326
+ ) -> list[str]:
327
+ """Get list with prefix."""
328
+ return get_list(self._make_name(name), default, separator)
329
+
330
+ def get_dict(
331
+ self,
332
+ name: str,
333
+ default: dict[str, str] | None = None,
334
+ item_separator: str = ",",
335
+ key_value_separator: str = "=",
336
+ ) -> dict[str, str]:
337
+ """Get dictionary with prefix."""
338
+ return get_dict(
339
+ self._make_name(name), default, item_separator, key_value_separator
340
+ )
341
+
342
+ def require(self, name: str, type_hint: type[T] | None = None) -> Any:
343
+ """Require variable with prefix."""
344
+ return require(self._make_name(name), type_hint)
345
+
346
+ def __getitem__(self, name: str) -> str | None:
347
+ """Get environment variable using subscript notation."""
348
+ return self.get_str(name)
349
+
350
+ def __contains__(self, name: str) -> bool:
351
+ """Check if environment variable exists."""
352
+ return self._make_name(name) in os.environ
353
+
354
+ def all_with_prefix(self) -> dict[str, str]:
355
+ """Get all environment variables with this prefix.
356
+
357
+ Returns:
358
+ Dictionary of variable names (without prefix) to values
359
+ """
360
+ result = {}
361
+ prefix_with_sep = f"{self.prefix}{self.separator}"
362
+
363
+ for key, value in os.environ.items():
364
+ if key.startswith(prefix_with_sep):
365
+ # Remove prefix and add to result
366
+ var_name = key[len(prefix_with_sep) :]
367
+ result[var_name] = value
368
+
369
+ return result
370
+
371
+
372
+ def parse_duration(value: str) -> int:
373
+ """Parse duration string to seconds.
374
+
375
+ Supports formats like: 30s, 5m, 2h, 1d, 1h30m, etc.
376
+
377
+ Args:
378
+ value: Duration string
379
+
380
+ Returns:
381
+ Duration in seconds
382
+
383
+ Examples:
384
+ >>> parse_duration('30s')
385
+ 30
386
+ >>> parse_duration('1h30m')
387
+ 5400
388
+ >>> parse_duration('2d')
389
+ 172800
390
+ """
391
+ import re
392
+
393
+ if value.isdigit():
394
+ return int(value)
395
+
396
+ total_seconds = 0
397
+
398
+ # Pattern for duration components
399
+ pattern = r"(\d+)([dhms])"
400
+ matches = re.findall(pattern, value.lower())
401
+
402
+ if not matches:
403
+ raise ValidationError(
404
+ f"Invalid duration format: {value}", value=value, rule="duration"
405
+ )
406
+
407
+ units = {
408
+ "s": 1,
409
+ "m": 60,
410
+ "h": 3600,
411
+ "d": 86400,
412
+ }
413
+
414
+ for amount, unit in matches:
415
+ if unit in units:
416
+ total_seconds += int(amount) * units[unit]
417
+ else:
418
+ raise ValidationError(
419
+ f"Unknown duration unit: {unit}", value=value, rule="duration_unit"
420
+ )
421
+
422
+ return total_seconds
423
+
424
+
425
+ def parse_size(value: str) -> int:
426
+ """Parse size string to bytes.
427
+
428
+ Supports formats like: 1024, 1KB, 10MB, 1.5GB, etc.
429
+
430
+ Args:
431
+ value: Size string
432
+
433
+ Returns:
434
+ Size in bytes
435
+
436
+ Examples:
437
+ >>> parse_size('1024')
438
+ 1024
439
+ >>> parse_size('10MB')
440
+ 10485760
441
+ >>> parse_size('1.5GB')
442
+ 1610612736
443
+ """
444
+ import re
445
+
446
+ if value.isdigit():
447
+ return int(value)
448
+
449
+ # Pattern for size with unit
450
+ pattern = r"^(\d+(?:\.\d+)?)\s*([KMGT]?B?)$"
451
+ match = re.match(pattern, value.upper())
452
+
453
+ if not match:
454
+ raise ValidationError(f"Invalid size format: {value}", value=value, rule="size")
455
+
456
+ amount = float(match.group(1))
457
+ unit = match.group(2) or "B"
458
+
459
+ units = {
460
+ "B": 1,
461
+ "KB": 1024,
462
+ "K": 1024,
463
+ "MB": 1024**2,
464
+ "M": 1024**2,
465
+ "GB": 1024**3,
466
+ "G": 1024**3,
467
+ "TB": 1024**4,
468
+ "T": 1024**4,
469
+ }
470
+
471
+ if unit not in units:
472
+ raise ValidationError(
473
+ f"Unknown size unit: {unit}", value=value, rule="size_unit"
474
+ )
475
+
476
+ return int(amount * units[unit])
477
+
478
+
479
+ __all__ = [
480
+ "EnvPrefix",
481
+ "get_bool",
482
+ "get_dict",
483
+ "get_float",
484
+ "get_int",
485
+ "get_list",
486
+ "get_path",
487
+ "get_str",
488
+ "parse_duration",
489
+ "parse_size",
490
+ "require",
491
+ ]