provide-foundation 0.0.0.dev1__py3-none-any.whl → 0.0.0.dev2__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 (93) hide show
  1. provide/foundation/__init__.py +29 -3
  2. provide/foundation/archive/operations.py +4 -6
  3. provide/foundation/cli/__init__.py +2 -2
  4. provide/foundation/cli/commands/deps.py +13 -7
  5. provide/foundation/cli/commands/logs/__init__.py +1 -1
  6. provide/foundation/cli/commands/logs/query.py +1 -1
  7. provide/foundation/cli/commands/logs/send.py +1 -1
  8. provide/foundation/cli/commands/logs/tail.py +1 -1
  9. provide/foundation/cli/decorators.py +11 -10
  10. provide/foundation/cli/main.py +1 -1
  11. provide/foundation/cli/testing.py +2 -35
  12. provide/foundation/cli/utils.py +21 -17
  13. provide/foundation/config/__init__.py +35 -2
  14. provide/foundation/config/converters.py +479 -0
  15. provide/foundation/config/defaults.py +67 -0
  16. provide/foundation/config/env.py +4 -19
  17. provide/foundation/config/loader.py +9 -3
  18. provide/foundation/console/input.py +5 -5
  19. provide/foundation/console/output.py +35 -13
  20. provide/foundation/context/__init__.py +8 -4
  21. provide/foundation/context/core.py +85 -109
  22. provide/foundation/crypto/certificates/operations.py +1 -1
  23. provide/foundation/errors/__init__.py +2 -3
  24. provide/foundation/errors/decorators.py +0 -231
  25. provide/foundation/errors/types.py +0 -97
  26. provide/foundation/file/directory.py +13 -22
  27. provide/foundation/file/lock.py +3 -1
  28. provide/foundation/hub/components.py +72 -384
  29. provide/foundation/hub/config.py +151 -0
  30. provide/foundation/hub/discovery.py +62 -0
  31. provide/foundation/hub/handlers.py +81 -0
  32. provide/foundation/hub/lifecycle.py +194 -0
  33. provide/foundation/hub/manager.py +4 -4
  34. provide/foundation/hub/processors.py +44 -0
  35. provide/foundation/integrations/__init__.py +11 -0
  36. provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
  37. provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
  38. provide/foundation/{observability → integrations}/openobserve/client.py +12 -12
  39. provide/foundation/{observability → integrations}/openobserve/commands.py +3 -3
  40. provide/foundation/integrations/openobserve/config.py +37 -0
  41. provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
  42. provide/foundation/{observability → integrations}/openobserve/otlp.py +1 -1
  43. provide/foundation/{observability → integrations}/openobserve/search.py +2 -2
  44. provide/foundation/{observability → integrations}/openobserve/streaming.py +4 -4
  45. provide/foundation/logger/config/logging.py +68 -298
  46. provide/foundation/logger/config/telemetry.py +41 -121
  47. provide/foundation/logger/setup/coordinator.py +1 -1
  48. provide/foundation/observability/__init__.py +2 -2
  49. provide/foundation/process/__init__.py +9 -0
  50. provide/foundation/process/exit.py +47 -0
  51. provide/foundation/process/lifecycle.py +33 -33
  52. provide/foundation/resilience/__init__.py +35 -0
  53. provide/foundation/resilience/circuit.py +164 -0
  54. provide/foundation/resilience/decorators.py +220 -0
  55. provide/foundation/resilience/fallback.py +193 -0
  56. provide/foundation/resilience/retry.py +325 -0
  57. provide/foundation/streams/config.py +79 -0
  58. provide/foundation/streams/console.py +7 -8
  59. provide/foundation/streams/core.py +6 -3
  60. provide/foundation/streams/file.py +12 -2
  61. provide/foundation/testing/__init__.py +7 -2
  62. provide/foundation/testing/cli.py +30 -17
  63. provide/foundation/testing/common/__init__.py +0 -2
  64. provide/foundation/testing/common/fixtures.py +0 -27
  65. provide/foundation/testing/file/content_fixtures.py +316 -0
  66. provide/foundation/testing/file/directory_fixtures.py +107 -0
  67. provide/foundation/testing/file/fixtures.py +45 -516
  68. provide/foundation/testing/file/special_fixtures.py +153 -0
  69. provide/foundation/testing/logger.py +76 -0
  70. provide/foundation/testing/process/async_fixtures.py +405 -0
  71. provide/foundation/testing/process/fixtures.py +50 -571
  72. provide/foundation/testing/process/subprocess_fixtures.py +209 -0
  73. provide/foundation/testing/threading/basic_fixtures.py +101 -0
  74. provide/foundation/testing/threading/data_fixtures.py +99 -0
  75. provide/foundation/testing/threading/execution_fixtures.py +263 -0
  76. provide/foundation/testing/threading/fixtures.py +34 -500
  77. provide/foundation/testing/threading/sync_fixtures.py +97 -0
  78. provide/foundation/testing/time/fixtures.py +4 -4
  79. provide/foundation/tools/cache.py +8 -6
  80. provide/foundation/tools/downloader.py +23 -12
  81. provide/foundation/tracer/spans.py +2 -2
  82. provide/foundation/transport/config.py +26 -95
  83. provide/foundation/transport/middleware.py +30 -36
  84. provide/foundation/utils/deps.py +14 -12
  85. provide/foundation/utils/parsing.py +49 -4
  86. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev2.dist-info}/METADATA +1 -1
  87. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev2.dist-info}/RECORD +93 -68
  88. /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
  89. /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
  90. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev2.dist-info}/WHEEL +0 -0
  91. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev2.dist-info}/entry_points.txt +0 -0
  92. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
  93. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev2.dist-info}/top_level.txt +0 -0
