provide-foundation 0.0.0.dev1__py3-none-any.whl → 0.0.0.dev3__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 (163) hide show
  1. provide/foundation/__init__.py +36 -10
  2. provide/foundation/archive/__init__.py +1 -1
  3. provide/foundation/archive/base.py +15 -14
  4. provide/foundation/archive/bzip2.py +40 -40
  5. provide/foundation/archive/gzip.py +42 -42
  6. provide/foundation/archive/operations.py +93 -96
  7. provide/foundation/archive/tar.py +33 -31
  8. provide/foundation/archive/zip.py +52 -50
  9. provide/foundation/asynctools/__init__.py +20 -0
  10. provide/foundation/asynctools/core.py +126 -0
  11. provide/foundation/cli/__init__.py +2 -2
  12. provide/foundation/cli/commands/deps.py +15 -9
  13. provide/foundation/cli/commands/logs/__init__.py +3 -3
  14. provide/foundation/cli/commands/logs/generate.py +2 -2
  15. provide/foundation/cli/commands/logs/query.py +4 -4
  16. provide/foundation/cli/commands/logs/send.py +3 -3
  17. provide/foundation/cli/commands/logs/tail.py +3 -3
  18. provide/foundation/cli/decorators.py +11 -11
  19. provide/foundation/cli/main.py +1 -1
  20. provide/foundation/cli/testing.py +2 -40
  21. provide/foundation/cli/utils.py +21 -18
  22. provide/foundation/config/__init__.py +35 -2
  23. provide/foundation/config/base.py +2 -2
  24. provide/foundation/config/converters.py +477 -0
  25. provide/foundation/config/defaults.py +67 -0
  26. provide/foundation/config/env.py +6 -20
  27. provide/foundation/config/loader.py +10 -4
  28. provide/foundation/config/sync.py +8 -6
  29. provide/foundation/config/types.py +5 -5
  30. provide/foundation/config/validators.py +4 -4
  31. provide/foundation/console/input.py +5 -5
  32. provide/foundation/console/output.py +36 -14
  33. provide/foundation/context/__init__.py +8 -4
  34. provide/foundation/context/core.py +88 -110
  35. provide/foundation/crypto/certificates/__init__.py +9 -5
  36. provide/foundation/crypto/certificates/base.py +2 -2
  37. provide/foundation/crypto/certificates/certificate.py +48 -19
  38. provide/foundation/crypto/certificates/factory.py +26 -18
  39. provide/foundation/crypto/certificates/generator.py +24 -23
  40. provide/foundation/crypto/certificates/loader.py +24 -16
  41. provide/foundation/crypto/certificates/operations.py +17 -10
  42. provide/foundation/crypto/certificates/trust.py +21 -21
  43. provide/foundation/env/__init__.py +28 -0
  44. provide/foundation/env/core.py +218 -0
  45. provide/foundation/errors/__init__.py +3 -3
  46. provide/foundation/errors/decorators.py +0 -234
  47. provide/foundation/errors/types.py +0 -98
  48. provide/foundation/eventsets/display.py +13 -14
  49. provide/foundation/eventsets/registry.py +61 -31
  50. provide/foundation/eventsets/resolver.py +50 -46
  51. provide/foundation/eventsets/sets/das.py +8 -8
  52. provide/foundation/eventsets/sets/database.py +14 -14
  53. provide/foundation/eventsets/sets/http.py +21 -21
  54. provide/foundation/eventsets/sets/llm.py +16 -16
  55. provide/foundation/eventsets/sets/task_queue.py +13 -13
  56. provide/foundation/eventsets/types.py +7 -7
  57. provide/foundation/file/directory.py +14 -23
  58. provide/foundation/file/lock.py +4 -3
  59. provide/foundation/hub/components.py +75 -389
  60. provide/foundation/hub/config.py +157 -0
  61. provide/foundation/hub/discovery.py +63 -0
  62. provide/foundation/hub/handlers.py +89 -0
  63. provide/foundation/hub/lifecycle.py +195 -0
  64. provide/foundation/hub/manager.py +7 -4
  65. provide/foundation/hub/processors.py +49 -0
  66. provide/foundation/integrations/__init__.py +11 -0
  67. provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
  68. provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
  69. provide/foundation/{observability → integrations}/openobserve/client.py +14 -14
  70. provide/foundation/{observability → integrations}/openobserve/commands.py +12 -12
  71. provide/foundation/integrations/openobserve/config.py +37 -0
  72. provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
  73. provide/foundation/{observability → integrations}/openobserve/otlp.py +2 -2
  74. provide/foundation/{observability → integrations}/openobserve/search.py +2 -3
  75. provide/foundation/{observability → integrations}/openobserve/streaming.py +5 -5
  76. provide/foundation/logger/__init__.py +0 -1
  77. provide/foundation/logger/config/base.py +1 -1
  78. provide/foundation/logger/config/logging.py +69 -299
  79. provide/foundation/logger/config/telemetry.py +39 -121
  80. provide/foundation/logger/factories.py +2 -2
  81. provide/foundation/logger/processors/main.py +12 -10
  82. provide/foundation/logger/ratelimit/limiters.py +4 -4
  83. provide/foundation/logger/ratelimit/processor.py +1 -1
  84. provide/foundation/logger/setup/coordinator.py +39 -25
  85. provide/foundation/logger/setup/processors.py +3 -3
  86. provide/foundation/logger/setup/testing.py +14 -0
  87. provide/foundation/logger/trace.py +5 -5
  88. provide/foundation/metrics/__init__.py +1 -1
  89. provide/foundation/metrics/otel.py +3 -1
  90. provide/foundation/observability/__init__.py +3 -3
  91. provide/foundation/process/__init__.py +9 -0
  92. provide/foundation/process/exit.py +48 -0
  93. provide/foundation/process/lifecycle.py +69 -46
  94. provide/foundation/resilience/__init__.py +36 -0
  95. provide/foundation/resilience/circuit.py +166 -0
  96. provide/foundation/resilience/decorators.py +236 -0
  97. provide/foundation/resilience/fallback.py +208 -0
  98. provide/foundation/resilience/retry.py +327 -0
  99. provide/foundation/serialization/__init__.py +16 -0
  100. provide/foundation/serialization/core.py +70 -0
  101. provide/foundation/streams/config.py +78 -0
  102. provide/foundation/streams/console.py +4 -5
  103. provide/foundation/streams/core.py +5 -2
  104. provide/foundation/streams/file.py +12 -2
  105. provide/foundation/testing/__init__.py +29 -9
  106. provide/foundation/testing/archive/__init__.py +7 -7
  107. provide/foundation/testing/archive/fixtures.py +58 -54
  108. provide/foundation/testing/cli.py +30 -20
  109. provide/foundation/testing/common/__init__.py +13 -15
  110. provide/foundation/testing/common/fixtures.py +27 -57
  111. provide/foundation/testing/file/__init__.py +15 -15
  112. provide/foundation/testing/file/content_fixtures.py +289 -0
  113. provide/foundation/testing/file/directory_fixtures.py +107 -0
  114. provide/foundation/testing/file/fixtures.py +42 -516
  115. provide/foundation/testing/file/special_fixtures.py +145 -0
  116. provide/foundation/testing/logger.py +89 -8
  117. provide/foundation/testing/mocking/__init__.py +21 -21
  118. provide/foundation/testing/mocking/fixtures.py +80 -67
  119. provide/foundation/testing/process/__init__.py +23 -23
  120. provide/foundation/testing/process/async_fixtures.py +414 -0
  121. provide/foundation/testing/process/fixtures.py +48 -571
  122. provide/foundation/testing/process/subprocess_fixtures.py +210 -0
  123. provide/foundation/testing/threading/__init__.py +17 -17
  124. provide/foundation/testing/threading/basic_fixtures.py +105 -0
  125. provide/foundation/testing/threading/data_fixtures.py +101 -0
  126. provide/foundation/testing/threading/execution_fixtures.py +278 -0
  127. provide/foundation/testing/threading/fixtures.py +32 -502
  128. provide/foundation/testing/threading/sync_fixtures.py +100 -0
  129. provide/foundation/testing/time/__init__.py +11 -11
  130. provide/foundation/testing/time/fixtures.py +95 -83
  131. provide/foundation/testing/transport/__init__.py +9 -9
  132. provide/foundation/testing/transport/fixtures.py +54 -54
  133. provide/foundation/time/__init__.py +18 -0
  134. provide/foundation/time/core.py +63 -0
  135. provide/foundation/tools/__init__.py +2 -2
  136. provide/foundation/tools/base.py +68 -67
  137. provide/foundation/tools/cache.py +69 -74
  138. provide/foundation/tools/downloader.py +68 -62
  139. provide/foundation/tools/installer.py +51 -57
  140. provide/foundation/tools/registry.py +38 -45
  141. provide/foundation/tools/resolver.py +70 -68
  142. provide/foundation/tools/verifier.py +39 -50
  143. provide/foundation/tracer/spans.py +2 -14
  144. provide/foundation/transport/__init__.py +26 -33
  145. provide/foundation/transport/base.py +32 -30
  146. provide/foundation/transport/client.py +44 -49
  147. provide/foundation/transport/config.py +36 -107
  148. provide/foundation/transport/errors.py +13 -27
  149. provide/foundation/transport/http.py +69 -55
  150. provide/foundation/transport/middleware.py +113 -114
  151. provide/foundation/transport/registry.py +29 -27
  152. provide/foundation/transport/types.py +6 -6
  153. provide/foundation/utils/deps.py +17 -14
  154. provide/foundation/utils/parsing.py +49 -4
  155. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
  156. provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
  157. provide_foundation-0.0.0.dev1.dist-info/RECORD +0 -200
  158. /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
  159. /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
  160. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
  161. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
  162. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
  163. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/top_level.txt +0 -0
