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
@@ -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
- import os
29
+ config = get_stream_config()
29
30
 
30
- # Check NO_COLOR environment variable
31
- if os.getenv("NO_COLOR"):
31
+ if config.no_color:
32
32
  return False
33
33
 
34
- # Check FORCE_COLOR environment variable
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
- import os
23
+
24
+ config = get_stream_config()
22
25
 
23
26
  # Check environment variables for Click testing
24
- if os.getenv("CLICK_TESTING"):
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
- print(f"Failed to open log file {log_file_path}: {e}", file=sys.stderr)
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
- print(f"Failed to flush log file handle: {e}", file=sys.stderr)
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 ["reset_foundation_setup_for_testing", "reset_foundation_state"]:
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 ["archive", "common", "file", "process", "transport", "mocking", "time", "threading"]:
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
- archive_stress_test_files,
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
- "archive_stress_test_files",
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\xFF\xFE data",
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(content.encode() if isinstance(content, str) else content)
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(b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03' + b'compressed data')
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'BZh91AY&SY' + b'compressed data')
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'PK\x03\x04' + b'\x00' * 16 + b'zipfile')
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'testfile.txt' + b'\x00' * 88 # name
108
- tar_header += b'0000644\x00' # mode
109
- tar_header += b'0000000\x00' # uid
110
- tar_header += b'0000000\x00' # gid
111
- tar_header += b'00000000000\x00' # size
112
- tar_header += b'00000000000\x00' # mtime
113
- tar_header += b' ' # checksum placeholder
114
- tar_header += b'0' # typeflag
115
- tar_header += b'\x00' * 355 # padding to 512 bytes
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'\x1f\x8c' + b'not really gzip data')
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'PK\x03') # Incomplete magic
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'BZX' + b'not bzip2')
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 Context
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(Context):
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 = Context(**{k: v for k, v in kwargs.items() if v is not None})
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
+ ]