@@ -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)
@@ -15,14 +15,15 @@ from unittest.mock import MagicMock
15
15
 
16
16
  import click
17
17
  from click.testing import CliRunner
18
+ import pytest
18
19
 
19
- from provide.foundation.context import Context
20
+ from provide.foundation.context import CLIContext
20
21
  from provide.foundation.logger import get_logger
21
22
 
22
23
  log = get_logger(__name__)
23
24
 
24
25
 
25
- class MockContext(Context):
26
+ class MockContext(CLIContext):
26
27
  """Mock context for testing that tracks method calls."""
27
28
 
28
29
  def __init__(self, **kwargs) -> None:
@@ -157,7 +158,7 @@ def create_test_cli(
157
158
  @click.pass_context
158
159
  def cli(ctx, **kwargs) -> None:
159
160
  """Test CLI for testing."""
160
- ctx.obj = Context(**{k: v for k, v in kwargs.items() if v is not None})
161
+ ctx.obj = CLIContext(**{k: v for k, v in kwargs.items() if v is not None})
161
162
 
162
163
  if commands:
163
164
  for cmd in commands:
@@ -166,20 +167,6 @@ def create_test_cli(
166
167
  return cli
167
168
 
168
169
 
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
170
 
184
171
 
185
172
  class CliTestCase:
@@ -225,3 +212,29 @@ class CliTestCase:
225
212
  assert output[key] == value, (
226
213
  f"Value mismatch for '{key}': {output[key]} != {value}"
227
214
  )
215
+
216
+
217
+ @pytest.fixture
218
+ def click_testing_mode():
219
+ """
220
+ Pytest fixture to enable Click testing mode.
221
+
222
+ Sets CLICK_TESTING=1 environment variable for the duration of the test,
223
+ then restores the original value. This fixture makes it easy to enable
224
+ Click testing mode without manual environment variable management.
225
+
226
+ Usage:
227
+ def test_my_cli(click_testing_mode):
228
+ # Test CLI code here - CLICK_TESTING is automatically set
229
+ pass
230
+ """
231
+ original_value = os.environ.get("CLICK_TESTING")
232
+ os.environ["CLICK_TESTING"] = "1"
233
+
234
+ try:
235
+ yield
236
+ finally:
237
+ if original_value is None:
238
+ os.environ.pop("CLICK_TESTING", None)
239
+ else:
240
+ os.environ["CLICK_TESTING"] = original_value
@@ -7,7 +7,6 @@ in any project that depends on provide.foundation.
7
7
 
8
8
  from provide.foundation.testing.common.fixtures import (
9
9
  mock_http_config,
10
- mock_logger,
11
10
  mock_telemetry_config,
12
11
  mock_config_source,
13
12
  mock_event_emitter,
@@ -21,7 +20,6 @@ from provide.foundation.testing.common.fixtures import (
21
20
 
22
21
  __all__ = [
23
22
  "mock_http_config",
24
- "mock_logger",
25
23
  "mock_telemetry_config",
26
24
  "mock_config_source",
27
25
  "mock_event_emitter",
@@ -37,33 +37,6 @@ def mock_http_config():
37
37
  )
38
38
 
39
39
 
40
- @pytest.fixture
41
- def mock_logger():
42
- """
43
- Mock logger with captured method calls.
44
-
45
- Returns:
46
- Mock logger with debug, info, warning, error methods.
47
- """
48
- logger = Mock()
49
- logger.debug = Mock()
50
- logger.info = Mock()
51
- logger.warning = Mock()
52
- logger.error = Mock()
53
- logger.exception = Mock()
54
- logger.critical = Mock()
55
-
56
- # Add common logger attributes
57
- logger.name = "mock_logger"
58
- logger.level = 10 # DEBUG level
59
- logger.handlers = []
60
- logger.disabled = False
61
-
62
- # Add bind method for structlog compatibility
63
- logger.bind = Mock(return_value=logger)
64
- logger.unbind = Mock(return_value=logger)
65
-
66
- return logger
67
40
 
68
41
 
69
42
  @pytest.fixture
@@ -0,0 +1,316 @@
1
+ """
2
+ Content-based file test fixtures.
3
+
4
+ Fixtures for creating files with specific content types like text, binary,
5
+ CSV, JSON, and other structured data.
6
+ """
7
+
8
+ import csv
9
+ import json
10
+ import random
11
+ import tempfile
12
+ from pathlib import Path
13
+
14
+ import pytest
15
+
16
+ from provide.foundation.file.safe import safe_delete
17
+
18
+
19
+ @pytest.fixture
20
+ def temp_file():
21
+ """
22
+ Create a temporary file factory with optional content.
23
+
24
+ Returns:
25
+ A function that creates temporary files with specified content and suffix.
26
+ """
27
+ created_files = []
28
+
29
+ def _make_temp_file(content: str = "test content", suffix: str = ".txt") -> Path:
30
+ """
31
+ Create a temporary file.
32
+
33
+ Args:
34
+ content: Content to write to the file
35
+ suffix: File suffix/extension
36
+
37
+ Returns:
38
+ Path to the created temporary file
39
+ """
40
+ with tempfile.NamedTemporaryFile(mode='w', suffix=suffix, delete=False) as f:
41
+ f.write(content)
42
+ path = Path(f.name)
43
+ created_files.append(path)
44
+ return path
45
+
46
+ yield _make_temp_file
47
+
48
+ # Cleanup all created files
49
+ for path in created_files:
50
+ safe_delete(path, missing_ok=True)
51
+
52
+
53
+ @pytest.fixture
54
+ def temp_named_file():
55
+ """
56
+ Create a named temporary file factory.
57
+
58
+ Returns:
59
+ Function that creates named temporary files.
60
+ """
61
+ created_files = []
62
+
63
+ def _make_named_file(
64
+ content: bytes | str = None,
65
+ suffix: str = "",
66
+ prefix: str = "tmp",
67
+ dir: Path | str = None,
68
+ mode: str = 'w+b',
69
+ delete: bool = False
70
+ ) -> Path:
71
+ """
72
+ Create a named temporary file.
73
+
74
+ Args:
75
+ content: Optional content to write
76
+ suffix: File suffix
77
+ prefix: File prefix
78
+ dir: Directory for the file
79
+ mode: File mode
80
+ delete: Whether to delete on close
81
+
82
+ Returns:
83
+ Path to the created file
84
+ """
85
+ if isinstance(dir, Path):
86
+ dir = str(dir)
87
+
88
+ f = tempfile.NamedTemporaryFile(
89
+ mode=mode,
90
+ suffix=suffix,
91
+ prefix=prefix,
92
+ dir=dir,
93
+ delete=delete
94
+ )
95
+
96
+ if content is not None:
97
+ if isinstance(content, str):
98
+ if 'b' in mode:
99
+ f.write(content.encode())
100
+ else:
101
+ f.write(content)
102
+ else:
103
+ f.write(content)
104
+ f.flush()
105
+
106
+ path = Path(f.name)
107
+ f.close()
108
+
109
+ if not delete:
110
+ created_files.append(path)
111
+
112
+ return path
113
+
114
+ yield _make_named_file
115
+
116
+ # Cleanup
117
+ for path in created_files:
118
+ safe_delete(path, missing_ok=True)
119
+
120
+
121
+ @pytest.fixture
122
+ def temp_file_with_content():
123
+ """
124
+ Create temporary files with specific content.
125
+
126
+ Returns:
127
+ Function that creates files with content.
128
+ """
129
+ created_files = []
130
+
131
+ def _make_file(
132
+ content: str | bytes,
133
+ suffix: str = ".txt",
134
+ encoding: str = "utf-8"
135
+ ) -> Path:
136
+ """
137
+ Create a temporary file with content.
138
+
139
+ Args:
140
+ content: Content to write
141
+ suffix: File suffix
142
+ encoding: Text encoding (for str content)
143
+
144
+ Returns:
145
+ Path to created file
146
+ """
147
+ with tempfile.NamedTemporaryFile(
148
+ mode='wb' if isinstance(content, bytes) else 'w',
149
+ suffix=suffix,
150
+ delete=False,
151
+ encoding=None if isinstance(content, bytes) else encoding
152
+ ) as f:
153
+ f.write(content)
154
+ path = Path(f.name)
155
+
156
+ created_files.append(path)
157
+ return path
158
+
159
+ yield _make_file
160
+
161
+ # Cleanup
162
+ for path in created_files:
163
+ safe_delete(path, missing_ok=True)
164
+
165
+
166
+ @pytest.fixture
167
+ def temp_binary_file():
168
+ """
169
+ Create temporary binary files.
170
+
171
+ Returns:
172
+ Function that creates binary files.
173
+ """
174
+ created_files = []
175
+
176
+ def _make_binary(
177
+ size: int = 1024,
178
+ pattern: bytes = None,
179
+ suffix: str = ".bin"
180
+ ) -> Path:
181
+ """
182
+ Create a temporary binary file.
183
+
184
+ Args:
185
+ size: File size in bytes
186
+ pattern: Optional byte pattern to repeat
187
+ suffix: File suffix
188
+
189
+ Returns:
190
+ Path to created binary file
191
+ """
192
+ if pattern is None:
193
+ # Create pseudo-random binary data
194
+ content = bytes(random.randint(0, 255) for _ in range(size))
195
+ else:
196
+ # Repeat pattern to reach size
197
+ repetitions = size // len(pattern) + 1
198
+ content = (pattern * repetitions)[:size]
199
+
200
+ with tempfile.NamedTemporaryFile(
201
+ mode='wb',
202
+ suffix=suffix,
203
+ delete=False
204
+ ) as f:
205
+ f.write(content)
206
+ path = Path(f.name)
207
+
208
+ created_files.append(path)
209
+ return path
210
+
211
+ yield _make_binary
212
+
213
+ # Cleanup
214
+ for path in created_files:
215
+ safe_delete(path, missing_ok=True)
216
+
217
+
218
+ @pytest.fixture
219
+ def temp_csv_file():
220
+ """
221
+ Create temporary CSV files for testing.
222
+
223
+ Returns:
224
+ Function that creates CSV files.
225
+ """
226
+ created_files = []
227
+
228
+ def _make_csv(
229
+ headers: list[str],
230
+ rows: list[list],
231
+ suffix: str = ".csv"
232
+ ) -> Path:
233
+ """
234
+ Create a temporary CSV file.
235
+
236
+ Args:
237
+ headers: Column headers
238
+ rows: Data rows
239
+ suffix: File suffix
240
+
241
+ Returns:
242
+ Path to created CSV file
243
+ """
244
+ with tempfile.NamedTemporaryFile(
245
+ mode='w',
246
+ suffix=suffix,
247
+ delete=False,
248
+ newline=''
249
+ ) as f:
250
+ writer = csv.writer(f)
251
+ writer.writerow(headers)
252
+ writer.writerows(rows)
253
+ path = Path(f.name)
254
+
255
+ created_files.append(path)
256
+ return path
257
+
258
+ yield _make_csv
259
+
260
+ # Cleanup
261
+ for path in created_files:
262
+ safe_delete(path, missing_ok=True)
263
+
264
+
265
+ @pytest.fixture
266
+ def temp_json_file():
267
+ """
268
+ Create temporary JSON files for testing.
269
+
270
+ Returns:
271
+ Function that creates JSON files.
272
+ """
273
+ created_files = []
274
+
275
+ def _make_json(
276
+ data: dict | list,
277
+ suffix: str = ".json",
278
+ indent: int = 2
279
+ ) -> Path:
280
+ """
281
+ Create a temporary JSON file.
282
+
283
+ Args:
284
+ data: JSON data to write
285
+ suffix: File suffix
286
+ indent: JSON indentation
287
+
288
+ Returns:
289
+ Path to created JSON file
290
+ """
291
+ with tempfile.NamedTemporaryFile(
292
+ mode='w',
293
+ suffix=suffix,
294
+ delete=False
295
+ ) as f:
296
+ json.dump(data, f, indent=indent)
297
+ path = Path(f.name)
298
+
299
+ created_files.append(path)
300
+ return path
301
+
302
+ yield _make_json
303
+
304
+ # Cleanup
305
+ for path in created_files:
306
+ safe_delete(path, missing_ok=True)
307
+
308
+
309
+ __all__ = [
310
+ "temp_file",
311
+ "temp_named_file",
312
+ "temp_file_with_content",
313
+ "temp_binary_file",
314
+ "temp_csv_file",
315
+ "temp_json_file",
316
+ ]
@@ -0,0 +1,107 @@
1
+ """
2
+ Directory-specific test fixtures.
3
+
4
+ Fixtures for creating temporary directories, nested structures,
5
+ and standard test directory layouts.
6
+ """
7
+
8
+ import tempfile
9
+ from pathlib import Path
10
+ from collections.abc import Generator
11
+
12
+ import pytest
13
+
14
+
15
+ @pytest.fixture
16
+ def temp_directory() -> Generator[Path, None, None]:
17
+ """
18
+ Create a temporary directory that's cleaned up after test.
19
+
20
+ Yields:
21
+ Path to the temporary directory.
22
+ """
23
+ with tempfile.TemporaryDirectory() as temp_dir:
24
+ yield Path(temp_dir)
25
+
26
+
27
+ @pytest.fixture
28
+ def test_files_structure() -> Generator[tuple[Path, Path], None, None]:
29
+ """
30
+ Create standard test file structure with files and subdirectories.
31
+
32
+ Creates:
33
+ - source/
34
+ - file1.txt (contains "Content 1")
35
+ - file2.txt (contains "Content 2")
36
+ - subdir/
37
+ - file3.txt (contains "Content 3")
38
+
39
+ Yields:
40
+ Tuple of (temp_path, source_path)
41
+ """
42
+ with tempfile.TemporaryDirectory() as temp_dir:
43
+ path = Path(temp_dir)
44
+ source = path / "source"
45
+ source.mkdir()
46
+
47
+ # Create test files
48
+ (source / "file1.txt").write_text("Content 1")
49
+ (source / "file2.txt").write_text("Content 2")
50
+
51
+ # Create subdirectory with files
52
+ subdir = source / "subdir"
53
+ subdir.mkdir()
54
+ (subdir / "file3.txt").write_text("Content 3")
55
+
56
+ yield path, source
57
+
58
+
59
+ @pytest.fixture
60
+ def nested_directory_structure() -> Generator[Path, None, None]:
61
+ """
62
+ Create a deeply nested directory structure for testing.
63
+
64
+ Creates:
65
+ - level1/
66
+ - level2/
67
+ - level3/
68
+ - deep_file.txt
69
+ - file_l2.txt
70
+ - file_l1.txt
71
+
72
+ Yields:
73
+ Path to the root of the structure.
74
+ """
75
+ with tempfile.TemporaryDirectory() as temp_dir:
76
+ root = Path(temp_dir)
77
+
78
+ # Create nested structure
79
+ deep_dir = root / "level1" / "level2" / "level3"
80
+ deep_dir.mkdir(parents=True)
81
+
82
+ # Add files at different levels
83
+ (root / "file_l1.txt").write_text("Level 1 file")
84
+ (root / "level1" / "file_l2.txt").write_text("Level 2 file")
85
+ (deep_dir / "deep_file.txt").write_text("Deep file")
86
+
87
+ yield root
88
+
89
+
90
+ @pytest.fixture
91
+ def empty_directory() -> Generator[Path, None, None]:
92
+ """
93
+ Create an empty temporary directory.
94
+
95
+ Yields:
96
+ Path to an empty directory.
97
+ """
98
+ with tempfile.TemporaryDirectory() as temp_dir:
99
+ yield Path(temp_dir)
100
+
101
+
102
+ __all__ = [
103
+ "temp_directory",
104
+ "test_files_structure",
105
+ "nested_directory_structure",
106
+ "empty_directory",
107
+ ]