@@ -20,31 +20,31 @@ class ConfigSource(Enum):
20
20
  ENV = 20
21
21
  RUNTIME = 30 # Highest precedence
22
22
 
23
- def __lt__(self, other):
23
+ def __lt__(self, other: object) -> bool:
24
24
  """Enable comparison for precedence."""
25
25
  if not isinstance(other, ConfigSource):
26
26
  return NotImplemented
27
27
  return self.value < other.value
28
28
 
29
- def __le__(self, other):
29
+ def __le__(self, other: object) -> bool:
30
30
  """Enable <= comparison for precedence."""
31
31
  if not isinstance(other, ConfigSource):
32
32
  return NotImplemented
33
33
  return self.value <= other.value
34
34
 
35
- def __gt__(self, other):
35
+ def __gt__(self, other: object) -> bool:
36
36
  """Enable > comparison for precedence."""
37
37
  if not isinstance(other, ConfigSource):
38
38
  return NotImplemented
39
39
  return self.value > other.value
40
40
 
41
- def __ge__(self, other):
41
+ def __ge__(self, other: object) -> bool:
42
42
  """Enable >= comparison for precedence."""
43
43
  if not isinstance(other, ConfigSource):
44
44
  return NotImplemented
45
45
  return self.value >= other.value
46
46
 
