provide-foundation 0.0.0.dev0__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 (161) hide show
  1. provide/foundation/__init__.py +41 -23
  2. provide/foundation/archive/__init__.py +23 -0
  3. provide/foundation/archive/base.py +70 -0
  4. provide/foundation/archive/bzip2.py +157 -0
  5. provide/foundation/archive/gzip.py +159 -0
  6. provide/foundation/archive/operations.py +334 -0
  7. provide/foundation/archive/tar.py +164 -0
  8. provide/foundation/archive/zip.py +203 -0
  9. provide/foundation/cli/__init__.py +2 -2
  10. provide/foundation/cli/commands/deps.py +13 -7
  11. provide/foundation/cli/commands/logs/__init__.py +1 -1
  12. provide/foundation/cli/commands/logs/query.py +1 -1
  13. provide/foundation/cli/commands/logs/send.py +1 -1
  14. provide/foundation/cli/commands/logs/tail.py +1 -1
  15. provide/foundation/cli/decorators.py +11 -10
  16. provide/foundation/cli/main.py +1 -1
  17. provide/foundation/cli/testing.py +2 -35
  18. provide/foundation/cli/utils.py +21 -17
  19. provide/foundation/config/__init__.py +35 -2
  20. provide/foundation/config/base.py +2 -2
  21. provide/foundation/config/converters.py +479 -0
  22. provide/foundation/config/defaults.py +67 -0
  23. provide/foundation/config/env.py +4 -19
  24. provide/foundation/config/loader.py +9 -3
  25. provide/foundation/config/sync.py +19 -4
  26. provide/foundation/console/input.py +5 -5
  27. provide/foundation/console/output.py +35 -13
  28. provide/foundation/context/__init__.py +8 -4
  29. provide/foundation/context/core.py +85 -109
  30. provide/foundation/core.py +1 -2
  31. provide/foundation/crypto/__init__.py +2 -0
  32. provide/foundation/crypto/certificates/__init__.py +34 -0
  33. provide/foundation/crypto/certificates/base.py +173 -0
  34. provide/foundation/crypto/certificates/certificate.py +290 -0
  35. provide/foundation/crypto/certificates/factory.py +213 -0
  36. provide/foundation/crypto/certificates/generator.py +138 -0
  37. provide/foundation/crypto/certificates/loader.py +130 -0
  38. provide/foundation/crypto/certificates/operations.py +198 -0
  39. provide/foundation/crypto/certificates/trust.py +107 -0
  40. provide/foundation/errors/__init__.py +2 -3
  41. provide/foundation/errors/decorators.py +0 -231
  42. provide/foundation/errors/types.py +0 -97
  43. provide/foundation/eventsets/__init__.py +0 -0
  44. provide/foundation/eventsets/display.py +84 -0
  45. provide/foundation/eventsets/registry.py +160 -0
  46. provide/foundation/eventsets/resolver.py +192 -0
  47. provide/foundation/eventsets/sets/das.py +128 -0
  48. provide/foundation/eventsets/sets/database.py +125 -0
  49. provide/foundation/eventsets/sets/http.py +153 -0
  50. provide/foundation/eventsets/sets/llm.py +139 -0
  51. provide/foundation/eventsets/sets/task_queue.py +107 -0
  52. provide/foundation/eventsets/types.py +70 -0
  53. provide/foundation/file/directory.py +13 -22
  54. provide/foundation/file/lock.py +3 -1
  55. provide/foundation/hub/components.py +77 -515
  56. provide/foundation/hub/config.py +151 -0
  57. provide/foundation/hub/discovery.py +62 -0
  58. provide/foundation/hub/handlers.py +81 -0
  59. provide/foundation/hub/lifecycle.py +194 -0
  60. provide/foundation/hub/manager.py +4 -4
  61. provide/foundation/hub/processors.py +44 -0
  62. provide/foundation/integrations/__init__.py +11 -0
  63. provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
  64. provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
  65. provide/foundation/{observability → integrations}/openobserve/client.py +12 -12
  66. provide/foundation/{observability → integrations}/openobserve/commands.py +3 -3
  67. provide/foundation/integrations/openobserve/config.py +37 -0
  68. provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
  69. provide/foundation/{observability → integrations}/openobserve/otlp.py +1 -1
  70. provide/foundation/{observability → integrations}/openobserve/search.py +2 -2
  71. provide/foundation/{observability → integrations}/openobserve/streaming.py +4 -4
  72. provide/foundation/logger/__init__.py +3 -10
  73. provide/foundation/logger/config/logging.py +68 -298
  74. provide/foundation/logger/config/telemetry.py +41 -121
  75. provide/foundation/logger/core.py +0 -2
  76. provide/foundation/logger/custom_processors.py +1 -0
  77. provide/foundation/logger/factories.py +11 -2
  78. provide/foundation/logger/processors/main.py +20 -84
  79. provide/foundation/logger/setup/__init__.py +5 -1
  80. provide/foundation/logger/setup/coordinator.py +76 -24
  81. provide/foundation/logger/setup/processors.py +2 -9
  82. provide/foundation/logger/trace.py +27 -0
  83. provide/foundation/metrics/otel.py +10 -10
  84. provide/foundation/observability/__init__.py +2 -2
  85. provide/foundation/process/__init__.py +9 -0
  86. provide/foundation/process/exit.py +47 -0
  87. provide/foundation/process/lifecycle.py +115 -59
  88. provide/foundation/resilience/__init__.py +35 -0
  89. provide/foundation/resilience/circuit.py +164 -0
  90. provide/foundation/resilience/decorators.py +220 -0
  91. provide/foundation/resilience/fallback.py +193 -0
  92. provide/foundation/resilience/retry.py +325 -0
  93. provide/foundation/streams/config.py +79 -0
  94. provide/foundation/streams/console.py +7 -8
  95. provide/foundation/streams/core.py +6 -3
  96. provide/foundation/streams/file.py +12 -2
  97. provide/foundation/testing/__init__.py +84 -2
  98. provide/foundation/testing/archive/__init__.py +24 -0
  99. provide/foundation/testing/archive/fixtures.py +217 -0
  100. provide/foundation/testing/cli.py +30 -17
  101. provide/foundation/testing/common/__init__.py +32 -0
  102. provide/foundation/testing/common/fixtures.py +236 -0
  103. provide/foundation/testing/file/__init__.py +40 -0
  104. provide/foundation/testing/file/content_fixtures.py +316 -0
  105. provide/foundation/testing/file/directory_fixtures.py +107 -0
  106. provide/foundation/testing/file/fixtures.py +52 -0
  107. provide/foundation/testing/file/special_fixtures.py +153 -0
  108. provide/foundation/testing/logger.py +117 -11
  109. provide/foundation/testing/mocking/__init__.py +46 -0
  110. provide/foundation/testing/mocking/fixtures.py +331 -0
  111. provide/foundation/testing/process/__init__.py +48 -0
  112. provide/foundation/testing/process/async_fixtures.py +405 -0
  113. provide/foundation/testing/process/fixtures.py +56 -0
  114. provide/foundation/testing/process/subprocess_fixtures.py +209 -0
  115. provide/foundation/testing/threading/__init__.py +38 -0
  116. provide/foundation/testing/threading/basic_fixtures.py +101 -0
  117. provide/foundation/testing/threading/data_fixtures.py +99 -0
  118. provide/foundation/testing/threading/execution_fixtures.py +263 -0
  119. provide/foundation/testing/threading/fixtures.py +54 -0
  120. provide/foundation/testing/threading/sync_fixtures.py +97 -0
  121. provide/foundation/testing/time/__init__.py +32 -0
  122. provide/foundation/testing/time/fixtures.py +409 -0
  123. provide/foundation/testing/transport/__init__.py +30 -0
  124. provide/foundation/testing/transport/fixtures.py +280 -0
  125. provide/foundation/tools/__init__.py +58 -0
  126. provide/foundation/tools/base.py +348 -0
  127. provide/foundation/tools/cache.py +268 -0
  128. provide/foundation/tools/downloader.py +224 -0
  129. provide/foundation/tools/installer.py +254 -0
  130. provide/foundation/tools/registry.py +223 -0
  131. provide/foundation/tools/resolver.py +321 -0
  132. provide/foundation/tools/verifier.py +186 -0
  133. provide/foundation/tracer/otel.py +7 -11
  134. provide/foundation/tracer/spans.py +2 -2
  135. provide/foundation/transport/__init__.py +155 -0
  136. provide/foundation/transport/base.py +171 -0
  137. provide/foundation/transport/client.py +266 -0
  138. provide/foundation/transport/config.py +140 -0
  139. provide/foundation/transport/errors.py +79 -0
  140. provide/foundation/transport/http.py +232 -0
  141. provide/foundation/transport/middleware.py +360 -0
  142. provide/foundation/transport/registry.py +167 -0
  143. provide/foundation/transport/types.py +45 -0
  144. provide/foundation/utils/deps.py +14 -12
  145. provide/foundation/utils/parsing.py +49 -4
  146. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/METADATA +5 -28
  147. provide_foundation-0.0.0.dev2.dist-info/RECORD +225 -0
  148. provide/foundation/cli/commands/logs/generate_old.py +0 -569
  149. provide/foundation/crypto/certificates.py +0 -896
  150. provide/foundation/logger/emoji/__init__.py +0 -44
  151. provide/foundation/logger/emoji/matrix.py +0 -209
  152. provide/foundation/logger/emoji/sets.py +0 -458
  153. provide/foundation/logger/emoji/types.py +0 -56
  154. provide/foundation/logger/setup/emoji_resolver.py +0 -64
  155. provide_foundation-0.0.0.dev0.dist-info/RECORD +0 -149
  156. /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
  157. /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
  158. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/WHEEL +0 -0
  159. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/entry_points.txt +0 -0
  160. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
  161. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,409 @@
