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.
- provide/foundation/__init__.py +36 -10
- provide/foundation/archive/__init__.py +1 -1
- provide/foundation/archive/base.py +15 -14
- provide/foundation/archive/bzip2.py +40 -40
- provide/foundation/archive/gzip.py +42 -42
- provide/foundation/archive/operations.py +93 -96
- provide/foundation/archive/tar.py +33 -31
- provide/foundation/archive/zip.py +52 -50
- provide/foundation/asynctools/__init__.py +20 -0
- provide/foundation/asynctools/core.py +126 -0
- provide/foundation/cli/__init__.py +2 -2
- provide/foundation/cli/commands/deps.py +15 -9
- provide/foundation/cli/commands/logs/__init__.py +3 -3
- provide/foundation/cli/commands/logs/generate.py +2 -2
- provide/foundation/cli/commands/logs/query.py +4 -4
- provide/foundation/cli/commands/logs/send.py +3 -3
- provide/foundation/cli/commands/logs/tail.py +3 -3
- provide/foundation/cli/decorators.py +11 -11
- provide/foundation/cli/main.py +1 -1
- provide/foundation/cli/testing.py +2 -40
- provide/foundation/cli/utils.py +21 -18
- provide/foundation/config/__init__.py +35 -2
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +477 -0
- provide/foundation/config/defaults.py +67 -0
- provide/foundation/config/env.py +6 -20
- provide/foundation/config/loader.py +10 -4
- provide/foundation/config/sync.py +8 -6
- provide/foundation/config/types.py +5 -5
- provide/foundation/config/validators.py +4 -4
- provide/foundation/console/input.py +5 -5
- provide/foundation/console/output.py +36 -14
- provide/foundation/context/__init__.py +8 -4
- provide/foundation/context/core.py +88 -110
- provide/foundation/crypto/certificates/__init__.py +9 -5
- provide/foundation/crypto/certificates/base.py +2 -2
- provide/foundation/crypto/certificates/certificate.py +48 -19
- provide/foundation/crypto/certificates/factory.py +26 -18
- provide/foundation/crypto/certificates/generator.py +24 -23
- provide/foundation/crypto/certificates/loader.py +24 -16
- provide/foundation/crypto/certificates/operations.py +17 -10
- provide/foundation/crypto/certificates/trust.py +21 -21
- provide/foundation/env/__init__.py +28 -0
- provide/foundation/env/core.py +218 -0
- provide/foundation/errors/__init__.py +3 -3
- provide/foundation/errors/decorators.py +0 -234
- provide/foundation/errors/types.py +0 -98
- provide/foundation/eventsets/display.py +13 -14
- provide/foundation/eventsets/registry.py +61 -31
- provide/foundation/eventsets/resolver.py +50 -46
- provide/foundation/eventsets/sets/das.py +8 -8
- provide/foundation/eventsets/sets/database.py +14 -14
- provide/foundation/eventsets/sets/http.py +21 -21
- provide/foundation/eventsets/sets/llm.py +16 -16
- provide/foundation/eventsets/sets/task_queue.py +13 -13
- provide/foundation/eventsets/types.py +7 -7
- provide/foundation/file/directory.py +14 -23
- provide/foundation/file/lock.py +4 -3
- provide/foundation/hub/components.py +75 -389
- provide/foundation/hub/config.py +157 -0
- provide/foundation/hub/discovery.py +63 -0
- provide/foundation/hub/handlers.py +89 -0
- provide/foundation/hub/lifecycle.py +195 -0
- provide/foundation/hub/manager.py +7 -4
- provide/foundation/hub/processors.py +49 -0
- provide/foundation/integrations/__init__.py +11 -0
- provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
- provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/client.py +14 -14
- provide/foundation/{observability → integrations}/openobserve/commands.py +12 -12
- provide/foundation/integrations/openobserve/config.py +37 -0
- provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/otlp.py +2 -2
- provide/foundation/{observability → integrations}/openobserve/search.py +2 -3
- provide/foundation/{observability → integrations}/openobserve/streaming.py +5 -5
- provide/foundation/logger/__init__.py +0 -1
- provide/foundation/logger/config/base.py +1 -1
- provide/foundation/logger/config/logging.py +69 -299
- provide/foundation/logger/config/telemetry.py +39 -121
- provide/foundation/logger/factories.py +2 -2
- provide/foundation/logger/processors/main.py +12 -10
- provide/foundation/logger/ratelimit/limiters.py +4 -4
- provide/foundation/logger/ratelimit/processor.py +1 -1
- provide/foundation/logger/setup/coordinator.py +39 -25
- provide/foundation/logger/setup/processors.py +3 -3
- provide/foundation/logger/setup/testing.py +14 -0
- provide/foundation/logger/trace.py +5 -5
- provide/foundation/metrics/__init__.py +1 -1
- provide/foundation/metrics/otel.py +3 -1
- provide/foundation/observability/__init__.py +3 -3
- provide/foundation/process/__init__.py +9 -0
- provide/foundation/process/exit.py +48 -0
- provide/foundation/process/lifecycle.py +69 -46
- provide/foundation/resilience/__init__.py +36 -0
- provide/foundation/resilience/circuit.py +166 -0
- provide/foundation/resilience/decorators.py +236 -0
- provide/foundation/resilience/fallback.py +208 -0
- provide/foundation/resilience/retry.py +327 -0
- provide/foundation/serialization/__init__.py +16 -0
- provide/foundation/serialization/core.py +70 -0
- provide/foundation/streams/config.py +78 -0
- provide/foundation/streams/console.py +4 -5
- provide/foundation/streams/core.py +5 -2
- provide/foundation/streams/file.py +12 -2
- provide/foundation/testing/__init__.py +29 -9
- provide/foundation/testing/archive/__init__.py +7 -7
- provide/foundation/testing/archive/fixtures.py +58 -54
- provide/foundation/testing/cli.py +30 -20
- provide/foundation/testing/common/__init__.py +13 -15
- provide/foundation/testing/common/fixtures.py +27 -57
- provide/foundation/testing/file/__init__.py +15 -15
- provide/foundation/testing/file/content_fixtures.py +289 -0
- provide/foundation/testing/file/directory_fixtures.py +107 -0
- provide/foundation/testing/file/fixtures.py +42 -516
- provide/foundation/testing/file/special_fixtures.py +145 -0
- provide/foundation/testing/logger.py +89 -8
- provide/foundation/testing/mocking/__init__.py +21 -21
- provide/foundation/testing/mocking/fixtures.py +80 -67
- provide/foundation/testing/process/__init__.py +23 -23
- provide/foundation/testing/process/async_fixtures.py +414 -0
- provide/foundation/testing/process/fixtures.py +48 -571
- provide/foundation/testing/process/subprocess_fixtures.py +210 -0
- provide/foundation/testing/threading/__init__.py +17 -17
- provide/foundation/testing/threading/basic_fixtures.py +105 -0
- provide/foundation/testing/threading/data_fixtures.py +101 -0
- provide/foundation/testing/threading/execution_fixtures.py +278 -0
- provide/foundation/testing/threading/fixtures.py +32 -502
- provide/foundation/testing/threading/sync_fixtures.py +100 -0
- provide/foundation/testing/time/__init__.py +11 -11
- provide/foundation/testing/time/fixtures.py +95 -83
- provide/foundation/testing/transport/__init__.py +9 -9
- provide/foundation/testing/transport/fixtures.py +54 -54
- provide/foundation/time/__init__.py +18 -0
- provide/foundation/time/core.py +63 -0
- provide/foundation/tools/__init__.py +2 -2
- provide/foundation/tools/base.py +68 -67
- provide/foundation/tools/cache.py +69 -74
- provide/foundation/tools/downloader.py +68 -62
- provide/foundation/tools/installer.py +51 -57
- provide/foundation/tools/registry.py +38 -45
- provide/foundation/tools/resolver.py +70 -68
- provide/foundation/tools/verifier.py +39 -50
- provide/foundation/tracer/spans.py +2 -14
- provide/foundation/transport/__init__.py +26 -33
- provide/foundation/transport/base.py +32 -30
- provide/foundation/transport/client.py +44 -49
- provide/foundation/transport/config.py +36 -107
- provide/foundation/transport/errors.py +13 -27
- provide/foundation/transport/http.py +69 -55
- provide/foundation/transport/middleware.py +113 -114
- provide/foundation/transport/registry.py +29 -27
- provide/foundation/transport/types.py +6 -6
- provide/foundation/utils/deps.py +17 -14
- provide/foundation/utils/parsing.py +49 -4
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
- provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
- provide_foundation-0.0.0.dev1.dist-info/RECORD +0 -200
- /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
- /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/top_level.txt +0 -0
@@ -9,6 +9,7 @@ Handles console-specific stream operations and formatting.
|
|
9
9
|
import sys
|
10
10
|
from typing import TextIO
|
11
11
|
|
12
|
+
from provide.foundation.streams.config import get_stream_config
|
12
13
|
from provide.foundation.streams.core import get_log_stream
|
13
14
|
|
14
15
|
|
@@ -25,14 +26,12 @@ def is_tty() -> bool:
|
|
25
26
|
|
26
27
|
def supports_color() -> bool:
|
27
28
|
"""Check if the current stream supports color output."""
|
28
|
-
|
29
|
+
config = get_stream_config()
|
29
30
|
|
30
|
-
|
31
|
-
if os.getenv("NO_COLOR"):
|
31
|
+
if config.no_color:
|
32
32
|
return False
|
33
33
|
|
34
|
-
|
35
|
-
if os.getenv("FORCE_COLOR"):
|
34
|
+
if config.force_color:
|
36
35
|
return True
|
37
36
|
|
38
37
|
# Check if we're in a TTY
|
@@ -10,6 +10,8 @@ import sys
|
|
10
10
|
import threading
|
11
11
|
from typing import TextIO
|
12
12
|
|
13
|
+
from provide.foundation.streams.config import get_stream_config
|
14
|
+
|
13
15
|
_PROVIDE_LOG_STREAM: TextIO = sys.stderr
|
14
16
|
_LOG_FILE_HANDLE: TextIO | None = None
|
15
17
|
_STREAM_LOCK = threading.Lock()
|
@@ -18,10 +20,11 @@ _STREAM_LOCK = threading.Lock()
|
|
18
20
|
def _is_in_click_testing() -> bool:
|
19
21
|
"""Check if we're running inside Click's testing framework."""
|
20
22
|
import inspect
|
21
|
-
|
23
|
+
|
24
|
+
config = get_stream_config()
|
22
25
|
|
23
26
|
# Check environment variables for Click testing
|
24
|
-
if
|
27
|
+
if config.click_testing:
|
25
28
|
return True
|
26
29
|
|
27
30
|
# Check the call stack for Click's testing module or CLI integration tests
|
@@ -18,6 +18,16 @@ from provide.foundation.streams.core import (
|
|
18
18
|
from provide.foundation.utils.streams import get_safe_stderr
|
19
19
|
|
20
20
|
|
21
|
+
def _safe_error_output(message: str) -> None:
|
22
|
+
"""
|
23
|
+
Output error message to stderr using basic print to avoid circular dependencies.
|
24
|
+
|
25
|
+
This function intentionally uses print() instead of Foundation's perr() to prevent
|
26
|
+
circular import issues during stream initialization and teardown phases.
|
27
|
+
"""
|
28
|
+
print(message, file=sys.stderr)
|
29
|
+
|
30
|
+
|
21
31
|
def configure_file_logging(log_file_path: str | None) -> None:
|
22
32
|
"""
|
23
33
|
Configure file logging if a path is provided.
|
@@ -56,7 +66,7 @@ def configure_file_logging(log_file_path: str | None) -> None:
|
|
56
66
|
_PROVIDE_LOG_STREAM = _LOG_FILE_HANDLE
|
57
67
|
except Exception as e:
|
58
68
|
# Log error to stderr and fall back
|
59
|
-
|
69
|
+
_safe_error_output(f"Failed to open log file {log_file_path}: {e}")
|
60
70
|
_PROVIDE_LOG_STREAM = get_safe_stderr()
|
61
71
|
elif not is_test_stream:
|
62
72
|
_PROVIDE_LOG_STREAM = get_safe_stderr()
|
@@ -71,7 +81,7 @@ def flush_log_streams() -> None:
|
|
71
81
|
try:
|
72
82
|
_LOG_FILE_HANDLE.flush()
|
73
83
|
except Exception as e:
|
74
|
-
|
84
|
+
_safe_error_output(f"Failed to flush log file handle: {e}")
|
75
85
|
|
76
86
|
|
77
87
|
def close_log_streams() -> None:
|
@@ -66,15 +66,20 @@ def __getattr__(name: str) -> Any:
|
|
66
66
|
"isolated_cli_runner",
|
67
67
|
"temp_config_file",
|
68
68
|
"create_test_cli",
|
69
|
-
"mock_logger",
|
70
69
|
"CliTestCase",
|
70
|
+
"click_testing_mode",
|
71
71
|
]:
|
72
72
|
import provide.foundation.testing.cli as cli_module
|
73
73
|
|
74
74
|
return getattr(cli_module, name)
|
75
75
|
|
76
76
|
# Logger testing utilities
|
77
|
-
elif name in [
|
77
|
+
elif name in [
|
78
|
+
"reset_foundation_setup_for_testing",
|
79
|
+
"reset_foundation_state",
|
80
|
+
"mock_logger",
|
81
|
+
"mock_logger_factory",
|
82
|
+
]:
|
78
83
|
import provide.foundation.testing.logger as logger_module
|
79
84
|
|
80
85
|
return getattr(logger_module, name)
|
@@ -93,12 +98,22 @@ def __getattr__(name: str) -> Any:
|
|
93
98
|
import provide.foundation.testing.fixtures as fixtures_module
|
94
99
|
|
95
100
|
return getattr(fixtures_module, name)
|
96
|
-
|
101
|
+
|
97
102
|
# Import submodules directly
|
98
|
-
elif name in [
|
103
|
+
elif name in [
|
104
|
+
"archive",
|
105
|
+
"common",
|
106
|
+
"file",
|
107
|
+
"process",
|
108
|
+
"transport",
|
109
|
+
"mocking",
|
110
|
+
"time",
|
111
|
+
"threading",
|
112
|
+
]:
|
99
113
|
import importlib
|
114
|
+
|
100
115
|
return importlib.import_module(f"provide.foundation.testing.{name}")
|
101
|
-
|
116
|
+
|
102
117
|
# File testing utilities (backward compatibility)
|
103
118
|
elif name in [
|
104
119
|
"temp_directory",
|
@@ -110,8 +125,9 @@ def __getattr__(name: str) -> Any:
|
|
110
125
|
"readonly_file",
|
111
126
|
]:
|
112
127
|
import provide.foundation.testing.file.fixtures as file_module
|
128
|
+
|
113
129
|
return getattr(file_module, name)
|
114
|
-
|
130
|
+
|
115
131
|
# Process/async testing utilities (backward compatibility)
|
116
132
|
elif name in [
|
117
133
|
"clean_event_loop",
|
@@ -126,8 +142,9 @@ def __getattr__(name: str) -> Any:
|
|
126
142
|
"mock_async_sleep",
|
127
143
|
]:
|
128
144
|
import provide.foundation.testing.process.fixtures as process_module
|
145
|
+
|
129
146
|
return getattr(process_module, name)
|
130
|
-
|
147
|
+
|
131
148
|
# Common mock utilities (backward compatibility)
|
132
149
|
elif name in [
|
133
150
|
"mock_http_config",
|
@@ -142,8 +159,9 @@ def __getattr__(name: str) -> Any:
|
|
142
159
|
"mock_subprocess",
|
143
160
|
]:
|
144
161
|
import provide.foundation.testing.common.fixtures as common_module
|
162
|
+
|
145
163
|
return getattr(common_module, name)
|
146
|
-
|
164
|
+
|
147
165
|
# Transport/network testing utilities (backward compatibility)
|
148
166
|
elif name in [
|
149
167
|
"free_port",
|
@@ -157,8 +175,9 @@ def __getattr__(name: str) -> Any:
|
|
157
175
|
"mock_http_headers",
|
158
176
|
]:
|
159
177
|
import provide.foundation.testing.transport.fixtures as transport_module
|
178
|
+
|
160
179
|
return getattr(transport_module, name)
|
161
|
-
|
180
|
+
|
162
181
|
# Archive testing utilities
|
163
182
|
elif name in [
|
164
183
|
"archive_test_content",
|
@@ -169,6 +188,7 @@ def __getattr__(name: str) -> Any:
|
|
169
188
|
"archive_stress_test_files",
|
170
189
|
]:
|
171
190
|
import provide.foundation.testing.archive.fixtures as archive_module
|
191
|
+
|
172
192
|
return getattr(archive_module, name)
|
173
193
|
|
174
194
|
# Crypto fixtures (many fixtures)
|
@@ -6,19 +6,19 @@ across any project that depends on provide.foundation.
|
|
6
6
|
"""
|
7
7
|
|
8
8
|
from provide.foundation.testing.archive.fixtures import (
|
9
|
+
archive_stress_test_files,
|
9
10
|
archive_test_content,
|
10
|
-
large_file_for_compression,
|
11
|
-
multi_format_archives,
|
12
11
|
archive_with_permissions,
|
13
12
|
corrupted_archives,
|
14
|
-
|
13
|
+
large_file_for_compression,
|
14
|
+
multi_format_archives,
|
15
15
|
)
|
16
16
|
|
17
17
|
__all__ = [
|
18
|
+
"archive_stress_test_files",
|
18
19
|
"archive_test_content",
|
19
|
-
"large_file_for_compression",
|
20
|
-
"multi_format_archives",
|
21
20
|
"archive_with_permissions",
|
22
21
|
"corrupted_archives",
|
23
|
-
"
|
24
|
-
|
22
|
+
"large_file_for_compression",
|
23
|
+
"multi_format_archives",
|
24
|
+
]
|
@@ -5,8 +5,8 @@ Fixtures specific to testing archive operations like tar, zip, gzip, bzip2.
|
|
5
5
|
Builds on top of file fixtures for archive-specific test scenarios.
|
6
6
|
"""
|
7
7
|
|
8
|
-
from pathlib import Path
|
9
8
|
from collections.abc import Generator
|
9
|
+
from pathlib import Path
|
10
10
|
|
11
11
|
import pytest
|
12
12
|
|
@@ -17,10 +17,10 @@ from provide.foundation.testing.file.fixtures import temp_directory
|
|
17
17
|
def archive_test_content() -> Generator[tuple[Path, dict[str, str]], None, None]:
|
18
18
|
"""
|
19
19
|
Create a standard set of files for archive testing.
|
20
|
-
|
20
|
+
|
21
21
|
Creates multiple files with different types of content to ensure
|
22
22
|
proper compression and extraction testing.
|
23
|
-
|
23
|
+
|
24
24
|
Yields:
|
25
25
|
Tuple of (source_dir, content_map) where content_map maps
|
26
26
|
relative paths to their expected content.
|
@@ -28,26 +28,28 @@ def archive_test_content() -> Generator[tuple[Path, dict[str, str]], None, None]
|
|
28
28
|
with temp_directory() as temp_dir:
|
29
29
|
source = temp_dir / "archive_source"
|
30
30
|
source.mkdir()
|
31
|
-
|
31
|
+
|
32
32
|
content_map = {
|
33
33
|
"text_file.txt": "This is a text file for archive testing.\n" * 10,
|
34
34
|
"data.json": '{"test": "data", "array": [1, 2, 3]}',
|
35
35
|
"script.py": "#!/usr/bin/env python\nprint('Hello from archive')\n",
|
36
36
|
"nested/dir/file.md": "# Nested File\nContent in nested directory",
|
37
|
-
"binary.dat": "Binary\x00\x01\x02\x03\
|
37
|
+
"binary.dat": "Binary\x00\x01\x02\x03\xff\xfe data",
|
38
38
|
"empty.txt": "",
|
39
39
|
}
|
40
|
-
|
40
|
+
|
41
41
|
# Create all files
|
42
42
|
for rel_path, content in content_map.items():
|
43
43
|
file_path = source / rel_path
|
44
44
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
45
|
-
|
45
|
+
|
46
46
|
if isinstance(content, str):
|
47
47
|
file_path.write_text(content)
|
48
48
|
else:
|
49
|
-
file_path.write_bytes(
|
50
|
-
|
49
|
+
file_path.write_bytes(
|
50
|
+
content.encode() if isinstance(content, str) else content
|
51
|
+
)
|
52
|
+
|
51
53
|
yield source, content_map
|
52
54
|
|
53
55
|
|
@@ -55,19 +57,19 @@ def archive_test_content() -> Generator[tuple[Path, dict[str, str]], None, None]
|
|
55
57
|
def large_file_for_compression() -> Generator[Path, None, None]:
|
56
58
|
"""
|
57
59
|
Create a large file suitable for compression testing.
|
58
|
-
|
60
|
+
|
59
61
|
The file contains repetitive content that compresses well.
|
60
|
-
|
62
|
+
|
61
63
|
Yields:
|
62
64
|
Path to a large file with compressible content.
|
63
65
|
"""
|
64
66
|
with temp_directory() as temp_dir:
|
65
67
|
large_file = temp_dir / "large_compressible.txt"
|
66
|
-
|
68
|
+
|
67
69
|
# Create 10MB of highly compressible content
|
68
70
|
content = "This is a line of text that will be repeated many times.\n" * 100
|
69
71
|
large_content = content * 1000 # ~6MB of repetitive text
|
70
|
-
|
72
|
+
|
71
73
|
large_file.write_text(large_content)
|
72
74
|
yield large_file
|
73
75
|
|
@@ -76,46 +78,48 @@ def large_file_for_compression() -> Generator[Path, None, None]:
|
|
76
78
|
def multi_format_archives() -> Generator[dict[str, Path], None, None]:
|
77
79
|
"""
|
78
80
|
Create sample archives in different formats for format detection testing.
|
79
|
-
|
81
|
+
|
80
82
|
Yields:
|
81
83
|
Dict mapping format names to paths of sample archives.
|
82
84
|
"""
|
83
85
|
with temp_directory() as temp_dir:
|
84
86
|
archives = {}
|
85
|
-
|
87
|
+
|
86
88
|
# Create minimal valid archives in different formats
|
87
89
|
# Note: These are minimal headers, not full valid archives
|
88
|
-
|
90
|
+
|
89
91
|
# GZIP file (magic: 1f 8b)
|
90
92
|
gzip_file = temp_dir / "sample.gz"
|
91
|
-
gzip_file.write_bytes(
|
93
|
+
gzip_file.write_bytes(
|
94
|
+
b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03" + b"compressed data"
|
95
|
+
)
|
92
96
|
archives["gzip"] = gzip_file
|
93
|
-
|
97
|
+
|
94
98
|
# BZIP2 file (magic: BZh)
|
95
99
|
bzip2_file = temp_dir / "sample.bz2"
|
96
|
-
bzip2_file.write_bytes(b
|
100
|
+
bzip2_file.write_bytes(b"BZh91AY&SY" + b"compressed data")
|
97
101
|
archives["bzip2"] = bzip2_file
|
98
|
-
|
102
|
+
|
99
103
|
# ZIP file (magic: PK\x03\x04)
|
100
104
|
zip_file = temp_dir / "sample.zip"
|
101
|
-
zip_file.write_bytes(b
|
105
|
+
zip_file.write_bytes(b"PK\x03\x04" + b"\x00" * 16 + b"zipfile")
|
102
106
|
archives["zip"] = zip_file
|
103
|
-
|
107
|
+
|
104
108
|
# TAR file (has specific header structure)
|
105
109
|
tar_file = temp_dir / "sample.tar"
|
106
110
|
# Minimal tar header (512 bytes)
|
107
|
-
tar_header = b
|
108
|
-
tar_header += b
|
109
|
-
tar_header += b
|
110
|
-
tar_header += b
|
111
|
-
tar_header += b
|
112
|
-
tar_header += b
|
113
|
-
tar_header += b
|
114
|
-
tar_header += b
|
115
|
-
tar_header += b
|
111
|
+
tar_header = b"testfile.txt" + b"\x00" * 88 # name
|
112
|
+
tar_header += b"0000644\x00" # mode
|
113
|
+
tar_header += b"0000000\x00" # uid
|
114
|
+
tar_header += b"0000000\x00" # gid
|
115
|
+
tar_header += b"00000000000\x00" # size
|
116
|
+
tar_header += b"00000000000\x00" # mtime
|
117
|
+
tar_header += b" " # checksum placeholder
|
118
|
+
tar_header += b"0" # typeflag
|
119
|
+
tar_header += b"\x00" * 355 # padding to 512 bytes
|
116
120
|
tar_file.write_bytes(tar_header[:512])
|
117
121
|
archives["tar"] = tar_file
|
118
|
-
|
122
|
+
|
119
123
|
yield archives
|
120
124
|
|
121
125
|
|
@@ -123,34 +127,34 @@ def multi_format_archives() -> Generator[dict[str, Path], None, None]:
|
|
123
127
|
def archive_with_permissions() -> Generator[Path, None, None]:
|
124
128
|
"""
|
125
129
|
Create files with specific permissions for archive permission testing.
|
126
|
-
|
130
|
+
|
127
131
|
Yields:
|
128
132
|
Path to directory containing files with various permission modes.
|
129
133
|
"""
|
130
134
|
with temp_directory() as temp_dir:
|
131
135
|
source = temp_dir / "permissions_test"
|
132
136
|
source.mkdir()
|
133
|
-
|
137
|
+
|
134
138
|
# Regular file
|
135
139
|
regular = source / "regular.txt"
|
136
140
|
regular.write_text("Regular file")
|
137
141
|
regular.chmod(0o644)
|
138
|
-
|
142
|
+
|
139
143
|
# Executable file
|
140
144
|
executable = source / "script.sh"
|
141
145
|
executable.write_text("#!/bin/bash\necho 'Hello'")
|
142
146
|
executable.chmod(0o755)
|
143
|
-
|
147
|
+
|
144
148
|
# Read-only file
|
145
149
|
readonly = source / "readonly.txt"
|
146
150
|
readonly.write_text("Read only content")
|
147
151
|
readonly.chmod(0o444)
|
148
|
-
|
152
|
+
|
149
153
|
# Directory with specific permissions
|
150
154
|
special_dir = source / "special"
|
151
155
|
special_dir.mkdir()
|
152
156
|
special_dir.chmod(0o700)
|
153
|
-
|
157
|
+
|
154
158
|
yield source
|
155
159
|
|
156
160
|
|
@@ -158,33 +162,33 @@ def archive_with_permissions() -> Generator[Path, None, None]:
|
|
158
162
|
def corrupted_archives() -> Generator[dict[str, Path], None, None]:
|
159
163
|
"""
|
160
164
|
Create corrupted archive files for error handling testing.
|
161
|
-
|
165
|
+
|
162
166
|
Yields:
|
163
167
|
Dict mapping format names to paths of corrupted archives.
|
164
168
|
"""
|
165
169
|
with temp_directory() as temp_dir:
|
166
170
|
corrupted = {}
|
167
|
-
|
171
|
+
|
168
172
|
# Corrupted GZIP (invalid header)
|
169
173
|
bad_gzip = temp_dir / "corrupted.gz"
|
170
|
-
bad_gzip.write_bytes(b
|
174
|
+
bad_gzip.write_bytes(b"\x1f\x8c" + b"not really gzip data")
|
171
175
|
corrupted["gzip"] = bad_gzip
|
172
|
-
|
176
|
+
|
173
177
|
# Corrupted ZIP (incomplete header)
|
174
178
|
bad_zip = temp_dir / "corrupted.zip"
|
175
|
-
bad_zip.write_bytes(b
|
179
|
+
bad_zip.write_bytes(b"PK\x03") # Incomplete magic
|
176
180
|
corrupted["zip"] = bad_zip
|
177
|
-
|
181
|
+
|
178
182
|
# Corrupted BZIP2 (wrong magic)
|
179
183
|
bad_bzip2 = temp_dir / "corrupted.bz2"
|
180
|
-
bad_bzip2.write_bytes(b
|
184
|
+
bad_bzip2.write_bytes(b"BZX" + b"not bzip2")
|
181
185
|
corrupted["bzip2"] = bad_bzip2
|
182
|
-
|
186
|
+
|
183
187
|
# Empty file claiming to be archive
|
184
188
|
empty_archive = temp_dir / "empty.tar.gz"
|
185
|
-
empty_archive.write_bytes(b
|
189
|
+
empty_archive.write_bytes(b"")
|
186
190
|
corrupted["empty"] = empty_archive
|
187
|
-
|
191
|
+
|
188
192
|
yield corrupted
|
189
193
|
|
190
194
|
|
@@ -192,26 +196,26 @@ def corrupted_archives() -> Generator[dict[str, Path], None, None]:
|
|
192
196
|
def archive_stress_test_files() -> Generator[Path, None, None]:
|
193
197
|
"""
|
194
198
|
Create a large number of files for stress testing archive operations.
|
195
|
-
|
199
|
+
|
196
200
|
Yields:
|
197
201
|
Path to directory with many files for stress testing.
|
198
202
|
"""
|
199
203
|
with temp_directory() as temp_dir:
|
200
204
|
stress_dir = temp_dir / "stress_test"
|
201
205
|
stress_dir.mkdir()
|
202
|
-
|
206
|
+
|
203
207
|
# Create 100 files in various subdirectories
|
204
208
|
for i in range(10):
|
205
209
|
subdir = stress_dir / f"subdir_{i}"
|
206
210
|
subdir.mkdir()
|
207
|
-
|
211
|
+
|
208
212
|
for j in range(10):
|
209
213
|
file_path = subdir / f"file_{j}.txt"
|
210
214
|
file_path.write_text(f"Content of file {i}_{j}\n" * 10)
|
211
|
-
|
215
|
+
|
212
216
|
# Add some binary files
|
213
217
|
for i in range(5):
|
214
218
|
bin_file = stress_dir / f"binary_{i}.dat"
|
215
219
|
bin_file.write_bytes(bytes(range(256)) * 10)
|
216
|
-
|
217
|
-
yield stress_dir
|
220
|
+
|
221
|
+
yield stress_dir
|
@@ -11,18 +11,18 @@ import os
|
|
11
11
|
from pathlib import Path
|
12
12
|
import tempfile
|
13
13
|
from typing import Any
|
14
|
-
from unittest.mock import MagicMock
|
15
14
|
|
16
15
|
import click
|
17
16
|
from click.testing import CliRunner
|
17
|
+
import pytest
|
18
18
|
|
19
|
-
from provide.foundation.context import
|
19
|
+
from provide.foundation.context import CLIContext
|
20
20
|
from provide.foundation.logger import get_logger
|
21
21
|
|
22
22
|
log = get_logger(__name__)
|
23
23
|
|
24
24
|
|
25
|
-
class MockContext(
|
25
|
+
class MockContext(CLIContext):
|
26
26
|
"""Mock context for testing that tracks method calls."""
|
27
27
|
|
28
28
|
def __init__(self, **kwargs) -> None:
|
@@ -157,7 +157,7 @@ def create_test_cli(
|
|
157
157
|
@click.pass_context
|
158
158
|
def cli(ctx, **kwargs) -> None:
|
159
159
|
"""Test CLI for testing."""
|
160
|
-
ctx.obj =
|
160
|
+
ctx.obj = CLIContext(**{k: v for k, v in kwargs.items() if v is not None})
|
161
161
|
|
162
162
|
if commands:
|
163
163
|
for cmd in commands:
|
@@ -166,22 +166,6 @@ def create_test_cli(
|
|
166
166
|
return cli
|
167
167
|
|
168
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
169
|
class CliTestCase:
|
186
170
|
"""Base class for CLI test cases with common utilities."""
|
187
171
|
|
@@ -225,3 +209,29 @@ class CliTestCase:
|
|
225
209
|
assert output[key] == value, (
|
226
210
|
f"Value mismatch for '{key}': {output[key]} != {value}"
|
227
211
|
)
|
212
|
+
|
213
|
+
|
214
|
+
@pytest.fixture
|
215
|
+
def click_testing_mode():
|
216
|
+
"""
|
217
|
+
Pytest fixture to enable Click testing mode.
|
218
|
+
|
219
|
+
Sets CLICK_TESTING=1 environment variable for the duration of the test,
|
220
|
+
then restores the original value. This fixture makes it easy to enable
|
221
|
+
Click testing mode without manual environment variable management.
|
222
|
+
|
223
|
+
Usage:
|
224
|
+
def test_my_cli(click_testing_mode):
|
225
|
+
# Test CLI code here - CLICK_TESTING is automatically set
|
226
|
+
pass
|
227
|
+
"""
|
228
|
+
original_value = os.environ.get("CLICK_TESTING")
|
229
|
+
os.environ["CLICK_TESTING"] = "1"
|
230
|
+
|
231
|
+
try:
|
232
|
+
yield
|
233
|
+
finally:
|
234
|
+
if original_value is None:
|
235
|
+
os.environ.pop("CLICK_TESTING", None)
|
236
|
+
else:
|
237
|
+
os.environ["CLICK_TESTING"] = original_value
|
@@ -6,29 +6,27 @@ in any project that depends on provide.foundation.
|
|
6
6
|
"""
|
7
7
|
|
8
8
|
from provide.foundation.testing.common.fixtures import (
|
9
|
-
mock_http_config,
|
10
|
-
mock_logger,
|
11
|
-
mock_telemetry_config,
|
12
|
-
mock_config_source,
|
13
|
-
mock_event_emitter,
|
14
|
-
mock_transport,
|
15
|
-
mock_metrics_collector,
|
16
9
|
mock_cache,
|
10
|
+
mock_config_source,
|
17
11
|
mock_database,
|
12
|
+
mock_event_emitter,
|
18
13
|
mock_file_system,
|
14
|
+
mock_http_config,
|
15
|
+
mock_metrics_collector,
|
19
16
|
mock_subprocess,
|
17
|
+
mock_telemetry_config,
|
18
|
+
mock_transport,
|
20
19
|
)
|
21
20
|
|
22
21
|
__all__ = [
|
23
|
-
"mock_http_config",
|
24
|
-
"mock_logger",
|
25
|
-
"mock_telemetry_config",
|
26
|
-
"mock_config_source",
|
27
|
-
"mock_event_emitter",
|
28
|
-
"mock_transport",
|
29
|
-
"mock_metrics_collector",
|
30
22
|
"mock_cache",
|
23
|
+
"mock_config_source",
|
31
24
|
"mock_database",
|
25
|
+
"mock_event_emitter",
|
32
26
|
"mock_file_system",
|
27
|
+
"mock_http_config",
|
28
|
+
"mock_metrics_collector",
|
33
29
|
"mock_subprocess",
|
34
|
-
|
30
|
+
"mock_telemetry_config",
|
31
|
+
"mock_transport",
|
32
|
+
]
|