47
- def __eq__(self, other):
47
+ def __eq__(self, other: object) -> bool:
48
48
  """Enable == comparison for precedence."""
49
49
  if not isinstance(other, ConfigSource):
50
50
  return NotImplemented
@@ -22,7 +22,7 @@ def validate_choice(choices: list[Any]) -> Callable[[Any, Any, Any], None]:
22
22
  Validator function
23
23
  """
24
24
 
25
- def validator(instance, attribute, value):
25
+ def validator(instance: object, attribute: object, value: Any) -> None:
26
26
  if value not in choices:
27
27
  raise ValidationError(
28
28
  f"Invalid value '{value}' for {attribute.name}. "
@@ -46,7 +46,7 @@ def validate_range(
46
46
  Validator function
47
47
  """
48
48
 
49
- def validator(instance, attribute, value):
49
+ def validator(instance: object, attribute: object, value: Any) -> None:
50
50
  if not isinstance(value, (int, float)):
51
51
  raise ValidationError(f"Value must be a number, got {type(value).__name__}")
52
52
 
@@ -58,7 +58,7 @@ def validate_range(
58
58
  return validator
59
59
 
60
60
 
61
- def validate_positive(instance, attribute, value):
61
+ def validate_positive(instance: object, attribute: object, value: Any) -> None:
62
62
  """
63
63
  Validate that a numeric value is positive.
64
64
  """
@@ -69,7 +69,7 @@ def validate_positive(instance, attribute, value):
69
69
  raise ValidationError(f"Value must be positive, got {value}")
70
70
 
71
71
 
72
- def validate_non_negative(instance, attribute, value):
72
+ def validate_non_negative(instance: object, attribute: object, value: Any) -> None:
73
73
  """
74
74
  Validate that a numeric value is non-negative.
75
75
  """
@@ -19,7 +19,7 @@ except ImportError:
19
19
  click = None
20
20
  _HAS_CLICK = False
21
21
 
22
- from provide.foundation.context import Context
22
+ from provide.foundation.context import CLIContext
23
23
  from provide.foundation.logger import get_logger
24
24
 
25
25
  plog = get_logger(__name__)
@@ -27,24 +27,24 @@ plog = get_logger(__name__)
27
27
  T = TypeVar("T")
28
28
 
29
29
 
30
- def _get_context() -> Context | None:
30
+ def _get_context() -> CLIContext | None:
31
31
  """Get current context from Click or environment."""
32
32
  if not _HAS_CLICK:
33
33
  return None
34
34
  ctx = click.get_current_context(silent=True)
35
- if ctx and hasattr(ctx, "obj") and isinstance(ctx.obj, Context):
35
+ if ctx and hasattr(ctx, "obj") and isinstance(ctx.obj, CLIContext):
36
36
  return ctx.obj
37
37
  return None
38
38
 
39
39
 
40
- def _should_use_json(ctx: Context | None = None) -> bool:
40
+ def _should_use_json(ctx: CLIContext | None = None) -> bool:
41
41
  """Determine if JSON output should be used."""
42
42
  if ctx is None:
43
43
  ctx = _get_context()
44
44
  return ctx.json_output if ctx else False
45
45
 
46
46
 
47
- def _should_use_color(ctx: Context | None = None) -> bool:
47
+ def _should_use_color(ctx: CLIContext | None = None) -> bool:
48
48
  """Determine if color output should be used."""
49
49
  if ctx is None:
50
50
  ctx = _get_context()
@@ -6,6 +6,7 @@ for JSON mode, colors, and proper stream separation.
6
6
  """
7
7
 
8
8
  import json
9
+ import os
9
10
  import sys
10
11
  from typing import Any
11
12
 
@@ -17,34 +18,48 @@ except ImportError:
17
18
  click = None
18
19
  _HAS_CLICK = False
19
20
 
20
- from provide.foundation.context import Context
21
+ from provide.foundation.context import CLIContext
22
+ from provide.foundation.errors.decorators import with_error_handling
21
23
  from provide.foundation.logger import get_logger
22
24
 
23
25
  log = get_logger(__name__)
24
26
 
25
27
 
26
- def _get_context() -> Context | None:
28
+ def _get_context() -> CLIContext | None:
27
29
  """Get current context from Click or environment."""
28
30
  if not _HAS_CLICK:
29
31
  return None
30
32
  ctx = click.get_current_context(silent=True)
31
- if ctx and hasattr(ctx, "obj") and isinstance(ctx.obj, Context):
33
+ if ctx and hasattr(ctx, "obj") and isinstance(ctx.obj, CLIContext):
32
34
  return ctx.obj
33
35
  return None
34
36
 
35
37
 
36
- def _should_use_json(ctx: Context | None = None) -> bool:
38
+ def _should_use_json(ctx: CLIContext | None = None) -> bool:
37
39
  """Determine if JSON output should be used."""
38
40
  if ctx is None:
39
41
  ctx = _get_context()
40
42
  return ctx.json_output if ctx else False
41
43
 
42
44
 
43
- def _should_use_color(ctx: Context | None = None, stream=None) -> bool:
45
+ def _should_use_color(ctx: CLIContext | None = None, stream: Any = None) -> bool:
44
46
  """Determine if color output should be used."""
45
47
  if ctx is None:
46
48
  ctx = _get_context()
47
49
 
50
+ # Check FORCE_COLOR first (enables color even for non-TTY)
51
+ force_color = os.environ.get("FORCE_COLOR", "").lower()
52
+ if force_color in ("1", "true", "yes"):
53
+ return True
54
+
55
+ # Check NO_COLOR (disables color even for TTY)
56
+ if os.environ.get("NO_COLOR"):
57
+ return False
58
+
59
+ # Check context no_color setting
60
+ if ctx and ctx.no_color:
61
+ return False
62
+
48
63
  # Check if stream is a TTY
49
64
  if stream:
50
65
  return getattr(stream, "isatty", lambda: False)()
@@ -52,19 +67,21 @@ def _should_use_color(ctx: Context | None = None, stream=None) -> bool:
52
67
  return sys.stdout.isatty() or sys.stderr.isatty()
53
68
 
54
69
 
55
- def _output_json(data: Any, stream=sys.stdout) -> None:
70
+ @with_error_handling(fallback=None, suppress=(TypeError, ValueError, AttributeError))
71
+ def _output_json(data: Any, stream: Any = sys.stdout) -> None:
56
72
  """Output data as JSON."""
57
- try:
58
- json_str = json.dumps(data, indent=2, default=str)
73
+ json_str = json.dumps(data, indent=2, default=str)
74
+ if _HAS_CLICK:
59
75
  click.echo(json_str, file=stream)
60
- except (TypeError, ValueError) as e:
61
- # Fallback to string representation
62
- click.echo(
63
- json.dumps({"error": f"JSON encoding failed: {e}", "data": str(data)}),
64
- file=stream,
65
- )
76
+ else:
77
+ print(json_str, file=stream)
66
78
 
67
79
 
80
+ @with_error_handling(
81
+ fallback=None,
82
+ suppress=(OSError, IOError, UnicodeEncodeError),
83
+ context_provider=lambda: {"function": "pout"},
84
+ )
68
85
  def pout(message: Any, **kwargs: Any) -> None:
69
86
  """
70
87
  Output message to stdout.
@@ -122,6 +139,11 @@ def pout(message: Any, **kwargs: Any) -> None:
122
139
  print(output, file=sys.stdout, end="")
123
140
 
124
141
 
142
+ @with_error_handling(
143
+ fallback=None,
144
+ suppress=(OSError, IOError, UnicodeEncodeError),
145
+ context_provider=lambda: {"function": "perr"},
146
+ )
125
147
  def perr(message: Any, **kwargs: Any) -> None:
126
148
  """
127
149
  Output message to stderr.
@@ -1,12 +1,16 @@
1
1
  """
2
2
  Core context management for provide-foundation.
3
3
 
4
- Provides unified application context that bridges configuration, runtime state,
5
- and presentation concerns across the foundation library.
4
+ Provides CLI runtime context for managing command execution state,
5
+ output formatting, and CLI-specific settings.
6
6
  """
7
7
 
8
- from provide.foundation.context.core import Context
8
+ from provide.foundation.context.core import CLIContext
9
+
10
+ # Backward compatibility
11
+ Context = CLIContext
9
12
 
10
13
  __all__ = [
11
- "Context",
14
+ "CLIContext",
15
+ "Context", # Backward compatibility
12
16
  ]
@@ -2,15 +2,15 @@
2
2
 
3
3
  import copy
4
4
  import json
5
- import os
6
5
  from pathlib import Path
7
6
  from typing import Any
8
7
 
9
8
  from attrs import define, field, fields, validators
10
9
 
10
+ from provide.foundation.config.base import ConfigSource, field as config_field
11
+ from provide.foundation.config.converters import parse_bool_strict
12
+ from provide.foundation.config.env import RuntimeConfig
11
13
  from provide.foundation.logger import get_logger
12
- from provide.foundation.logger.config import TelemetryConfig
13
- from provide.foundation.utils.parsing import parse_bool
14
14
 
15
15
  try:
16
16
  import tomli as tomllib
@@ -29,36 +29,68 @@ except ImportError:
29
29
  VALID_LOG_LEVELS = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
30
30
 
31
31
 
32
- @define(slots=True, frozen=False)
33
- class Context:
32
+ @define(slots=True, repr=False)
33
+ class CLIContext(RuntimeConfig):
34
34
  """
35
- Unified context for configuration and CLI state.
35
+ Runtime context for CLI execution and state management.
36
36
 
37
- Combines configuration management with runtime state for CLI tools
38
- and services. Supports loading from files, environment variables,
39
- and programmatic updates.
37
+ Manages CLI-specific settings, output formatting, and runtime state
38
+ during command execution. Supports loading from files, environment variables,
39
+ and programmatic updates during CLI command execution.
40
40
  """
41
41
 
42
- log_level: str = field(
43
- default="INFO", validator=validators.in_(VALID_LOG_LEVELS), converter=str.upper
42
+ log_level: str = config_field(
43
+ default="INFO",
44
+ env_var="PROVIDE_LOG_LEVEL",
45
+ converter=str.upper,
46
+ validator=validators.in_(VALID_LOG_LEVELS),
47
+ description="Logging level for CLI output",
44
48
  )
45
- profile: str = field(default="default")
46
- debug: bool = field(default=False, converter=lambda x: parse_bool(x, strict=True))
47
- json_output: bool = field(
48
- default=False, converter=lambda x: parse_bool(x, strict=True)
49
+ profile: str = config_field(
50
+ default="default",
51
+ env_var="PROVIDE_PROFILE",
52
+ description="Configuration profile to use",
49
53
  )
50
- config_file: Path | None = field(
51
- default=None, converter=lambda x: Path(x) if x else None
54
+ debug: bool = config_field(
55
+ default=False,
56
+ env_var="PROVIDE_DEBUG",
57
+ converter=parse_bool_strict,
58
+ description="Enable debug mode",
52
59
  )
53
- log_file: Path | None = field(
54
- default=None, converter=lambda x: Path(x) if x else None
60
+ json_output: bool = config_field(
61
+ default=False,
62
+ env_var="PROVIDE_JSON_OUTPUT",
63
+ converter=parse_bool_strict,
64
+ description="Output in JSON format",
55
65
  )
56
- log_format: str = field(default="key_value")
57
- no_color: bool = field(
58
- default=False, converter=lambda x: parse_bool(x, strict=True)
66
+ config_file: Path | None = config_field(
67
+ default=None,
68
+ env_var="PROVIDE_CONFIG_FILE",
69
+ converter=lambda x: Path(x) if x else None,
70
+ description="Path to configuration file",
59
71
  )
60
- no_emoji: bool = field(
61
- default=False, converter=lambda x: parse_bool(x, strict=True)
72
+ log_file: Path | None = config_field(
73
+ default=None,
74
+ env_var="PROVIDE_LOG_FILE",
75
+ converter=lambda x: Path(x) if x else None,
76
+ description="Path to log file",
77
+ )
78
+ log_format: str = config_field(
79
+ default="key_value",
80
+ env_var="PROVIDE_LOG_FORMAT",
81
+ description="Log output format (key_value or json)",
82
+ )
83
+ no_color: bool = config_field(
84
+ default=False,
85
+ env_var="NO_COLOR",
86
+ converter=parse_bool_strict,
87
+ description="Disable colored output",
88
+ )
89
+ no_emoji: bool = config_field(
90
+ default=False,
91
+ env_var="PROVIDE_NO_EMOJI",
92
+ converter=parse_bool_strict,
93
+ description="Disable emoji in output",
62
94
  )
63
95
 
64
96
  # Private fields - using Factory for mutable defaults
@@ -69,62 +101,9 @@ class Context:
69
101
  """Post-initialization hook."""
70
102
  pass # Validation is handled by attrs validators
71
103
 
72
- def _validate(self) -> None:
73
- """Validate context values. For attrs compatibility."""
74
- # Validation is handled by attrs validators automatically
75
- pass
76
-
77
- @classmethod
78
- def from_env(cls, prefix: str = "PROVIDE") -> "Context":
79
- """
80
- Create context from environment variables using TelemetryConfig system.
81
-
82
- Args:
83
- prefix: Environment variable prefix (default: PROVIDE)
84
-
85
- Returns:
86
- New Context instance with values from environment
87
- """
88
- # Use the main TelemetryConfig system for parsing
89
- telemetry_config = TelemetryConfig.from_env(strict=False)
90
-
91
- kwargs = {}
92
-
93
- # Map telemetry config values to CLI context
94
- kwargs["log_level"] = telemetry_config.logging.default_level
95
- if telemetry_config.logging.console_formatter:
96
- kwargs["log_format"] = telemetry_config.logging.console_formatter
97
- if telemetry_config.logging.log_file:
98
- kwargs["log_file"] = telemetry_config.logging.log_file
99
-
100
- # CLI-specific environment variables that don't exist in TelemetryConfig
101
- if profile := os.environ.get(f"{prefix}_PROFILE"):
102
- kwargs["profile"] = profile
103
-
104
- if debug := os.environ.get(f"{prefix}_DEBUG"):
105
- kwargs["debug"] = debug.lower() in ("true", "1", "yes", "on")
106
-
107
- if json_output := os.environ.get(f"{prefix}_JSON_OUTPUT"):
108
- kwargs["json_output"] = json_output.lower() in ("true", "1", "yes", "on")
109
-
110
- if config_file := os.environ.get(f"{prefix}_CONFIG_FILE"):
111
- kwargs["config_file"] = Path(config_file)
112
-
113
- # Map emoji settings to no_emoji (inverted)
114
- kwargs["no_emoji"] = not (
115
- telemetry_config.logging.logger_name_emoji_prefix_enabled
116
- and telemetry_config.logging.das_emoji_prefix_enabled
117
- )
118
-
119
- # Check for explicit NO_COLOR override
120
- if no_color := os.environ.get(f"{prefix}_NO_COLOR"):
121
- kwargs["no_color"] = no_color.lower() in ("true", "1", "yes", "on")
122
-
123
- return cls(**kwargs)
124
-
125
104
  def update_from_env(self, prefix: str = "PROVIDE") -> None:
126
105
  """
127
- Update context from environment variables using TelemetryConfig system.
106
+ Update context from environment variables.
128
107
 
129
108
  Args:
130
109
  prefix: Environment variable prefix (default: PROVIDE)
@@ -132,28 +111,19 @@ class Context:
132
111
  if self._frozen:
133
112
  raise RuntimeError("Context is frozen and cannot be modified")
134
113
 
135
- env_ctx = self.from_env(prefix)
136
-
137
- # Update values from TelemetryConfig (these are always updated since they're the primary source)
138
- self.log_level = env_ctx.log_level
139
- self.log_format = env_ctx.log_format
140
- if env_ctx.log_file:
141
- self.log_file = env_ctx.log_file
142
- self.no_emoji = env_ctx.no_emoji
143
-
144
- # Update CLI-specific values only if explicitly set in environment
145
- if os.environ.get(f"{prefix}_PROFILE"):
146
- self.profile = env_ctx.profile
147
- if os.environ.get(f"{prefix}_DEBUG"):
148
- self.debug = env_ctx.debug
149
- if os.environ.get(f"{prefix}_JSON_OUTPUT"):
150
- self.json_output = env_ctx.json_output
151
- if os.environ.get(f"{prefix}_CONFIG_FILE"):
152
- self.config_file = env_ctx.config_file
153
- if os.environ.get(f"{prefix}_NO_COLOR"):
154
- self.no_color = env_ctx.no_color
114
+ # Create default instance and environment instance
115
+ default_ctx = self.__class__() # All defaults
116
+ env_ctx = self.from_env(prefix=prefix) # Environment + defaults
155
117
 
156
- self._validate()
118
+ # Only update fields where environment differs from default
119
+ for attr in fields(self.__class__):
120
+ if not attr.name.startswith("_"): # Skip private fields
121
+ default_value = getattr(default_ctx, attr.name)
122
+ env_value = getattr(env_ctx, attr.name)
123
+
124
+ # If environment value differs from default, it came from env
125
+ if env_value != default_value:
126
+ setattr(self, attr.name, env_value)
157
127
 
158
128
  def to_dict(self) -> dict[str, Any]:
159
129
  """Convert context to dictionary."""
@@ -170,15 +140,18 @@ class Context:
170
140
  }
