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,65 @@
|
|
1
|
+
"""
|
2
|
+
Main CLI entry point for Foundation.
|
3
|
+
"""
|
4
|
+
|
5
|
+
try:
|
6
|
+
import click
|
7
|
+
|
8
|
+
_HAS_CLICK = True
|
9
|
+
except ImportError:
|
10
|
+
click = None
|
11
|
+
_HAS_CLICK = False
|
12
|
+
|
13
|
+
|
14
|
+
def _require_click():
|
15
|
+
"""Ensure click is available for CLI."""
|
16
|
+
if not _HAS_CLICK:
|
17
|
+
raise ImportError(
|
18
|
+
"CLI requires optional dependencies. "
|
19
|
+
"Install with: pip install 'provide-foundation[cli]'"
|
20
|
+
)
|
21
|
+
|
22
|
+
|
23
|
+
if _HAS_CLICK:
|
24
|
+
|
25
|
+
@click.group()
|
26
|
+
@click.version_option()
|
27
|
+
def cli():
|
28
|
+
"""Foundation CLI - Telemetry and observability tools."""
|
29
|
+
pass
|
30
|
+
|
31
|
+
# Register commands from commands module
|
32
|
+
try:
|
33
|
+
from provide.foundation.cli.commands.deps import deps_command
|
34
|
+
|
35
|
+
cli.add_command(deps_command)
|
36
|
+
except ImportError:
|
37
|
+
pass
|
38
|
+
|
39
|
+
# Register logs commands
|
40
|
+
try:
|
41
|
+
from provide.foundation.cli.commands.logs import logs_group
|
42
|
+
|
43
|
+
cli.add_command(logs_group)
|
44
|
+
except ImportError:
|
45
|
+
pass
|
46
|
+
|
47
|
+
# Register OpenObserve commands if available
|
48
|
+
try:
|
49
|
+
from provide.foundation.observability.openobserve.commands import (
|
50
|
+
openobserve_group,
|
51
|
+
)
|
52
|
+
|
53
|
+
cli.add_command(openobserve_group)
|
54
|
+
except ImportError:
|
55
|
+
pass
|
56
|
+
|
57
|
+
else:
|
58
|
+
|
59
|
+
def cli():
|
60
|
+
"""CLI stub when click is not available."""
|
61
|
+
_require_click()
|
62
|
+
|
63
|
+
|
64
|
+
if __name__ == "__main__":
|
65
|
+
cli()
|
@@ -0,0 +1,220 @@
|
|
1
|
+
"""Testing utilities specifically for CLI applications."""
|
2
|
+
|
3
|
+
from contextlib import contextmanager
|
4
|
+
import json
|
5
|
+
import os
|
6
|
+
from pathlib import Path
|
7
|
+
import tempfile
|
8
|
+
from typing import Any
|
9
|
+
from unittest.mock import MagicMock
|
10
|
+
|
11
|
+
import click
|
12
|
+
from click.testing import CliRunner
|
13
|
+
|
14
|
+
from provide.foundation.context import Context
|
15
|
+
from provide.foundation.logger import get_logger
|
16
|
+
|
17
|
+
log = get_logger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
class MockContext(Context):
|
21
|
+
"""Mock context for testing that tracks method calls."""
|
22
|
+
|
23
|
+
def __init__(self, **kwargs) -> None:
|
24
|
+
"""Initialize mock context with tracking."""
|
25
|
+
super().__init__(**kwargs)
|
26
|
+
self.calls = []
|
27
|
+
self.saved_configs = []
|
28
|
+
self.loaded_configs = []
|
29
|
+
|
30
|
+
def save_config(self, path: str | Path) -> None:
|
31
|
+
"""Track save_config calls."""
|
32
|
+
self.saved_configs.append(path)
|
33
|
+
super().save_config(path)
|
34
|
+
|
35
|
+
def load_config(self, path: str | Path) -> None:
|
36
|
+
"""Track load_config calls."""
|
37
|
+
self.loaded_configs.append(path)
|
38
|
+
super().load_config(path)
|
39
|
+
|
40
|
+
|
41
|
+
@contextmanager
|
42
|
+
def isolated_cli_runner(
|
43
|
+
env: dict[str, str] | None = None,
|
44
|
+
):
|
45
|
+
"""
|
46
|
+
Create an isolated test environment for CLI testing.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
env: Environment variables to set
|
50
|
+
|
51
|
+
Yields:
|
52
|
+
CliRunner instance in isolated filesystem
|
53
|
+
"""
|
54
|
+
runner = CliRunner()
|
55
|
+
|
56
|
+
with runner.isolated_filesystem():
|
57
|
+
# Set up environment
|
58
|
+
old_env = {}
|
59
|
+
if env:
|
60
|
+
for key, value in env.items():
|
61
|
+
old_env[key] = os.environ.get(key)
|
62
|
+
os.environ[key] = value
|
63
|
+
|
64
|
+
try:
|
65
|
+
yield runner
|
66
|
+
finally:
|
67
|
+
# Restore environment
|
68
|
+
for key, old_value in old_env.items():
|
69
|
+
if old_value is None:
|
70
|
+
os.environ.pop(key, None)
|
71
|
+
else:
|
72
|
+
os.environ[key] = old_value
|
73
|
+
|
74
|
+
|
75
|
+
@contextmanager
|
76
|
+
def temp_config_file(
|
77
|
+
content: dict[str, Any] | str,
|
78
|
+
format: str = "json",
|
79
|
+
) -> Path:
|
80
|
+
"""
|
81
|
+
Create a temporary configuration file for testing.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
content: Configuration content (dict or string)
|
85
|
+
format: File format (json, toml, yaml)
|
86
|
+
|
87
|
+
Yields:
|
88
|
+
Path to temporary config file
|
89
|
+
"""
|
90
|
+
suffix = f".{format}"
|
91
|
+
|
92
|
+
with tempfile.NamedTemporaryFile(
|
93
|
+
mode="w",
|
94
|
+
suffix=suffix,
|
95
|
+
delete=False,
|
96
|
+
) as f:
|
97
|
+
if isinstance(content, dict):
|
98
|
+
if format == "json":
|
99
|
+
json.dump(content, f, indent=2)
|
100
|
+
elif format == "toml":
|
101
|
+
try:
|
102
|
+
import tomli_w
|
103
|
+
|
104
|
+
tomli_w.dump(content, f)
|
105
|
+
except ImportError:
|
106
|
+
# Fall back to manual formatting
|
107
|
+
for key, value in content.items():
|
108
|
+
if isinstance(value, str):
|
109
|
+
f.write(f'{key} = "{value}"\n')
|
110
|
+
else:
|
111
|
+
f.write(f"{key} = {value}\n")
|
112
|
+
elif format == "yaml":
|
113
|
+
try:
|
114
|
+
import yaml
|
115
|
+
|
116
|
+
yaml.safe_dump(content, f)
|
117
|
+
except ImportError:
|
118
|
+
raise ImportError("PyYAML required for YAML testing")
|
119
|
+
else:
|
120
|
+
f.write(content)
|
121
|
+
|
122
|
+
config_path = Path(f.name)
|
123
|
+
|
124
|
+
try:
|
125
|
+
yield config_path
|
126
|
+
finally:
|
127
|
+
config_path.unlink(missing_ok=True)
|
128
|
+
|
129
|
+
|
130
|
+
def create_test_cli(
|
131
|
+
name: str = "test-cli",
|
132
|
+
version: str = "1.0.0",
|
133
|
+
commands: list[click.Command] | None = None,
|
134
|
+
) -> click.Group:
|
135
|
+
"""
|
136
|
+
Create a test CLI group with standard options.
|
137
|
+
|
138
|
+
Args:
|
139
|
+
name: CLI name
|
140
|
+
version: CLI version
|
141
|
+
commands: Optional list of commands to add
|
142
|
+
|
143
|
+
Returns:
|
144
|
+
Click Group configured for testing
|
145
|
+
"""
|
146
|
+
from provide.foundation.cli.decorators import standard_options
|
147
|
+
|
148
|
+
@click.group(name=name)
|
149
|
+
@standard_options
|
150
|
+
@click.pass_context
|
151
|
+
def cli(ctx, **kwargs) -> None:
|
152
|
+
"""Test CLI for testing."""
|
153
|
+
ctx.obj = Context(**{k: v for k, v in kwargs.items() if v is not None})
|
154
|
+
|
155
|
+
if commands:
|
156
|
+
for cmd in commands:
|
157
|
+
cli.add_command(cmd)
|
158
|
+
|
159
|
+
return cli
|
160
|
+
|
161
|
+
|
162
|
+
def mock_logger():
|
163
|
+
"""
|
164
|
+
Create a mock logger for testing.
|
165
|
+
|
166
|
+
Returns:
|
167
|
+
MagicMock with common logger methods
|
168
|
+
"""
|
169
|
+
mock = MagicMock()
|
170
|
+
mock.debug = MagicMock()
|
171
|
+
mock.info = MagicMock()
|
172
|
+
mock.warning = MagicMock()
|
173
|
+
mock.error = MagicMock()
|
174
|
+
mock.critical = MagicMock()
|
175
|
+
return mock
|
176
|
+
|
177
|
+
|
178
|
+
class CliTestCase:
|
179
|
+
"""Base class for CLI test cases with common utilities."""
|
180
|
+
|
181
|
+
def setup_method(self) -> None:
|
182
|
+
"""Set up test case."""
|
183
|
+
self.runner = CliRunner()
|
184
|
+
self.temp_files = []
|
185
|
+
|
186
|
+
def teardown_method(self) -> None:
|
187
|
+
"""Clean up test case."""
|
188
|
+
for path in self.temp_files:
|
189
|
+
if path.exists():
|
190
|
+
path.unlink()
|
191
|
+
|
192
|
+
def invoke(self, *args, **kwargs):
|
193
|
+
"""Invoke CLI command."""
|
194
|
+
return self.runner.invoke(*args, **kwargs)
|
195
|
+
|
196
|
+
def create_temp_file(self, content: str = "", suffix: str = "") -> Path:
|
197
|
+
"""Create a temporary file that will be cleaned up."""
|
198
|
+
with tempfile.NamedTemporaryFile(
|
199
|
+
mode="w",
|
200
|
+
suffix=suffix,
|
201
|
+
delete=False,
|
202
|
+
) as f:
|
203
|
+
f.write(content)
|
204
|
+
path = Path(f.name)
|
205
|
+
|
206
|
+
self.temp_files.append(path)
|
207
|
+
return path
|
208
|
+
|
209
|
+
def assert_json_output(self, result, expected: dict[str, Any]) -> None:
|
210
|
+
"""Assert that output is valid JSON matching expected."""
|
211
|
+
try:
|
212
|
+
output = json.loads(result.output)
|
213
|
+
except json.JSONDecodeError as e:
|
214
|
+
raise AssertionError(f"Output is not valid JSON: {e}\n{result.output}")
|
215
|
+
|
216
|
+
for key, value in expected.items():
|
217
|
+
assert key in output, f"Key '{key}' not in output"
|
218
|
+
assert output[key] == value, (
|
219
|
+
f"Value mismatch for '{key}': {output[key]} != {value}"
|
220
|
+
)
|
@@ -0,0 +1,210 @@
|
|
1
|
+
"""Common CLI utilities for output, logging, and testing."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
import click
|
7
|
+
from click.testing import CliRunner, Result
|
8
|
+
|
9
|
+
from provide.foundation.context import Context
|
10
|
+
from provide.foundation.logger import (
|
11
|
+
LoggingConfig,
|
12
|
+
TelemetryConfig,
|
13
|
+
get_logger,
|
14
|
+
)
|
15
|
+
from provide.foundation.setup import setup_telemetry
|
16
|
+
|
17
|
+
log = get_logger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
def echo_json(data: Any, err: bool = False) -> None:
|
21
|
+
"""
|
22
|
+
Output data as JSON.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
data: Data to output as JSON
|
26
|
+
err: Whether to output to stderr
|
27
|
+
"""
|
28
|
+
click.echo(json.dumps(data, indent=2, default=str), err=err)
|
29
|
+
|
30
|
+
|
31
|
+
def echo_error(message: str, json_output: bool = False) -> None:
|
32
|
+
"""
|
33
|
+
Output an error message.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
message: Error message to output
|
37
|
+
json_output: Whether to output as JSON
|
38
|
+
"""
|
39
|
+
if json_output:
|
40
|
+
echo_json({"error": message}, err=True)
|
41
|
+
else:
|
42
|
+
click.secho(f"✗ {message}", fg="red", err=True)
|
43
|
+
|
44
|
+
|
45
|
+
def echo_success(message: str, json_output: bool = False) -> None:
|
46
|
+
"""
|
47
|
+
Output a success message.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
message: Success message to output
|
51
|
+
json_output: Whether to output as JSON
|
52
|
+
"""
|
53
|
+
if json_output:
|
54
|
+
echo_json({"success": message})
|
55
|
+
else:
|
56
|
+
click.secho(f"✓ {message}", fg="green")
|
57
|
+
|
58
|
+
|
59
|
+
def echo_warning(message: str, json_output: bool = False) -> None:
|
60
|
+
"""
|
61
|
+
Output a warning message.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
message: Warning message to output
|
65
|
+
json_output: Whether to output as JSON
|
66
|
+
"""
|
67
|
+
if json_output:
|
68
|
+
echo_json({"warning": message}, err=True)
|
69
|
+
else:
|
70
|
+
click.secho(f"⚠ {message}", fg="yellow", err=True)
|
71
|
+
|
72
|
+
|
73
|
+
def echo_info(message: str, json_output: bool = False) -> None:
|
74
|
+
"""
|
75
|
+
Output an informational message.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
message: Info message to output
|
79
|
+
json_output: Whether to output as JSON
|
80
|
+
"""
|
81
|
+
if json_output:
|
82
|
+
echo_json({"info": message})
|
83
|
+
else:
|
84
|
+
click.echo(f"ℹ {message}")
|
85
|
+
|
86
|
+
|
87
|
+
def setup_cli_logging(
|
88
|
+
ctx: Context,
|
89
|
+
) -> None:
|
90
|
+
"""
|
91
|
+
Setup logging for CLI applications using a Context object.
|
92
|
+
|
93
|
+
This function is the designated way to configure logging within a CLI
|
94
|
+
application built with foundation. It uses the provided context object
|
95
|
+
to construct a full TelemetryConfig and initializes the system.
|
96
|
+
|
97
|
+
Args:
|
98
|
+
ctx: The foundation Context, populated by CLI decorators.
|
99
|
+
"""
|
100
|
+
console_formatter = "json" if ctx.json_output else ctx.log_format
|
101
|
+
|
102
|
+
logging_config = LoggingConfig(
|
103
|
+
default_level=ctx.log_level,
|
104
|
+
console_formatter=console_formatter,
|
105
|
+
omit_timestamp=False,
|
106
|
+
logger_name_emoji_prefix_enabled=not ctx.no_emoji,
|
107
|
+
das_emoji_prefix_enabled=not ctx.no_emoji,
|
108
|
+
log_file=ctx.log_file,
|
109
|
+
)
|
110
|
+
|
111
|
+
telemetry_config = TelemetryConfig(
|
112
|
+
service_name=ctx.profile,
|
113
|
+
logging=logging_config,
|
114
|
+
)
|
115
|
+
|
116
|
+
setup_telemetry(config=telemetry_config)
|
117
|
+
|
118
|
+
|
119
|
+
def create_cli_context(**kwargs) -> Context:
|
120
|
+
"""
|
121
|
+
Create a Context for CLI usage.
|
122
|
+
|
123
|
+
Loads from environment, then overlays any provided kwargs.
|
124
|
+
|
125
|
+
Args:
|
126
|
+
**kwargs: Override values for the context
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
Configured Context instance
|
130
|
+
"""
|
131
|
+
ctx = Context.from_env()
|
132
|
+
for key, value in kwargs.items():
|
133
|
+
if value is not None and hasattr(ctx, key):
|
134
|
+
setattr(ctx, key, value)
|
135
|
+
return ctx
|
136
|
+
|
137
|
+
|
138
|
+
class CliTestRunner:
|
139
|
+
"""Test runner for CLI commands using Click's testing facilities."""
|
140
|
+
|
141
|
+
def __init__(self) -> None:
|
142
|
+
self.runner = CliRunner()
|
143
|
+
|
144
|
+
def invoke(
|
145
|
+
self,
|
146
|
+
cli: click.Command | click.Group,
|
147
|
+
args: list[str] | None = None,
|
148
|
+
input: str | None = None,
|
149
|
+
env: dict[str, str] | None = None,
|
150
|
+
catch_exceptions: bool = True,
|
151
|
+
**kwargs,
|
152
|
+
) -> Result:
|
153
|
+
"""Invoke a CLI command for testing."""
|
154
|
+
return self.runner.invoke(
|
155
|
+
cli,
|
156
|
+
args=args,
|
157
|
+
input=input,
|
158
|
+
env=env,
|
159
|
+
catch_exceptions=catch_exceptions,
|
160
|
+
**kwargs,
|
161
|
+
)
|
162
|
+
|
163
|
+
def isolated_filesystem(self):
|
164
|
+
"""
|
165
|
+
Context manager for isolated filesystem.
|
166
|
+
"""
|
167
|
+
return self.runner.isolated_filesystem()
|
168
|
+
|
169
|
+
|
170
|
+
def assert_cli_success(result: Result, expected_output: str | None = None) -> None:
|
171
|
+
"""
|
172
|
+
Assert that a CLI command succeeded.
|
173
|
+
"""
|
174
|
+
if result.exit_code != 0:
|
175
|
+
raise AssertionError(
|
176
|
+
f"Command failed with exit code {result.exit_code}\n"
|
177
|
+
f"Output: {result.output}\n"
|
178
|
+
f"Exception: {result.exception}"
|
179
|
+
)
|
180
|
+
|
181
|
+
if expected_output and expected_output not in result.output:
|
182
|
+
raise AssertionError(
|
183
|
+
f"Expected output not found.\n"
|
184
|
+
f"Expected: {expected_output}\n"
|
185
|
+
f"Actual: {result.output}"
|
186
|
+
)
|
187
|
+
|
188
|
+
|
189
|
+
def assert_cli_error(
|
190
|
+
result: Result,
|
191
|
+
expected_error: str | None = None,
|
192
|
+
exit_code: int | None = None,
|
193
|
+
) -> None:
|
194
|
+
"""
|
195
|
+
Assert that a CLI command failed.
|
196
|
+
"""
|
197
|
+
if result.exit_code == 0:
|
198
|
+
raise AssertionError(f"Command succeeded unexpectedly\nOutput: {result.output}")
|
199
|
+
|
200
|
+
if exit_code is not None and result.exit_code != exit_code:
|
201
|
+
raise AssertionError(
|
202
|
+
f"Wrong exit code.\nExpected: {exit_code}\nActual: {result.exit_code}"
|
203
|
+
)
|
204
|
+
|
205
|
+
if expected_error and expected_error not in result.output:
|
206
|
+
raise AssertionError(
|
207
|
+
f"Expected error not found.\n"
|
208
|
+
f"Expected: {expected_error}\n"
|
209
|
+
f"Actual: {result.output}"
|
210
|
+
)
|
@@ -0,0 +1,106 @@
|
|
1
|
+
"""
|
2
|
+
Foundation Configuration System.
|
3
|
+
|
4
|
+
A comprehensive, extensible configuration framework for the provide.io ecosystem.
|
5
|
+
Supports multiple configuration sources with precedence, validation, and type safety.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from provide.foundation.config.base import (
|
9
|
+
BaseConfig,
|
10
|
+
field,
|
11
|
+
)
|
12
|
+
from provide.foundation.config.env import (
|
13
|
+
RuntimeConfig,
|
14
|
+
env_field,
|
15
|
+
get_env,
|
16
|
+
get_env_async,
|
17
|
+
)
|
18
|
+
from provide.foundation.config.loader import (
|
19
|
+
ConfigLoader,
|
20
|
+
DictConfigLoader,
|
21
|
+
FileConfigLoader,
|
22
|
+
MultiSourceLoader,
|
23
|
+
)
|
24
|
+
from provide.foundation.config.manager import (
|
25
|
+
ConfigManager,
|
26
|
+
get_config,
|
27
|
+
set_config,
|
28
|
+
)
|
29
|
+
from provide.foundation.config.schema import (
|
30
|
+
ConfigSchema,
|
31
|
+
SchemaField,
|
32
|
+
validate_schema,
|
33
|
+
)
|
34
|
+
|
35
|
+
# Import sync wrappers for convenience
|
36
|
+
from provide.foundation.config.sync import (
|
37
|
+
SyncConfigManager,
|
38
|
+
load_config,
|
39
|
+
load_config_from_env,
|
40
|
+
load_config_from_file,
|
41
|
+
validate_config,
|
42
|
+
)
|
43
|
+
from provide.foundation.config.types import (
|
44
|
+
ConfigDict,
|
45
|
+
ConfigSource,
|
46
|
+
ConfigValue,
|
47
|
+
)
|
48
|
+
from provide.foundation.config.validators import (
|
49
|
+
validate_choice,
|
50
|
+
validate_non_negative,
|
51
|
+
validate_positive,
|
52
|
+
validate_range,
|
53
|
+
)
|
54
|
+
from provide.foundation.errors.config import (
|
55
|
+
ConfigurationError as ConfigError,
|
56
|
+
ValidationError as ConfigValidationError,
|
57
|
+
)
|
58
|
+
from provide.foundation.utils.parsing import (
|
59
|
+
parse_bool,
|
60
|
+
parse_dict,
|
61
|
+
parse_list,
|
62
|
+
)
|
63
|
+
|
64
|
+
__all__ = [
|
65
|
+
# Base
|
66
|
+
"BaseConfig",
|
67
|
+
# Types
|
68
|
+
"ConfigDict",
|
69
|
+
"ConfigError",
|
70
|
+
# Loader
|
71
|
+
"ConfigLoader",
|
72
|
+
# Manager
|
73
|
+
"ConfigManager",
|
74
|
+
# Schema
|
75
|
+
"ConfigSchema",
|
76
|
+
"ConfigSource",
|
77
|
+
"ConfigValidationError",
|
78
|
+
"ConfigValue",
|
79
|
+
"DictConfigLoader",
|
80
|
+
# Environment
|
81
|
+
"RuntimeConfig",
|
82
|
+
"FileConfigLoader",
|
83
|
+
"MultiSourceLoader",
|
84
|
+
"SchemaField",
|
85
|
+
"SyncConfigManager",
|
86
|
+
"env_field",
|
87
|
+
"field",
|
88
|
+
"get_config",
|
89
|
+
"get_env",
|
90
|
+
"get_env_async",
|
91
|
+
# Sync wrappers
|
92
|
+
"load_config",
|
93
|
+
"load_config_from_env",
|
94
|
+
"load_config_from_file",
|
95
|
+
"parse_bool",
|
96
|
+
"parse_dict",
|
97
|
+
"parse_list",
|
98
|
+
"set_config",
|
99
|
+
# Validators
|
100
|
+
"validate_choice",
|
101
|
+
"validate_config",
|
102
|
+
"validate_non_negative",
|
103
|
+
"validate_positive",
|
104
|
+
"validate_range",
|
105
|
+
"validate_schema",
|
106
|
+
]
|