1
+ """
2
+ Time Testing Fixtures and Utilities.
3
+
4
+ Fixtures for mocking time, freezing time, and testing time-dependent code
5
+ across the provide-io ecosystem.
6
+ """
7
+
8
+ import time
9
+ import datetime
10
+ from typing import Any, Callable
11
+ from collections.abc import Generator
12
+ from unittest.mock import Mock, patch
13
+
14
+ import pytest
15
+
16
+
17
+ @pytest.fixture
18
+ def freeze_time():
19
+ """
20
+ Fixture to freeze time at a specific point.
21
+
22
+ Returns:
23
+ Function that freezes time and returns a context manager.
24
+ """
25
+ class FrozenTime:
26
+ def __init__(self, frozen_time: datetime.datetime | None = None):
27
+ self.frozen_time = frozen_time or datetime.datetime.now()
28
+ self.original_time = time.time
29
+ self.original_datetime = datetime.datetime
30
+ self.patches = []
31
+
32
+ def __enter__(self):
33
+ # Patch time.time()
34
+ time_patch = patch('time.time', return_value=self.frozen_time.timestamp())
35
+ self.patches.append(time_patch)
36
+ time_patch.start()
37
+
38
+ # Patch datetime.datetime.now()
39
+ datetime_patch = patch('datetime.datetime', wraps=datetime.datetime)
40
+ mock_datetime = datetime_patch.start()
41
+ mock_datetime.now.return_value = self.frozen_time
42
+ mock_datetime.utcnow.return_value = self.frozen_time
43
+ self.patches.append(datetime_patch)
44
+
45
+ return self
46
+
47
+ def __exit__(self, *args):
48
+ for p in self.patches:
49
+ p.stop()
50
+
51
+ def tick(self, seconds: float = 1.0):
52
+ """Advance the frozen time by the specified seconds."""
53
+ self.frozen_time += datetime.timedelta(seconds=seconds)
54
+ # Update mocks
55
+ for p in self.patches:
56
+ if hasattr(p, 'return_value'):
57
+ p.return_value = self.frozen_time.timestamp()
58
+
59
+ def _freeze(at: datetime.datetime | None = None) -> FrozenTime:
60
+ """
61
+ Freeze time at a specific point.
62
+
63
+ Args:
64
+ at: Optional datetime to freeze at (defaults to now)
65
+
66
+ Returns:
67
+ FrozenTime context manager
68
+ """
69
+ return FrozenTime(at)
70
+
71
+ return _freeze
72
+
73
+
74
+ @pytest.fixture
75
+ def mock_sleep():
76
+ """
77
+ Mock time.sleep to speed up tests.
78
+
79
+ Returns:
80
+ Mock object that replaces time.sleep.
81
+ """
82
+ with patch('time.sleep') as mock:
83
+ # Make sleep instant by default
84
+ mock.return_value = None
85
+ yield mock
86
+
87
+
88
+ @pytest.fixture
89
+ def mock_sleep_with_callback():
90
+ """
91
+ Mock time.sleep with a callback for each sleep call.
92
+
93
+ Returns:
94
+ Function to set up sleep mock with callback.
95
+ """
96
+ def _mock_sleep(callback: Callable[[float], None] = None):
97
+ """
98
+ Create a mock sleep with optional callback.
99
+
100
+ Args:
101
+ callback: Function called with sleep duration
102
+
103
+ Returns:
104
+ Mock sleep object
105
+ """
106
+ def sleep_side_effect(seconds):
107
+ if callback:
108
+ callback(seconds)
109
+ return None
110
+
111
+ mock = Mock(side_effect=sleep_side_effect)
112
+ return mock
113
+
114
+ return _mock_sleep
115
+
116
+
117
+ @pytest.fixture
118
+ def time_machine():
119
+ """
120
+ Advanced time manipulation fixture.
121
+
122
+ Provides methods to:
123
+ - Freeze time
124
+ - Speed up/slow down time
125
+ - Jump to specific times
126
+
127
+ Returns:
128
+ TimeMachine instance for time manipulation.
129
+ """
130
+ class TimeMachine:
131
+ def __init__(self):
132
+ self.current_time = time.time()
133
+ self.speed_multiplier = 1.0
134
+ self.patches = []
135
+ self.is_frozen = False
136
+
137
+ def freeze(self, at: float | None = None):
138
+ """Freeze time at a specific timestamp."""
139
+ self.is_frozen = True
140
+ self.current_time = at or time.time()
141
+
142
+ patcher = patch('time.time', return_value=self.current_time)
143
+ mock = patcher.start()
144
+ self.patches.append(patcher)
145
+ return self
146
+
147
+ def unfreeze(self):
148
+ """Unfreeze time."""
149
+ self.is_frozen = False
150
+ for p in self.patches:
151
+ p.stop()
152
+ self.patches.clear()
153
+
154
+ def jump(self, seconds: float):
155
+ """Jump forward or backward in time."""
156
+ self.current_time += seconds
157
+ if self.is_frozen:
158
+ for p in self.patches:
159
+ if hasattr(p, 'return_value'):
160
+ p.return_value = self.current_time
161
+
162
+ def speed_up(self, factor: float):
163
+ """Speed up time by a factor."""
164
+ self.speed_multiplier = factor
165
+
166
+ def slow_down(self, factor: float):
167
+ """Slow down time by a factor."""
168
+ self.speed_multiplier = 1.0 / factor
169
+
170
+ def cleanup(self):
171
+ """Clean up all patches."""
172
+ for p in self.patches:
173
+ p.stop()
174
+
175
+ machine = TimeMachine()
176
+ yield machine
177
+ machine.cleanup()
178
+
179
+
180
+ @pytest.fixture
181
+ def timer():
182
+ """
183
+ Timer fixture for measuring execution time.
184
+
185
+ Returns:
186
+ Timer instance for measuring durations.
187
+ """
188
+ class Timer:
189
+ def __init__(self):
190
+ self.start_time = None
191
+ self.end_time = None
192
+ self.durations = []
193
+
194
+ def start(self):
195
+ """Start the timer."""
196
+ self.start_time = time.perf_counter()
197
+ return self
198
+
199
+ def stop(self) -> float:
200
+ """Stop the timer and return duration."""
201
+ self.end_time = time.perf_counter()
202
+ if self.start_time is None:
203
+ raise RuntimeError("Timer not started")
204
+ duration = self.end_time - self.start_time
205
+ self.durations.append(duration)
206
+ return duration
207
+
208
+ def __enter__(self):
209
+ """Context manager entry."""
210
+ self.start()
211
+ return self
212
+
213
+ def __exit__(self, *args):
214
+ """Context manager exit."""
215
+ self.stop()
216
+
217
+ @property
218
+ def elapsed(self) -> float:
219
+ """Get elapsed time since start."""
220
+ if self.start_time is None:
221
+ raise RuntimeError("Timer not started")
222
+ if self.end_time is None:
223
+ return time.perf_counter() - self.start_time
224
+ return self.end_time - self.start_time
225
+
226
+ @property
227
+ def average(self) -> float:
228
+ """Get average duration from all measurements."""
229
+ if not self.durations:
230
+ return 0.0
231
+ return sum(self.durations) / len(self.durations)
232
+
233
+ def reset(self):
234
+ """Reset the timer."""
235
+ self.start_time = None
236
+ self.end_time = None
237
+ self.durations.clear()
238
+
239
+ return Timer()
240
+
241
+
242
+ @pytest.fixture
243
+ def mock_datetime():
244
+ """
245
+ Mock datetime module for testing.
246
+
247
+ Returns:
248
+ Mock datetime module with common methods mocked.
249
+ """
250
+ with patch('datetime.datetime') as mock_dt:
251
+ # Set up a fake "now"
252
+ fake_now = datetime.datetime(2024, 1, 1, 12, 0, 0)
253
+ mock_dt.now.return_value = fake_now
254
+ mock_dt.utcnow.return_value = fake_now
255
+ mock_dt.today.return_value = fake_now.date()
256
+
257
+ # Allow normal datetime construction
258
+ mock_dt.side_effect = lambda *args, **kwargs: datetime.datetime(*args, **kwargs)
259
+
260
+ yield mock_dt
261
+
262
+
263
+ @pytest.fixture
264
+ def time_travel():
265
+ """
266
+ Fixture for traveling through time in tests.
267
+
268
+ Returns:
269
+ Function to travel to specific time points.
270
+ """
271
+ original_time = time.time
272
+ current_offset = 0.0
273
+
274
+ def mock_time():
275
+ return original_time() + current_offset
276
+
277
+ def _travel_to(target: datetime.datetime):
278
+ """
279
+ Travel to a specific point in time.
280
+
281
+ Args:
282
+ target: The datetime to travel to
283
+ """
284
+ nonlocal current_offset
285
+ current_offset = target.timestamp() - original_time()
286
+
287
+ with patch('time.time', mock_time):
288
+ yield _travel_to
289
+
290
+
291
+ @pytest.fixture
292
+ def rate_limiter_mock():
293
+ """
294
+ Mock for testing rate-limited code.
295
+
296
+ Returns:
297
+ Mock rate limiter that can be controlled in tests.
298
+ """
299
+ class MockRateLimiter:
300
+ def __init__(self):
301
+ self.calls = []
302
+ self.should_limit = False
303
+ self.limit_after = None
304
+ self.call_count = 0
305
+
306
+ def check(self) -> bool:
307
+ """Check if rate limit is exceeded."""
308
+ self.call_count += 1
309
+ self.calls.append(time.time())
310
+
311
+ if self.limit_after and self.call_count > self.limit_after:
312
+ return False # Rate limited
313
+
314
+ return not self.should_limit
315
+
316
+ def reset(self):
317
+ """Reset the rate limiter."""
318
+ self.calls.clear()
319
+ self.call_count = 0
320
+ self.should_limit = False
321
+ self.limit_after = None
322
+
323
+ def set_limit(self, after_calls: int):
324
+ """Set to limit after N calls."""
325
+ self.limit_after = after_calls
326
+
327
+ return MockRateLimiter()
328
+
329
+
330
+ @pytest.fixture
331
+ def benchmark_timer():
332
+ """
333
+ Timer specifically for benchmarking code.
334
+
335
+ Returns:
336
+ Benchmark timer with statistics.
337
+ """
338
+ class BenchmarkTimer:
339
+ def __init__(self):
340
+ self.measurements = []
341
+
342
+ def measure(self, func: Callable, *args, **kwargs) -> tuple[Any, float]:
343
+ """
344
+ Measure execution time of a function.
345
+
346
+ Args:
347
+ func: Function to measure
348
+ *args: Function arguments
349
+ **kwargs: Function keyword arguments
350
+
351
+ Returns:
352
+ Tuple of (result, duration)
353
+ """
354
+ start = time.perf_counter()
355
+ result = func(*args, **kwargs)
356
+ duration = time.perf_counter() - start
357
+ self.measurements.append(duration)
358
+ return result, duration
359
+
360
+ @property
361
+ def min_time(self) -> float:
362
+ """Get minimum execution time."""
363
+ return min(self.measurements) if self.measurements else 0.0
364
+
365
+ @property
366
+ def max_time(self) -> float:
367
+ """Get maximum execution time."""
368
+ return max(self.measurements) if self.measurements else 0.0
369
+
370
+ @property
371
+ def avg_time(self) -> float:
372
+ """Get average execution time."""
373
+ return sum(self.measurements) / len(self.measurements) if self.measurements else 0.0
374
+
375
+ def assert_faster_than(self, seconds: float):
376
+ """Assert all measurements were faster than threshold."""
377
+ if not self.measurements:
378
+ raise AssertionError("No measurements taken")
379
+ if self.max_time > seconds:
380
+ raise AssertionError(f"Maximum time {self.max_time:.3f}s exceeded threshold {seconds:.3f}s")
381
+
382
+ return BenchmarkTimer()
383
+
384
+
385
+ # Utility functions that can be imported directly
386
+ def advance_time(mock_time: Mock, seconds: float):
387
+ """
388
+ Advance a mocked time by specified seconds.
389
+
390
+ Args:
391
+ mock_time: The mock time object
392
+ seconds: Number of seconds to advance
393
+ """
394
+ if hasattr(mock_time, 'return_value'):
395
+ mock_time.return_value += seconds
396
+
397
+
398
+ __all__ = [
399
+ "freeze_time",
400
+ "mock_sleep",
401
+ "mock_sleep_with_callback",
402
+ "time_machine",
403
+ "timer",
404
+ "mock_datetime",
405
+ "time_travel",
406
+ "rate_limiter_mock",
407
+ "benchmark_timer",
408
+ "advance_time",
409
+ ]
@@ -0,0 +1,30 @@
1
+ """
2
+ Transport and network testing fixtures for the provide-io ecosystem.
3
+
4
+ Standard fixtures for testing HTTP clients, WebSocket connections, and
5
+ network operations across any project that depends on provide.foundation.
6
+ """
7
+
8
+ from provide.foundation.testing.transport.fixtures import (
9
+ free_port,
10
+ mock_server,
11
+ httpx_mock_responses,
12
+ mock_websocket,
13
+ mock_dns_resolver,
14
+ tcp_client_server,
15
+ mock_ssl_context,
16
+ network_timeout,
17
+ mock_http_headers,
18
+ )
19
+
20
+ __all__ = [
21
+ "free_port",
22
+ "mock_server",
23
+ "httpx_mock_responses",
24
+ "mock_websocket",
25
+ "mock_dns_resolver",
26
+ "tcp_client_server",
27
+ "mock_ssl_context",
28
+ "network_timeout",
29
+ "mock_http_headers",
30
+ ]
@@ -0,0 +1,280 @@
1
+ """
2
+ Transport and Network Testing Fixtures.
3
+
4
+ Fixtures and helpers for testing network operations, including
5
+ mock servers, free port allocation, and HTTP client mocking.
6
+ """
7
+
8
+ import socket
9
+ import threading
10
+ from http.server import HTTPServer, BaseHTTPRequestHandler
11
+ from typing import Any
12
+ from collections.abc import Generator
13
+
14
+ import pytest
15
+
16
+
17
+ @pytest.fixture
18
+ def free_port() -> int:
19
+ """
20
+ Get a free port for testing.
21
+
22
+ Returns:
23
+ An available port number on localhost.
24
+ """
25
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
26
+ s.bind(('', 0))
27
+ s.listen(1)
28
+ port = s.getsockname()[1]
29
+ return port
30
+
31
+
32
+ @pytest.fixture
33
+ def mock_server(free_port) -> Generator[dict[str, Any], None, None]:
34
+ """
35
+ Create a simple mock HTTP server for testing.
36
+
37
+ Args:
38
+ free_port: Free port number from fixture.
39
+
40
+ Yields:
41
+ Dict with server info including url, port, and server instance.
42
+ """
43
+ responses = {}
44
+ requests_received = []
45
+
46
+ class MockHandler(BaseHTTPRequestHandler):
47
+ """Handler for mock HTTP server."""
48
+
49
+ def do_GET(self):
50
+ """Handle GET requests."""
51
+ requests_received.append({
52
+ "method": "GET",
53
+ "path": self.path,
54
+ "headers": dict(self.headers)
55
+ })
56
+
57
+ response = responses.get(self.path, {"status": 404, "body": b"Not Found"})
58
+ self.send_response(response["status"])
59
+ for header, value in response.get("headers", {}).items():
60
+ self.send_header(header, value)
61
+ self.end_headers()
62
+ self.wfile.write(response["body"])
63
+
64
+ def do_POST(self):
65
+ """Handle POST requests."""
66
+ content_length = int(self.headers.get('Content-Length', 0))
67
+ body = self.rfile.read(content_length) if content_length else b""
68
+
69
+ requests_received.append({
70
+ "method": "POST",
71
+ "path": self.path,
72
+ "headers": dict(self.headers),
73
+ "body": body
74
+ })
75
+
76
+ response = responses.get(self.path, {"status": 200, "body": b"OK"})
77
+ self.send_response(response["status"])
78
+ for header, value in response.get("headers", {}).items():
79
+ self.send_header(header, value)
80
+ self.end_headers()
81
+ self.wfile.write(response["body"])
82
+
83
+ def log_message(self, format, *args):
84
+ """Suppress log messages."""
85
+ pass
86
+
87
+ server = HTTPServer(('localhost', free_port), MockHandler)
88
+ server_thread = threading.Thread(target=server.serve_forever)
89
+ server_thread.daemon = True
90
+ server_thread.start()
91
+
92
+ yield {
93
+ "url": f"http://localhost:{free_port}",
94
+ "port": free_port,
95
+ "server": server,
96
+ "responses": responses,
97
+ "requests": requests_received,
98
+ }
99
+
100
+ server.shutdown()
101
+ server.server_close()
102
+
103
+
104
+ @pytest.fixture
105
+ def httpx_mock_responses():
106
+ """
107
+ Pre-configured responses for HTTPX mocking.
108
+
109
+ Returns:
110
+ Dict of common mock responses.
111
+ """
112
+ return {
113
+ "success": {
114
+ "status_code": 200,
115
+ "json": {"status": "ok", "data": {}},
116
+ },
117
+ "created": {
118
+ "status_code": 201,
119
+ "json": {"id": "123", "created": True},
120
+ },
121
+ "not_found": {
122
+ "status_code": 404,
123
+ "json": {"error": "Not found"},
124
+ },
125
+ "server_error": {
126
+ "status_code": 500,
127
+ "json": {"error": "Internal server error"},
128
+ },
129
+ "unauthorized": {
130
+ "status_code": 401,
131
+ "json": {"error": "Unauthorized"},
132
+ },
133
+ "rate_limited": {
134
+ "status_code": 429,
135
+ "headers": {"Retry-After": "60"},
136
+ "json": {"error": "Rate limit exceeded"},
137
+ },
138
+ }
139
+
140
+
141
+ @pytest.fixture
142
+ def mock_websocket():
143
+ """
144
+ Mock WebSocket connection for testing.
145
+
146
+ Returns:
147
+ Mock WebSocket with send, receive, close methods.
148
+ """
149
+ from unittest.mock import Mock, AsyncMock
150
+
151
+ ws = Mock()
152
+ ws.send = AsyncMock()
153
+ ws.receive = AsyncMock(return_value={"type": "text", "data": "message"})
154
+ ws.close = AsyncMock()
155
+ ws.accept = AsyncMock()
156
+ ws.ping = AsyncMock()
157
+ ws.pong = AsyncMock()
158
+
159
+ # State properties
160
+ ws.closed = False
161
+ ws.url = "ws://localhost:8000/ws"
162
+
163
+ return ws
164
+
165
+
166
+ @pytest.fixture
167
+ def mock_dns_resolver():
168
+ """
169
+ Mock DNS resolver for testing.
170
+
171
+ Returns:
172
+ Mock resolver with resolve method.
173
+ """
174
+ from unittest.mock import Mock
175
+
176
+ resolver = Mock()
177
+ resolver.resolve = Mock(return_value=["127.0.0.1", "::1"])
178
+ resolver.reverse = Mock(return_value="localhost")
179
+ resolver.clear_cache = Mock()
180
+
181
+ return resolver
182
+
183
+
184
+ @pytest.fixture
185
+ def tcp_client_server(free_port) -> Generator[dict[str, Any], None, None]:
186
+ """
187
+ Create a TCP client-server pair for testing.
188
+
189
+ Yields:
190
+ Dict with client socket, server socket, and port info.
191
+ """
192
+ server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
193
+ server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
194
+ server_socket.bind(('localhost', free_port))
195
+ server_socket.listen(1)
196
+
197
+ client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
198
+
199
+ # Run server accept in thread
200
+ connection = None
201
+
202
+ def accept_connection():
203
+ nonlocal connection
204
+ connection, _ = server_socket.accept()
205
+
206
+ accept_thread = threading.Thread(target=accept_connection)
207
+ accept_thread.daemon = True
208
+ accept_thread.start()
209
+
210
+ # Connect client
211
+ client_socket.connect(('localhost', free_port))
212
+ accept_thread.join(timeout=1)
213
+
214
+ yield {
215
+ "client": client_socket,
216
+ "server": connection,
217
+ "server_socket": server_socket,
218
+ "port": free_port,
219
+ }
220
+
221
+ # Cleanup
222
+ client_socket.close()
223
+ if connection:
224
+ connection.close()
225
+ server_socket.close()
226
+
227
+
228
+ @pytest.fixture
229
+ def mock_ssl_context():
230
+ """
231
+ Mock SSL context for testing secure connections.
232
+
233
+ Returns:
234
+ Mock SSL context with common methods.
235
+ """
236
+ from unittest.mock import Mock
237
+
238
+ context = Mock()
239
+ context.load_cert_chain = Mock()
240
+ context.load_verify_locations = Mock()
241
+ context.set_ciphers = Mock()
242
+ context.wrap_socket = Mock()
243
+ context.check_hostname = True
244
+ context.verify_mode = 2 # ssl.CERT_REQUIRED
245
+
246
+ return context
247
+
248
+
249
+ @pytest.fixture
250
+ def network_timeout():
251
+ """
252
+ Provide network timeout configuration for tests.
253
+
254
+ Returns:
255
+ Dict with timeout values for different operations.
256
+ """
257
+ return {
258
+ "connect": 5.0,
259
+ "read": 10.0,
260
+ "write": 10.0,
261
+ "total": 30.0,
262
+ }
263
+
264
+
265
+ @pytest.fixture
266
+ def mock_http_headers():
267
+ """
268
+ Common HTTP headers for testing.
269
+
270
+ Returns:
271
+ Dict of typical HTTP headers.
272
+ """
273
+ return {
274
+ "User-Agent": "TestClient/1.0",
275
+ "Accept": "application/json",
276
+ "Content-Type": "application/json",
277
+ "Authorization": "Bearer test_token",
278
+ "X-Request-ID": "test-request-123",
279
+ "X-Correlation-ID": "test-correlation-456",
280
+ }