171
141
 
172
142
  @classmethod
173
- def from_dict(cls, data: dict[str, Any]) -> "Context":
143
+ def from_dict(
144
+ cls, data: dict[str, Any], source: ConfigSource = ConfigSource.RUNTIME
145
+ ) -> "CLIContext":
174
146
  """
175
147
  Create context from dictionary.
176
148
 
177
149
  Args:
178
150
  data: Dictionary with context values
151
+ source: Source of the configuration data
179
152
 
180
153
  Returns:
181
- New Context instance
154
+ New CLIContext instance
182
155
  """
183
156
  kwargs = {}
184
157
 
@@ -212,9 +185,7 @@ class Context:
212
185
  Args:
213
186
  path: Path to configuration file
214
187
  """
215
- if self._frozen:
216
- raise RuntimeError("Context is frozen and cannot be modified")
217
-
188
+ # CLIContext is not frozen, so we can modify it
218
189
  path = Path(path)
219
190
  if not path.exists():
220
191
  raise FileNotFoundError(f"Config file not found: {path}")
@@ -294,16 +265,18 @@ class Context:
294
265
 
295
266
  path.write_text(content)
296
267
 
297
- def merge(self, other: "Context", override_defaults: bool = False) -> "Context":
268
+ def merge(
269
+ self, other: "CLIContext", override_defaults: bool = False
270
+ ) -> "CLIContext":
298
271
  """
