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,104 @@
|
|
1
|
+
#
|
2
|
+
# file.py
|
3
|
+
#
|
4
|
+
"""
|
5
|
+
File stream management for Foundation.
|
6
|
+
Handles file-based logging streams and file operations.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import io
|
10
|
+
from pathlib import Path
|
11
|
+
import sys
|
12
|
+
|
13
|
+
from provide.foundation.streams.core import (
|
14
|
+
_LOG_FILE_HANDLE,
|
15
|
+
_PROVIDE_LOG_STREAM,
|
16
|
+
_STREAM_LOCK,
|
17
|
+
)
|
18
|
+
from provide.foundation.utils.streams import get_safe_stderr
|
19
|
+
|
20
|
+
|
21
|
+
def configure_file_logging(log_file_path: str | None) -> None:
|
22
|
+
"""
|
23
|
+
Configure file logging if a path is provided.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
log_file_path: Path to log file, or None to disable file logging
|
27
|
+
"""
|
28
|
+
global _PROVIDE_LOG_STREAM, _LOG_FILE_HANDLE
|
29
|
+
|
30
|
+
# Import here to avoid circular dependency
|
31
|
+
from provide.foundation.streams.core import _is_in_click_testing
|
32
|
+
|
33
|
+
with _STREAM_LOCK:
|
34
|
+
# Don't modify streams if we're in Click testing context
|
35
|
+
if _is_in_click_testing():
|
36
|
+
return
|
37
|
+
# Close existing file handle if it exists
|
38
|
+
if _LOG_FILE_HANDLE and _LOG_FILE_HANDLE is not _PROVIDE_LOG_STREAM:
|
39
|
+
try:
|
40
|
+
_LOG_FILE_HANDLE.close()
|
41
|
+
except Exception:
|
42
|
+
pass
|
43
|
+
_LOG_FILE_HANDLE = None
|
44
|
+
|
45
|
+
# Check if we're in testing mode
|
46
|
+
is_test_stream = _PROVIDE_LOG_STREAM is not sys.stderr and not isinstance(
|
47
|
+
_PROVIDE_LOG_STREAM, io.TextIOWrapper
|
48
|
+
)
|
49
|
+
|
50
|
+
if log_file_path:
|
51
|
+
try:
|
52
|
+
Path(log_file_path).parent.mkdir(parents=True, exist_ok=True)
|
53
|
+
_LOG_FILE_HANDLE = open(
|
54
|
+
log_file_path, "a", encoding="utf-8", buffering=1
|
55
|
+
)
|
56
|
+
_PROVIDE_LOG_STREAM = _LOG_FILE_HANDLE
|
57
|
+
except Exception as e:
|
58
|
+
# Log error to stderr and fall back
|
59
|
+
print(f"Failed to open log file {log_file_path}: {e}", file=sys.stderr)
|
60
|
+
_PROVIDE_LOG_STREAM = get_safe_stderr()
|
61
|
+
elif not is_test_stream:
|
62
|
+
_PROVIDE_LOG_STREAM = get_safe_stderr()
|
63
|
+
|
64
|
+
|
65
|
+
def flush_log_streams() -> None:
|
66
|
+
"""Flush all log streams."""
|
67
|
+
global _LOG_FILE_HANDLE
|
68
|
+
|
69
|
+
with _STREAM_LOCK:
|
70
|
+
if _LOG_FILE_HANDLE:
|
71
|
+
try:
|
72
|
+
_LOG_FILE_HANDLE.flush()
|
73
|
+
except Exception as e:
|
74
|
+
print(f"Failed to flush log file handle: {e}", file=sys.stderr)
|
75
|
+
|
76
|
+
|
77
|
+
def close_log_streams() -> None:
|
78
|
+
"""Close file log streams and reset to stderr."""
|
79
|
+
global _PROVIDE_LOG_STREAM, _LOG_FILE_HANDLE
|
80
|
+
|
81
|
+
# Import here to avoid circular dependency
|
82
|
+
from provide.foundation.streams.core import _is_in_click_testing
|
83
|
+
|
84
|
+
with _STREAM_LOCK:
|
85
|
+
if _LOG_FILE_HANDLE:
|
86
|
+
try:
|
87
|
+
_LOG_FILE_HANDLE.close()
|
88
|
+
except Exception:
|
89
|
+
pass
|
90
|
+
_LOG_FILE_HANDLE = None
|
91
|
+
|
92
|
+
# Don't reset stream to stderr if we're in Click testing context
|
93
|
+
if not _is_in_click_testing():
|
94
|
+
_PROVIDE_LOG_STREAM = sys.stderr
|
95
|
+
|
96
|
+
|
97
|
+
def reset_streams() -> None:
|
98
|
+
"""Reset all stream state (for testing)."""
|
99
|
+
# Import here to avoid circular dependency
|
100
|
+
from provide.foundation.streams.core import _is_in_click_testing
|
101
|
+
|
102
|
+
# Don't reset streams if we're in Click testing context
|
103
|
+
if not _is_in_click_testing():
|
104
|
+
close_log_streams()
|
@@ -0,0 +1,166 @@
|
|
1
|
+
#
|
2
|
+
# __init__.py
|
3
|
+
#
|
4
|
+
"""
|
5
|
+
Foundation Testing Module.
|
6
|
+
|
7
|
+
Unified testing utilities for Foundation with automatic context detection and warnings.
|
8
|
+
This module consolidates all testing helpers in one place with clear warnings when active.
|
9
|
+
"""
|
10
|
+
|
11
|
+
import os
|
12
|
+
import sys
|
13
|
+
from typing import Any
|
14
|
+
import warnings
|
15
|
+
|
16
|
+
|
17
|
+
# Context detection functions
|
18
|
+
def _is_testing_context() -> bool:
|
19
|
+
"""Detect if we're running in a testing context."""
|
20
|
+
return (
|
21
|
+
"pytest" in sys.modules
|
22
|
+
or os.getenv("PYTEST_CURRENT_TEST") is not None
|
23
|
+
or "unittest" in sys.modules
|
24
|
+
or os.getenv("TESTING") == "true"
|
25
|
+
or any(arg.endswith(("pytest", "py.test")) for arg in sys.argv)
|
26
|
+
)
|
27
|
+
|
28
|
+
|
29
|
+
def _warn_testing_active() -> None:
|
30
|
+
"""Issue warning that testing helpers are active."""
|
31
|
+
if not os.getenv("FOUNDATION_SUPPRESS_TESTING_WARNINGS"):
|
32
|
+
warnings.warn(
|
33
|
+
"🚨 Foundation testing helpers are active - production behavior may differ",
|
34
|
+
UserWarning,
|
35
|
+
stacklevel=3,
|
36
|
+
)
|
37
|
+
|
38
|
+
|
39
|
+
def _should_warn() -> bool:
|
40
|
+
"""Check if we should issue testing warnings."""
|
41
|
+
# Don't warn if explicitly suppressed
|
42
|
+
if os.getenv("FOUNDATION_SUPPRESS_TESTING_WARNINGS"):
|
43
|
+
return False
|
44
|
+
|
45
|
+
# Don't warn if we're not in a testing context
|
46
|
+
if not _is_testing_context():
|
47
|
+
return False
|
48
|
+
|
49
|
+
# Don't warn if already warned (module-level state)
|
50
|
+
return not getattr(_should_warn, "_warned", False)
|
51
|
+
|
52
|
+
|
53
|
+
# Issue warning on import if in testing context
|
54
|
+
if _should_warn():
|
55
|
+
_warn_testing_active()
|
56
|
+
_should_warn._warned = True
|
57
|
+
|
58
|
+
|
59
|
+
# Lazy imports to avoid importing testing utilities in production
|
60
|
+
def __getattr__(name: str) -> Any:
|
61
|
+
"""Lazy import testing utilities only when accessed."""
|
62
|
+
|
63
|
+
# CLI testing utilities
|
64
|
+
if name in [
|
65
|
+
"MockContext",
|
66
|
+
"isolated_cli_runner",
|
67
|
+
"temp_config_file",
|
68
|
+
"create_test_cli",
|
69
|
+
"mock_logger",
|
70
|
+
"CliTestCase",
|
71
|
+
]:
|
72
|
+
import provide.foundation.testing.cli as cli_module
|
73
|
+
|
74
|
+
return getattr(cli_module, name)
|
75
|
+
|
76
|
+
# Logger testing utilities
|
77
|
+
elif name in ["reset_foundation_setup_for_testing", "reset_foundation_state"]:
|
78
|
+
import provide.foundation.testing.logger as logger_module
|
79
|
+
|
80
|
+
return getattr(logger_module, name)
|
81
|
+
|
82
|
+
# Stream testing utilities
|
83
|
+
elif name in ["set_log_stream_for_testing"]:
|
84
|
+
import provide.foundation.testing.streams as streams_module
|
85
|
+
|
86
|
+
return getattr(streams_module, name)
|
87
|
+
|
88
|
+
# Fixture utilities
|
89
|
+
elif name in [
|
90
|
+
"captured_stderr_for_foundation",
|
91
|
+
"setup_foundation_telemetry_for_test",
|
92
|
+
]:
|
93
|
+
import provide.foundation.testing.fixtures as fixtures_module
|
94
|
+
|
95
|
+
return getattr(fixtures_module, name)
|
96
|
+
|
97
|
+
# Crypto fixtures (many fixtures)
|
98
|
+
elif name in [
|
99
|
+
"client_cert",
|
100
|
+
"server_cert",
|
101
|
+
"ca_cert",
|
102
|
+
"valid_cert_pem",
|
103
|
+
"valid_key_pem",
|
104
|
+
"invalid_cert_pem",
|
105
|
+
"invalid_key_pem",
|
106
|
+
"malformed_cert_pem",
|
107
|
+
"empty_cert",
|
108
|
+
"temporary_cert_file",
|
109
|
+
"temporary_key_file",
|
110
|
+
"cert_with_windows_line_endings",
|
111
|
+
"cert_with_utf8_bom",
|
112
|
+
"cert_with_extra_whitespace",
|
113
|
+
"external_ca_pem",
|
114
|
+
]:
|
115
|
+
import provide.foundation.testing.crypto as crypto_module
|
116
|
+
|
117
|
+
return getattr(crypto_module, name)
|
118
|
+
|
119
|
+
# Hub fixtures
|
120
|
+
elif name in ["default_container_directory"]:
|
121
|
+
import provide.foundation.testing.hub as hub_module
|
122
|
+
|
123
|
+
return getattr(hub_module, name)
|
124
|
+
|
125
|
+
else:
|
126
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
127
|
+
|
128
|
+
|
129
|
+
# Public API - these will be available for import but loaded lazily
|
130
|
+
__all__ = [
|
131
|
+
# Context detection
|
132
|
+
"_is_testing_context",
|
133
|
+
# CLI testing
|
134
|
+
"MockContext",
|
135
|
+
"isolated_cli_runner",
|
136
|
+
"temp_config_file",
|
137
|
+
"create_test_cli",
|
138
|
+
"mock_logger",
|
139
|
+
"CliTestCase",
|
140
|
+
# Logger testing
|
141
|
+
"reset_foundation_setup_for_testing",
|
142
|
+
"reset_foundation_state",
|
143
|
+
# Stream testing
|
144
|
+
"set_log_stream_for_testing",
|
145
|
+
# Common fixtures
|
146
|
+
"captured_stderr_for_foundation",
|
147
|
+
"setup_foundation_telemetry_for_test",
|
148
|
+
# Crypto fixtures
|
149
|
+
"client_cert",
|
150
|
+
"server_cert",
|
151
|
+
"ca_cert",
|
152
|
+
"valid_cert_pem",
|
153
|
+
"valid_key_pem",
|
154
|
+
"invalid_cert_pem",
|
155
|
+
"invalid_key_pem",
|
156
|
+
"malformed_cert_pem",
|
157
|
+
"empty_cert",
|
158
|
+
"temporary_cert_file",
|
159
|
+
"temporary_key_file",
|
160
|
+
"cert_with_windows_line_endings",
|
161
|
+
"cert_with_utf8_bom",
|
162
|
+
"cert_with_extra_whitespace",
|
163
|
+
"external_ca_pem",
|
164
|
+
# Hub fixtures
|
165
|
+
"default_container_directory",
|
166
|
+
]
|
@@ -0,0 +1,227 @@
|
|
1
|
+
"""
|
2
|
+
CLI Testing Utilities for Foundation.
|
3
|
+
|
4
|
+
Provides comprehensive testing support for CLI applications including
|
5
|
+
context mocking, isolated runners, and configuration helpers.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from contextlib import contextmanager
|
9
|
+
import json
|
10
|
+
import os
|
11
|
+
from pathlib import Path
|
12
|
+
import tempfile
|
13
|
+
from typing import Any
|
14
|
+
from unittest.mock import MagicMock
|
15
|
+
|
16
|
+
import click
|
17
|
+
from click.testing import CliRunner
|
18
|
+
|
19
|
+
from provide.foundation.context import Context
|
20
|
+
from provide.foundation.logger import get_logger
|
21
|
+
|
22
|
+
log = get_logger(__name__)
|
23
|
+
|
24
|
+
|
25
|
+
class MockContext(Context):
|
26
|
+
"""Mock context for testing that tracks method calls."""
|
27
|
+
|
28
|
+
def __init__(self, **kwargs) -> None:
|
29
|
+
"""Initialize mock context with tracking."""
|
30
|
+
super().__init__(**kwargs)
|
31
|
+
self.calls = []
|
32
|
+
self.saved_configs = []
|
33
|
+
self.loaded_configs = []
|
34
|
+
|
35
|
+
def save_config(self, path: str | Path) -> None:
|
36
|
+
"""Track save_config calls."""
|
37
|
+
self.saved_configs.append(path)
|
38
|
+
super().save_config(path)
|
39
|
+
|
40
|
+
def load_config(self, path: str | Path) -> None:
|
41
|
+
"""Track load_config calls."""
|
42
|
+
self.loaded_configs.append(path)
|
43
|
+
super().load_config(path)
|
44
|
+
|
45
|
+
|
46
|
+
@contextmanager
|
47
|
+
def isolated_cli_runner(
|
48
|
+
env: dict[str, str] | None = None,
|
49
|
+
):
|
50
|
+
"""
|
51
|
+
Create an isolated test environment for CLI testing.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
env: Environment variables to set
|
55
|
+
|
56
|
+
Yields:
|
57
|
+
CliRunner instance in isolated filesystem
|
58
|
+
"""
|
59
|
+
runner = CliRunner()
|
60
|
+
|
61
|
+
with runner.isolated_filesystem():
|
62
|
+
# Set up environment
|
63
|
+
old_env = {}
|
64
|
+
if env:
|
65
|
+
for key, value in env.items():
|
66
|
+
old_env[key] = os.environ.get(key)
|
67
|
+
os.environ[key] = value
|
68
|
+
|
69
|
+
try:
|
70
|
+
yield runner
|
71
|
+
finally:
|
72
|
+
# Restore environment
|
73
|
+
for key, old_value in old_env.items():
|
74
|
+
if old_value is None:
|
75
|
+
os.environ.pop(key, None)
|
76
|
+
else:
|
77
|
+
os.environ[key] = old_value
|
78
|
+
|
79
|
+
|
80
|
+
@contextmanager
|
81
|
+
def temp_config_file(
|
82
|
+
content: dict[str, Any] | str,
|
83
|
+
format: str = "json",
|
84
|
+
) -> Path:
|
85
|
+
"""
|
86
|
+
Create a temporary configuration file for testing.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
content: Configuration content (dict or string)
|
90
|
+
format: File format (json, toml, yaml)
|
91
|
+
|
92
|
+
Yields:
|
93
|
+
Path to temporary config file
|
94
|
+
"""
|
95
|
+
suffix = f".{format}"
|
96
|
+
|
97
|
+
with tempfile.NamedTemporaryFile(
|
98
|
+
mode="w",
|
99
|
+
suffix=suffix,
|
100
|
+
delete=False,
|
101
|
+
) as f:
|
102
|
+
if isinstance(content, dict):
|
103
|
+
if format == "json":
|
104
|
+
json.dump(content, f, indent=2)
|
105
|
+
elif format == "toml":
|
106
|
+
try:
|
107
|
+
import tomli_w
|
108
|
+
|
109
|
+
# tomli_w needs the content as a string, not written to file handle
|
110
|
+
toml_content = tomli_w.dumps(content)
|
111
|
+
f.write(toml_content)
|
112
|
+
except ImportError:
|
113
|
+
# Fall back to manual formatting
|
114
|
+
for key, value in content.items():
|
115
|
+
if isinstance(value, str):
|
116
|
+
f.write(f'{key} = "{value}"\n')
|
117
|
+
else:
|
118
|
+
f.write(f"{key} = {value}\n")
|
119
|
+
elif format == "yaml":
|
120
|
+
try:
|
121
|
+
import yaml
|
122
|
+
|
123
|
+
yaml.safe_dump(content, f)
|
124
|
+
except ImportError:
|
125
|
+
raise ImportError("PyYAML required for YAML testing")
|
126
|
+
else:
|
127
|
+
f.write(content)
|
128
|
+
|
129
|
+
config_path = Path(f.name)
|
130
|
+
|
131
|
+
try:
|
132
|
+
yield config_path
|
133
|
+
finally:
|
134
|
+
config_path.unlink(missing_ok=True)
|
135
|
+
|
136
|
+
|
137
|
+
def create_test_cli(
|
138
|
+
name: str = "test-cli",
|
139
|
+
version: str = "1.0.0",
|
140
|
+
commands: list[click.Command] | None = None,
|
141
|
+
) -> click.Group:
|
142
|
+
"""
|
143
|
+
Create a test CLI group with standard options.
|
144
|
+
|
145
|
+
Args:
|
146
|
+
name: CLI name
|
147
|
+
version: CLI version
|
148
|
+
commands: Optional list of commands to add
|
149
|
+
|
150
|
+
Returns:
|
151
|
+
Click Group configured for testing
|
152
|
+
"""
|
153
|
+
from provide.foundation.cli.decorators import standard_options
|
154
|
+
|
155
|
+
@click.group(name=name)
|
156
|
+
@standard_options
|
157
|
+
@click.pass_context
|
158
|
+
def cli(ctx, **kwargs) -> None:
|
159
|
+
"""Test CLI for testing."""
|
160
|
+
ctx.obj = Context(**{k: v for k, v in kwargs.items() if v is not None})
|
161
|
+
|
162
|
+
if commands:
|
163
|
+
for cmd in commands:
|
164
|
+
cli.add_command(cmd)
|
165
|
+
|
166
|
+
return cli
|
167
|
+
|
168
|
+
|
169
|
+
def mock_logger():
|
170
|
+
"""
|
171
|
+
Create a mock logger for testing.
|
172
|
+
|
173
|
+
Returns:
|
174
|
+
MagicMock with common logger methods
|
175
|
+
"""
|
176
|
+
mock = MagicMock()
|
177
|
+
mock.debug = MagicMock()
|
178
|
+
mock.info = MagicMock()
|
179
|
+
mock.warning = MagicMock()
|
180
|
+
mock.error = MagicMock()
|
181
|
+
mock.critical = MagicMock()
|
182
|
+
return mock
|
183
|
+
|
184
|
+
|
185
|
+
class CliTestCase:
|
186
|
+
"""Base class for CLI test cases with common utilities."""
|
187
|
+
|
188
|
+
def setup_method(self) -> None:
|
189
|
+
"""Set up test case."""
|
190
|
+
self.runner = CliRunner()
|
191
|
+
self.temp_files = []
|
192
|
+
|
193
|
+
def teardown_method(self) -> None:
|
194
|
+
"""Clean up test case."""
|
195
|
+
for path in self.temp_files:
|
196
|
+
if path.exists():
|
197
|
+
path.unlink()
|
198
|
+
|
199
|
+
def invoke(self, *args, **kwargs):
|
200
|
+
"""Invoke CLI command."""
|
201
|
+
return self.runner.invoke(*args, **kwargs)
|
202
|
+
|
203
|
+
def create_temp_file(self, content: str = "", suffix: str = "") -> Path:
|
204
|
+
"""Create a temporary file that will be cleaned up."""
|
205
|
+
with tempfile.NamedTemporaryFile(
|
206
|
+
mode="w",
|
207
|
+
suffix=suffix,
|
208
|
+
delete=False,
|
209
|
+
) as f:
|
210
|
+
f.write(content)
|
211
|
+
path = Path(f.name)
|
212
|
+
|
213
|
+
self.temp_files.append(path)
|
214
|
+
return path
|
215
|
+
|
216
|
+
def assert_json_output(self, result, expected: dict[str, Any]) -> None:
|
217
|
+
"""Assert that output is valid JSON matching expected."""
|
218
|
+
try:
|
219
|
+
output = json.loads(result.output)
|
220
|
+
except json.JSONDecodeError as e:
|
221
|
+
raise AssertionError(f"Output is not valid JSON: {e}\n{result.output}")
|
222
|
+
|
223
|
+
for key, value in expected.items():
|
224
|
+
assert key in output, f"Key '{key}' not in output"
|
225
|
+
assert output[key] == value, (
|
226
|
+
f"Value mismatch for '{key}': {output[key]} != {value}"
|
227
|
+
)
|
@@ -0,0 +1,163 @@
|
|
1
|
+
"""
|
2
|
+
Crypto Testing Fixtures for Foundation.
|
3
|
+
|
4
|
+
Provides comprehensive pytest fixtures for testing certificate functionality,
|
5
|
+
including valid/invalid certificates, keys, chains, and edge cases.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import pytest
|
9
|
+
|
10
|
+
from provide.foundation import logger
|
11
|
+
from provide.foundation.crypto import Certificate
|
12
|
+
|
13
|
+
|
14
|
+
@pytest.fixture(scope="module")
|
15
|
+
def client_cert():
|
16
|
+
"""Create a client certificate for testing."""
|
17
|
+
cert_pem = """-----BEGIN CERTIFICATE-----
|
18
|
+
MIIB+jCCAYGgAwIBAgIJAPsxOr78BIU0MAoGCCqGSM49BAMEMCgxEjAQBgNVBAoM
|
19
|
+
CUhhc2hpQ29ycDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDIwNTIzMTkzN1oX
|
20
|
+
DTI2MDIwNTIzMTkzN1owKDESMBAGA1UECgwJSGFzaGlDb3JwMRIwEAYDVQQDDAls
|
21
|
+
b2NhbGhvc3QwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARCi3SNYYDpSeScRM52tFYr
|
22
|
+
URzsPOE/ad8BzvpvL+mfy1c5oHQhh6KPnxpoo1WyDJGYplwPTGS68DvvWmolrPAt
|
23
|
+
C7I7r7spgyJS1358E5fA2NWk9/YPaiUzK2gsyrL9dKajdzB1MA8GA1UdEwEB/wQF
|
24
|
+
MAMBAf8wFAYDVR0RBA0wC4IJbG9jYWxob3N0MB0GA1UdJQQWMBQGCCsGAQUFBwMC
|
25
|
+
BggrBgEFBQcDATAOBgNVHQ8BAf8EBAMCA6gwHQYDVR0OBBYEFOwuttXPh5kTPSpX
|
26
|
+
a2ex0+VKjlpaMAoGCCqGSM49BAMEA2cAMGQCMGbN17Zt1GxZ41cXTaQOKuv/BIQd
|
27
|
+
nkaRz51XrITKaULNie4bgW6gT94cTUFQ9SNwEAIwOpmKeZqYG9WHcqol4QEUmMVM
|
28
|
+
MY3jxMiLpb9Mt/ysstXmsrQY7UoLu+c6zfKwyTEJ
|
29
|
+
-----END CERTIFICATE-----"""
|
30
|
+
|
31
|
+
key_pem = """-----BEGIN EC PRIVATE KEY-----
|
32
|
+
MIGkAgEBBDAkxo19KczdciRiJjOWEKGY5mH9s1D0aUS5XBdvktcaonIOdqNrkCt1
|
33
|
+
BC5YjEAVLNWgBwYFK4EEACKhZANiAARCi3SNYYDpSeScRM52tFYrURzsPOE/ad8B
|
34
|
+
zvpvL+mfy1c5oHQhh6KPnxpoo1WyDJGYplwPTGS68DvvWmolrPAtC7I7r7spgyJS
|
35
|
+
1358E5fA2NWk9/YPaiUzK2gsyrL9dKY=
|
36
|
+
-----END EC PRIVATE KEY-----"""
|
37
|
+
|
38
|
+
logger.debug(f"Created CLIENT_CERT fixture: {cert_pem[:30]}...")
|
39
|
+
return Certificate(cert_pem_or_uri=cert_pem, key_pem_or_uri=key_pem)
|
40
|
+
|
41
|
+
|
42
|
+
@pytest.fixture(scope="module")
|
43
|
+
def server_cert():
|
44
|
+
"""Create a server certificate for testing."""
|
45
|
+
cert_pem = """-----BEGIN CERTIFICATE-----
|
46
|
+
MIIB+jCCAYGgAwIBAgIJAKrIoEQw7N9LMAoGCCqGSM49BAMEMCgxEjAQBgNVBAoM
|
47
|
+
CUhhc2hpQ29ycDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDIwNTIzMTkzN1oX
|
48
|
+
DTI2MDIwNTIzMTkzN1owKDESMBAGA1UECgwJSGFzaGlDb3JwMRIwEAYDVQQDDAls
|
49
|
+
b2NhbGhvc3QwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARMxEVmGX3a4IWPOAJ2MX2s
|
50
|
+
2Wj3KZ0Io5EwUPMkxknGheO2e55qeHp/tkEFzYt9AH8du1xJLKKFbsGV5q9vipGN
|
51
|
+
x5XMbj2RMdH5VXHTAdc/bLFFy9kybQqo300Rv6ViW2KjdzB1MA8GA1UdEwEB/wQF
|
52
|
+
MAMBAf8wFAYDVR0RBA0wC4IJbG9jYWxob3N0MB0GA1UdJQQWMBQGCCsGAQUFBwMC
|
53
|
+
BggrBgEFBQcDATAOBgNVHQ8BAf8EBAMCA6gwHQYDVR0OBBYEFJy7Iz7whfiALYDB
|
54
|
+
TsM+IHXb1E8+MAoGCCqGSM49BAMEA2cAMGQCMFwxBS3lZSUprvrNGfJL83oGVY97
|
55
|
+
emQpHy/SEWpHBK8awn1XeTf+ZAwLaxc3K+AKqwIwPwIbIlmstd69zAYMFNHtzceN
|
56
|
+
XOzBx35sWRw92gr/hbE4hYeDBqEUwstSFNZ6MZu0
|
57
|
+
-----END CERTIFICATE-----"""
|
58
|
+
|
59
|
+
key_pem = """-----BEGIN EC PRIVATE KEY-----
|
60
|
+
MIGkAgEBBDDZ1MORWFVI0HtgKv+zZys/5e1HVmfcs4bwdp3VEsuwS6an3gTwGnSP
|
61
|
+
Ce+bI6f/TvGgBwYFK4EEACKhZANiAARMxEVmGX3a4IWPOAJ2MX2s2Wj3KZ0Io5Ew
|
62
|
+
UPMkxknGheO2e55qeHp/tkEFzYt9AH8du1xJLKKFbsGV5q9vipGNx5XMbj2RMdH5
|
63
|
+
VXHTAdc/bLFFy9kybQqo300Rv6ViW2I=
|
64
|
+
-----END EC PRIVATE KEY-----"""
|
65
|
+
|
66
|
+
logger.debug(f"Created SERVER_CERT fixture: {cert_pem[:30]}...")
|
67
|
+
return Certificate(cert_pem_or_uri=cert_pem, key_pem_or_uri=key_pem)
|
68
|
+
|
69
|
+
|
70
|
+
@pytest.fixture(scope="module")
|
71
|
+
def ca_cert():
|
72
|
+
"""Create a self-signed CA certificate for testing."""
|
73
|
+
return Certificate.create_ca(
|
74
|
+
common_name="Test CA", organization_name="Test Organization", validity_days=365
|
75
|
+
)
|
76
|
+
|
77
|
+
|
78
|
+
@pytest.fixture(scope="module")
|
79
|
+
def valid_key_pem(client_cert):
|
80
|
+
"""Get a valid key PEM from the client cert fixture."""
|
81
|
+
return client_cert.key
|
82
|
+
|
83
|
+
|
84
|
+
@pytest.fixture
|
85
|
+
def valid_cert_pem(client_cert):
|
86
|
+
"""Get a valid certificate PEM from the client cert fixture."""
|
87
|
+
return client_cert.cert
|
88
|
+
|
89
|
+
|
90
|
+
@pytest.fixture
|
91
|
+
def invalid_key_pem() -> str:
|
92
|
+
"""Returns an invalid PEM key."""
|
93
|
+
return "INVALID KEY DATA"
|
94
|
+
|
95
|
+
|
96
|
+
@pytest.fixture
|
97
|
+
def invalid_cert_pem() -> str:
|
98
|
+
"""Returns an invalid PEM certificate."""
|
99
|
+
return "INVALID CERTIFICATE DATA"
|
100
|
+
|
101
|
+
|
102
|
+
@pytest.fixture
|
103
|
+
def malformed_cert_pem() -> str:
|
104
|
+
"""Returns a PEM certificate with incorrect headers."""
|
105
|
+
return "-----BEGIN CERT-----\nMALFORMED DATA\n-----END CERT-----"
|
106
|
+
|
107
|
+
|
108
|
+
@pytest.fixture
|
109
|
+
def empty_cert() -> str:
|
110
|
+
"""Returns an empty certificate string."""
|
111
|
+
return ""
|
112
|
+
|
113
|
+
|
114
|
+
@pytest.fixture
|
115
|
+
def temporary_cert_file(tmp_path, client_cert) -> str:
|
116
|
+
"""Creates a temporary file containing the client certificate."""
|
117
|
+
cert_file = tmp_path / "client_cert.pem"
|
118
|
+
cert_file.write_text(client_cert.cert)
|
119
|
+
return f"file://{cert_file}"
|
120
|
+
|
121
|
+
|
122
|
+
@pytest.fixture
|
123
|
+
def temporary_key_file(tmp_path, client_cert) -> str:
|
124
|
+
"""Creates a temporary file containing the client private key."""
|
125
|
+
key_file = tmp_path / "client_key.pem"
|
126
|
+
key_file.write_text(client_cert.key)
|
127
|
+
return f"file://{key_file}"
|
128
|
+
|
129
|
+
|
130
|
+
@pytest.fixture
|
131
|
+
def cert_with_windows_line_endings(client_cert) -> str:
|
132
|
+
"""Returns a certificate PEM with Windows line endings."""
|
133
|
+
return client_cert.cert.replace("\n", "\r\n")
|
134
|
+
|
135
|
+
|
136
|
+
@pytest.fixture
|
137
|
+
def cert_with_utf8_bom(client_cert) -> str:
|
138
|
+
"""Returns a certificate PEM with UTF-8 BOM."""
|
139
|
+
return "\ufeff" + client_cert.cert
|
140
|
+
|
141
|
+
|
142
|
+
@pytest.fixture
|
143
|
+
def cert_with_extra_whitespace(client_cert) -> str:
|
144
|
+
"""Returns a certificate PEM with extra whitespace."""
|
145
|
+
return f" {client_cert.cert} \n\n "
|
146
|
+
|
147
|
+
|
148
|
+
@pytest.fixture(scope="module")
|
149
|
+
def external_ca_pem() -> str:
|
150
|
+
"""Provides an externally generated CA certificate PEM."""
|
151
|
+
return """-----BEGIN CERTIFICATE-----
|
152
|
+
MIIB4TCCAYegAwIBAgIJAPZ9vcVfR8AdMAoGCCqGSM49BAMCMFExCzAJBgNVBAYT
|
153
|
+
AlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FuIEZyYW5jaXNjbzEOMAwGA1UE
|
154
|
+
CgwFTXlPcmcxEzARBgNVBAMMCkV4dGVybmFsIENBMB4XDTI0MDgwMjEwNTgwMVoX
|
155
|
+
DTM0MDczMDEwNTgwMVowUTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMREwDwYD
|
156
|
+
VQQHDAhTYW5EaWVnbzEOMAwGA1UECgwFTXlPcmcxEzARBgNVBAMMCkV4dGVybmFs
|
157
|
+
IENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEgyF5Y8upm+M3ZzO8P4n7q2sS+L4c
|
158
|
+
mhl5XGg3vIOwFf7lG8XZCgJ6Xy4t1t8oD3zY0m9X8H8Z4YhY7K6b7c8Y7Xv6Y9fV
|
159
|
+
Q8M7Jg9nJ0x5c1N40zQwZzKjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E
|
160
|
+
BTADAQH/MB0GA1UdDgQWBBTGX00Gq7b09y/0C9eK0XgJp0mY7DAKBggqhkjOPQQD
|
161
|
+
AgNJADBGAiEAx1xH/b83/u5t7r29a/THZnFjQ7pvT2N0L4hG4BgGgXACIQD02W2+
|
162
|
+
MHB78ZWM+JOgikYj99qD6nLp0nkMyGmkSC7RYg==
|
163
|
+
-----END CERTIFICATE-----"""
|
@@ -0,0 +1,49 @@
|
|
1
|
+
"""
|
2
|
+
Common Test Fixtures for Foundation.
|
3
|
+
|
4
|
+
Provides pytest fixtures for capturing output, setting up telemetry,
|
5
|
+
and other common testing scenarios across the Foundation test suite.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from collections.abc import Callable, Generator
|
9
|
+
import io
|
10
|
+
from typing import TextIO
|
11
|
+
|
12
|
+
import pytest
|
13
|
+
|
14
|
+
from provide.foundation import TelemetryConfig, setup_telemetry
|
15
|
+
from provide.foundation.testing.streams import set_log_stream_for_testing
|
16
|
+
|
17
|
+
|
18
|
+
@pytest.fixture
|
19
|
+
def captured_stderr_for_foundation() -> Generator[TextIO]:
|
20
|
+
"""
|
21
|
+
Fixture to capture stderr output from Foundation's logging system.
|
22
|
+
|
23
|
+
It redirects Foundation's log stream to an `io.StringIO` buffer, yields the buffer
|
24
|
+
to the test, and then restores the original stream.
|
25
|
+
"""
|
26
|
+
current_test_stream = io.StringIO()
|
27
|
+
set_log_stream_for_testing(current_test_stream)
|
28
|
+
yield current_test_stream
|
29
|
+
set_log_stream_for_testing(None)
|
30
|
+
current_test_stream.close()
|
31
|
+
|
32
|
+
|
33
|
+
@pytest.fixture
|
34
|
+
def setup_foundation_telemetry_for_test(
|
35
|
+
captured_stderr_for_foundation: TextIO,
|
36
|
+
) -> Callable[[TelemetryConfig | None], None]:
|
37
|
+
"""
|
38
|
+
Fixture providing a function to set up Foundation Telemetry for tests.
|
39
|
+
|
40
|
+
This fixture captures stderr via `captured_stderr_for_foundation`
|
41
|
+
and provides a callable to configure telemetry with custom settings.
|
42
|
+
"""
|
43
|
+
|
44
|
+
def _setup(config: TelemetryConfig | None = None) -> None:
|
45
|
+
if config is None:
|
46
|
+
config = TelemetryConfig()
|
47
|
+
setup_telemetry(config)
|
48
|
+
|
49
|
+
return _setup
|