299
272
  Merge with another context, with other taking precedence.
300
273
 
301
274
  Args:
302
- other: Context to merge with
275
+ other: CLIContext to merge with
303
276
  override_defaults: If False, only override if other's value differs from its class default
304
277
 
305
278
  Returns:
306
- New merged Context instance
279
+ New merged CLIContext instance
307
280
  """
308
281
  merged_data = self.to_dict()
309
282
  other_data = other.to_dict()
@@ -318,7 +291,7 @@ class Context:
318
291
  from attrs import Factory
319
292
 
320
293
  defaults = {}
321
- for f in fields(Context):
294
+ for f in fields(CLIContext):
322
295
  if not f.name.startswith("_"): # Skip private fields
323
296
  if isinstance(f.default, Factory):
324
297
  defaults[f.name] = f.default.factory()
@@ -333,7 +306,7 @@ class Context:
333
306
  continue
334
307
  merged_data[key] = value
335
308
 
336
- return Context.from_dict(merged_data)
309
+ return CLIContext.from_dict(merged_data)
337
310
 
338
311
  def freeze(self) -> None:
339
312
  """Freeze context to prevent further modifications."""
@@ -341,7 +314,7 @@ class Context:
341
314
  # This is kept for API compatibility but does nothing
342
315
  self._frozen = True
343
316
 
344
- def copy(self) -> "Context":
317
+ def copy(self) -> "CLIContext":
345
318
  """Create a deep copy of the context."""
346
319
  return copy.deepcopy(self)
347
320
 
@@ -354,3 +327,8 @@ class Context:
354
327
  profile=self.profile,
355
328
  )
356
329
  return self._logger
330
+
331
+ def _validate(self) -> None:
332
+ """Validate context values. For attrs compatibility."""
333
+ # Validation is handled by attrs validators automatically
334
+ pass
@@ -2,6 +2,7 @@
2
2
 
3
3
  # Import from submodules using absolute imports
4
4
  from provide.foundation.crypto.certificates.base import (
5
+ _HAS_CRYPTO,
5
6
  CertificateBase,
6
7
  CertificateConfig,
7
8
  CertificateError,
@@ -9,7 +10,6 @@ from provide.foundation.crypto.certificates.base import (
9
10
  KeyPair,
10
11
  KeyType,
11
12
  PublicKey,
12
- _HAS_CRYPTO,
13
13
  _require_crypto,
14
14
  )
15
15
  from provide.foundation.crypto.certificates.certificate import Certificate
@@ -21,14 +21,18 @@ from provide.foundation.crypto.certificates.operations import (
21
21
 
22
22
  # Re-export public types - maintaining exact same API
23
23
  __all__ = [
24
+ "_HAS_CRYPTO", # For testing
24
25
  "Certificate",
25
26
  "CertificateBase",
26
27
  "CertificateConfig",
27
28
  "CertificateError",
28
29
  "CurveType",
30
+ "KeyPair",
29
31
  "KeyType",
30
- "create_self_signed",
31
- "create_ca",
32
- "_HAS_CRYPTO", # For testing
32
+ "PublicKey",
33
33
  "_require_crypto", # For testing
34
- ]
34
+ "create_ca",
35
+ "create_self_signed",
36
+ "create_x509_certificate",
37
+ "validate_signature",
38
+ ]
@@ -5,7 +5,7 @@ from enum import StrEnum, auto
5
5
  import traceback
6
6
  from typing import NotRequired, Self, TypeAlias, TypedDict
7
7
 
8
- from attrs import define, field
8
+ from attrs import define
9
9
 
10
10
  try:
11
11
  from cryptography import x509
@@ -170,4 +170,4 @@ class CertificateBase:
170
170
  x509.NameAttribute(NameOID.COMMON_NAME, common_name),
171
171
  x509.NameAttribute(NameOID.ORGANIZATION_NAME, org),
172
172
  ]
173
- )
173
+ )