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
@@ -1,10 +1,11 @@
1
1
  """ZIP archive implementation."""
2
2
 
3
- import zipfile
4
3
  from pathlib import Path
4
+ import zipfile
5
+
5
6
  from attrs import define, field
6
7
 
7
- from provide.foundation.archive.base import BaseArchive, ArchiveError
8
+ from provide.foundation.archive.base import ArchiveError, BaseArchive
8
9
  from provide.foundation.file import ensure_parent_dir
9
10
  from provide.foundation.logger import get_logger
10
11
 
@@ -15,45 +16,46 @@ logger = get_logger(__name__)
15
16
  class ZipArchive(BaseArchive):
16
17
  """
17
18
  ZIP archive implementation.
18
-
19
+
19
20
  Creates and extracts ZIP archives with optional compression and encryption.
20
21
  Supports adding files to existing archives.
21
22
  """
22
-
23
+
23
24
  compression_level: int = field(default=6) # Compression level 0-9 (0=store, 9=best)
24
25
  compression_type: int = field(default=zipfile.ZIP_DEFLATED)
25
26
  password: bytes | None = field(default=None)
26
-
27
+
27
28
  @compression_level.validator
28
- def _validate_level(self, attribute, value):
29
+ def _validate_level(self, attribute: object, value: int) -> None:
29
30
  if not 0 <= value <= 9:
30
31
  raise ValueError(f"Compression level must be 0-9, got {value}")
31
-
32
+
32
33
  def create(self, source: Path, output: Path) -> Path:
33
34
  """
34
35
  Create ZIP archive from source.
35
-
36
+
36
37
  Args:
37
38
  source: Source file or directory to archive
38
39
  output: Output ZIP file path
39
-
40
+
40
41
  Returns:
41
42
  Path to created archive
42
-
43
+
43
44
  Raises:
44
45
  ArchiveError: If archive creation fails
45
46
  """
46
47
  try:
47
48
  ensure_parent_dir(output)
48
-
49
+
49
50
  with zipfile.ZipFile(
50
- output, 'w',
51
+ output,
52
+ "w",
51
53
  compression=self.compression_type,
52
- compresslevel=self.compression_level
54
+ compresslevel=self.compression_level,
53
55
  ) as zf:
54
56
  if self.password:
55
57
  zf.setpassword(self.password)
56
-
58
+
57
59
  if source.is_dir():
58
60
  # Add all files in directory
59
61
  for item in sorted(source.rglob("*")):
@@ -63,141 +65,141 @@ class ZipArchive(BaseArchive):
63
65
  else:
64
66
  # Add single file
65
67
  zf.write(source, source.name)
66
-
68
+
67
69
  logger.debug(f"Created ZIP archive: {output}")
68
70
  return output
69
-
71
+
70
72
  except Exception as e:
71
73
  raise ArchiveError(f"Failed to create ZIP archive: {e}") from e
72
74
 
73
75
  def extract(self, archive: Path, output: Path) -> Path:
74
76
  """
75
77
  Extract ZIP archive to output directory.
76
-
78
+
77
79
  Args:
78
80
  archive: ZIP archive file path
79
81
  output: Output directory path
80
-
82
+
81
83
  Returns:
82
84
  Path to extraction directory
83
-
85
+
84
86
  Raises:
85
87
  ArchiveError: If extraction fails
86
88
  """
87
89
  try:
88
90
  output.mkdir(parents=True, exist_ok=True)
89
-
90
- with zipfile.ZipFile(archive, 'r') as zf:
91
+
92
+ with zipfile.ZipFile(archive, "r") as zf:
91
93
  if self.password:
92
94
  zf.setpassword(self.password)
93
-
95
+
94
96
  # Security check - prevent path traversal
95
97
  for member in zf.namelist():
96
98
  if member.startswith("/") or ".." in member:
97
99
  raise ArchiveError(f"Unsafe path in archive: {member}")
98
-
100
+
99
101
  # Extract all
100
102
  zf.extractall(output)
101
-
103
+
102
104
  logger.debug(f"Extracted ZIP archive to: {output}")
103
105
  return output
104
-
106
+
105
107
  except Exception as e:
106
108
  raise ArchiveError(f"Failed to extract ZIP archive: {e}") from e
107
109
 
108
110
  def validate(self, archive: Path) -> bool:
109
111
  """
110
112
  Validate ZIP archive integrity.
111
-
113
+
112
114
  Args:
113
115
  archive: ZIP archive file path
114
-
116
+
115
117
  Returns:
116
118
  True if archive is valid, False otherwise
117
119
  """
118
120
  try:
119
- with zipfile.ZipFile(archive, 'r') as zf:
121
+ with zipfile.ZipFile(archive, "r") as zf:
120
122
  # Test the archive
121
123
  result = zf.testzip()
122
124
  return result is None # None means no bad files
123
125
  except Exception:
124
126
  return False
125
-
127
+
126
128
  def list_contents(self, archive: Path) -> list[str]:
127
129
  """
128
130
  List contents of ZIP archive.
129
-
131
+
130
132
  Args:
131
133
  archive: ZIP archive file path
132
-
134
+
133
135
  Returns:
134
136
  List of file paths in archive
135
-
137
+
136
138
  Raises:
137
139
  ArchiveError: If listing fails
138
140
  """
139
141
  try:
140
- with zipfile.ZipFile(archive, 'r') as zf:
142
+ with zipfile.ZipFile(archive, "r") as zf:
141
143
  return sorted(zf.namelist())
142
144
  except Exception as e:
143
145
  raise ArchiveError(f"Failed to list ZIP contents: {e}") from e
144
-
146
+
145
147
  def add_file(self, archive: Path, file: Path, arcname: str | None = None) -> None:
146
148
  """
147
149
  Add file to existing ZIP archive.
148
-
150
+
149
151
  Args:
150
152
  archive: ZIP archive file path
151
153
  file: File to add
152
154
  arcname: Name in archive (defaults to file name)
153
-
155
+
154
156
  Raises:
155
157
  ArchiveError: If adding file fails
156
158
  """
157
159
  try:
158
- with zipfile.ZipFile(archive, 'a', compression=self.compression_type) as zf:
160
+ with zipfile.ZipFile(archive, "a", compression=self.compression_type) as zf:
159
161
  if self.password:
160
162
  zf.setpassword(self.password)
161
-
163
+
162
164
  zf.write(file, arcname or file.name)
163
-
165
+
164
166
  logger.debug(f"Added {file} to ZIP archive {archive}")
165
-
167
+
166
168
  except Exception as e:
167
169
  raise ArchiveError(f"Failed to add file to ZIP: {e}") from e
168
-
170
+
169
171
  def extract_file(self, archive: Path, member: str, output: Path) -> Path:
170
172
  """
171
173
  Extract single file from ZIP archive.
172
-
174
+
173
175
  Args:
174
176
  archive: ZIP archive file path
175
177
  member: Name of file in archive
176
178
  output: Output directory or file path
177
-
179
+
178
180
  Returns:
179
181
  Path to extracted file
180
-
182
+
181
183
  Raises:
182
184
  ArchiveError: If extraction fails
183
185
  """
184
186
  try:
185
- with zipfile.ZipFile(archive, 'r') as zf:
187
+ with zipfile.ZipFile(archive, "r") as zf:
186
188
  if self.password:
187
189
  zf.setpassword(self.password)
188
-
190
+
189
191
  # Security check
190
192
  if member.startswith("/") or ".." in member:
191
193
  raise ArchiveError(f"Unsafe path: {member}")
192
-
194
+
193
195
  if output.is_dir():
194
196
  zf.extract(member, output)
195
197
  return output / member
196
198
  else:
197
199
  ensure_parent_dir(output)
198
- with zf.open(member) as source, open(output, 'wb') as target:
200
+ with zf.open(member) as source, open(output, "wb") as target:
199
201
  target.write(source.read())
200
202
  return output
201
-
203
+
202
204
  except Exception as e:
203
- raise ArchiveError(f"Failed to extract file from ZIP: {e}") from e
205
+ raise ArchiveError(f"Failed to extract file from ZIP: {e}") from e
@@ -0,0 +1,20 @@
1
+ """
2
+ Async utilities for Foundation.
3
+
4
+ Provides consistent async/await patterns, task management,
5
+ and async context utilities for Foundation applications.
6
+ """
7
+
8
+ from provide.foundation.asynctools.core import (
9
+ provide_gather,
10
+ provide_run,
11
+ provide_sleep_async,
12
+ provide_wait_for,
13
+ )
14
+
15
+ __all__ = [
16
+ "provide_gather",
17
+ "provide_run",
18
+ "provide_sleep_async",
19
+ "provide_wait_for",
20
+ ]
@@ -0,0 +1,126 @@
1
+ """Core async utilities for Foundation."""
2
+
3
+ import asyncio
4
+ from collections.abc import Awaitable, Callable
5
+ from typing import Any
6
+
7
+ from provide.foundation.errors import ValidationError
8
+
9
+
10
+ async def provide_sleep_async(delay: float) -> None:
11
+ """
12
+ Async sleep with Foundation tracking and cancellation support.
13
+
14
+ Args:
15
+ delay: Number of seconds to sleep
16
+
17
+ Raises:
18
+ ValidationError: If delay is negative
19
+
20
+ Example:
21
+ >>> import asyncio
22
+ >>> async def main():
23
+ ... await provide_sleep_async(0.1)
24
+ >>> asyncio.run(main())
25
+ """
26
+ if delay < 0:
27
+ raise ValidationError("Sleep delay must be non-negative")
28
+ await asyncio.sleep(delay)
29
+
30
+
31
+ async def provide_gather(
32
+ *awaitables: Awaitable[Any], return_exceptions: bool = False
33
+ ) -> list[Any]:
34
+ """
35
+ Run awaitables concurrently with Foundation tracking.
36
+
37
+ Args:
38
+ *awaitables: Awaitable objects to run concurrently
39
+ return_exceptions: If True, exceptions are returned as results
40
+
41
+ Returns:
42
+ List of results in the same order as input awaitables
43
+
44
+ Raises:
45
+ ValidationError: If no awaitables provided
46
+
47
+ Example:
48
+ >>> import asyncio
49
+ >>> async def fetch_data(n):
50
+ ... await provide_sleep_async(0.1)
51
+ ... return n * 2
52
+ >>> async def main():
53
+ ... results = await provide_gather(
54
+ ... fetch_data(1), fetch_data(2), fetch_data(3)
55
+ ... )
56
+ ... return results
57
+ >>> asyncio.run(main())
58
+ [2, 4, 6]
59
+ """
60
+ if not awaitables:
61
+ raise ValidationError("At least one awaitable must be provided")
62
+
63
+ return await asyncio.gather(*awaitables, return_exceptions=return_exceptions)
64
+
65
+
66
+ async def provide_wait_for(awaitable: Awaitable[Any], timeout: float | None) -> Any:
67
+ """
68
+ Wait for an awaitable with optional timeout.
69
+
70
+ Args:
71
+ awaitable: The awaitable to wait for
72
+ timeout: Timeout in seconds (None for no timeout)
73
+
74
+ Returns:
75
+ Result of the awaitable
76
+
77
+ Raises:
78
+ ValidationError: If timeout is negative
79
+ asyncio.TimeoutError: If timeout is exceeded
80
+
81
+ Example:
82
+ >>> import asyncio
83
+ >>> async def slow_task():
84
+ ... await provide_sleep_async(0.2)
85
+ ... return "done"
86
+ >>> async def main():
87
+ ... try:
88
+ ... result = await provide_wait_for(slow_task(), timeout=0.1)
89
+ ... except asyncio.TimeoutError:
90
+ ... result = "timed out"
91
+ ... return result
92
+ >>> asyncio.run(main())
93
+ 'timed out'
94
+ """
95
+ if timeout is not None and timeout < 0:
96
+ raise ValidationError("Timeout must be non-negative")
97
+
98
+ return await asyncio.wait_for(awaitable, timeout=timeout)
99
+
100
+
101
+ def provide_run(main: Callable[[], Awaitable[Any]], *, debug: bool = False) -> Any:
102
+ """
103
+ Run async function with Foundation tracking.
104
+
105
+ Args:
106
+ main: Async function to run
107
+ debug: Whether to run in debug mode
108
+
109
+ Returns:
110
+ Result of the main function
111
+
112
+ Raises:
113
+ ValidationError: If main is not callable
114
+
115
+ Example:
116
+ >>> async def main():
117
+ ... await provide_sleep_async(0.1)
118
+ ... return "hello"
119
+ >>> result = provide_run(main)
120
+ >>> result
121
+ 'hello'
122
+ """
123
+ if not callable(main):
124
+ raise ValidationError("Main must be callable")
125
+
126
+ return asyncio.run(main(), debug=debug)
@@ -17,10 +17,8 @@ from provide.foundation.cli.decorators import (
17
17
  )
18
18
  from provide.foundation.cli.testing import (
19
19
  CliTestCase,
20
- MockContext,
21
20
  create_test_cli,
22
21
  isolated_cli_runner,
23
- mock_logger,
24
22
  temp_config_file,
25
23
  )
26
24
  from provide.foundation.cli.utils import (
@@ -35,6 +33,8 @@ from provide.foundation.cli.utils import (
35
33
  echo_warning,
36
34
  setup_cli_logging,
37
35
  )
36
+ from provide.foundation.testing.cli import MockContext
37
+ from provide.foundation.testing.logger import mock_logger
38
38
 
39
39
  __all__ = [
40
40
  "CliTestCase",
@@ -8,10 +8,12 @@ except ImportError:
8
8
  click = None
9
9
  _HAS_CLICK = False
10
10
 
11
- from provide.foundation.utils.deps import check_optional_deps
11
+ from provide.foundation.console.output import pout
12
+ from provide.foundation.process import exit_error, exit_success
13
+ from provide.foundation.utils.deps import check_optional_deps, has_dependency
12
14
 
13
15
 
14
- def _require_click():
16
+ def _require_click() -> None:
15
17
  """Ensure click is available for CLI commands."""
16
18
  if not _HAS_CLICK:
17
19
  raise ImportError(
@@ -23,21 +25,25 @@ def _require_click():
23
25
  def _deps_command_impl(quiet: bool, check: str | None) -> None:
24
26
  """Implementation of deps command logic."""
25
27
  if check:
26
- from provide.foundation.utils.deps import has_dependency
27
-
28
28
  available = has_dependency(check)
29
29
  if not quiet:
30
30
  status = "✅" if available else "❌"
31
- print(f"{status} {check}: {'Available' if available else 'Missing'}")
31
+ pout(f"{status} {check}: {'Available' if available else 'Missing'}")
32
32
  if not available:
33
- print(f"Install with: pip install 'provide-foundation[{check}]'")
34
- exit(0 if available else 1)
33
+ pout(f"Install with: pip install 'provide-foundation[{check}]'")
34
+ if available:
35
+ exit_success()
36
+ else:
37
+ exit_error("Dependency check failed")
35
38
  else:
36
39
  # Check all dependencies
37
40
  deps = check_optional_deps(quiet=quiet, return_status=True)
38
41
  available_count = sum(1 for dep in deps if dep.available)
39
42
  total_count = len(deps)
40
- exit(0 if available_count == total_count else 1)
43
+ if available_count == total_count:
44
+ exit_success()
45
+ else:
46
+ exit_error(f"Missing {total_count - available_count} dependencies")
41
47
 
42
48
 
43
49
  if _HAS_CLICK:
@@ -62,7 +68,7 @@ if _HAS_CLICK:
62
68
  _deps_command_impl(quiet, check)
63
69
  else:
64
70
  # Stub for when click is not available
65
- def deps_command(*args, **kwargs):
71
+ def deps_command(*args: object, **kwargs: object) -> None:
66
72
  """Deps command stub when click is not available."""
67
73
  _require_click()
68
74
 
@@ -21,14 +21,14 @@ if _HAS_CLICK:
21
21
 
22
22
  @click.group("logs", help="Send and query logs with OpenTelemetry integration")
23
23
  @click.pass_context
24
- def logs_group(ctx):
24
+ def logs_group(ctx: click.Context) -> None:
25
25
  """Logs management commands with OTEL correlation."""
26
26
  # Store shared context
27
27
  ctx.ensure_object(dict)
28
28
 
29
29
  # Try to get OpenObserve client if available
30
30
  try:
31
- from provide.foundation.observability.openobserve import OpenObserveClient
31
+ from provide.foundation.integrations.openobserve import OpenObserveClient
32
32
 
33
33
  ctx.obj["client"] = OpenObserveClient.from_config()
34
34
  except Exception as e:
@@ -53,7 +53,7 @@ if _HAS_CLICK:
53
53
 
54
54
  else:
55
55
  # Stub when click is not available
56
- def logs_group(*args, **kwargs):
56
+ def logs_group(*args: object, **kwargs: object) -> None:
57
57
  """Logs command stub when click is not available."""
58
58
  raise ImportError(
59
59
  "CLI commands require optional dependencies. "
@@ -205,7 +205,7 @@ def generate_logs_command(
205
205
  error_rate: float,
206
206
  enable_rate_limit: bool,
207
207
  rate_limit: float,
208
- ):
208
+ ) -> None:
209
209
  """Generate logs to test OpenObserve integration with Foundation's rate limiting."""
210
210
 
211
211
  click.echo("🚀 Starting log generation...")
@@ -351,7 +351,7 @@ def generate_logs_command(
351
351
 
352
352
  if not _HAS_CLICK:
353
353
 
354
- def generate_logs_command(*args, **kwargs):
354
+ def generate_logs_command(*args: object, **kwargs: object) -> None:
355
355
  raise ImportError(
356
356
  "Click is required for CLI commands. Install with: pip install click"
357
357
  )
@@ -66,8 +66,8 @@ if _HAS_CLICK:
66
66
  )
67
67
  @click.pass_context
68
68
  def query_command(
69
- ctx, sql, current_trace, trace_id, level, service, last, stream, size, format
70
- ):
69
+ ctx: click.Context, sql: str | None, current_trace: bool, trace_id: str | None, level: str | None, service: str | None, last: str, stream: str, size: int, format: str
70
+ ) -> int | None:
71
71
  """Query logs from OpenObserve.
72
72
 
73
73
  Examples:
@@ -89,7 +89,7 @@ if _HAS_CLICK:
89
89
  # Custom SQL query
90
90
  foundation logs query --sql "SELECT * FROM default WHERE duration_ms > 1000"
91
91
  """
92
- from provide.foundation.observability.openobserve import (
92
+ from provide.foundation.integrations.openobserve import (
93
93
  format_output,
94
94
  search_logs,
95
95
  )
@@ -166,7 +166,7 @@ if _HAS_CLICK:
166
166
 
167
167
  else:
168
168
 
169
- def query_command(*args, **kwargs):
169
+ def query_command(*args: object, **kwargs: object) -> None:
170
170
  """Query command stub when click is not available."""
171
171
  raise ImportError(
172
172
  "CLI commands require optional dependencies. "
@@ -66,8 +66,8 @@ if _HAS_CLICK:
66
66
  )
67
67
  @click.pass_context
68
68
  def send_command(
69
- ctx, message, level, service, json_attrs, attr, trace_id, span_id, use_otlp
70
- ):
69
+ ctx: click.Context, message: str | None, level: str, service: str | None, json_attrs: str | None, attr: tuple[str, ...], trace_id: str | None, span_id: str | None, use_otlp: bool
70
+ ) -> int | None:
71
71
  """Send a log entry to OpenObserve.
72
72
 
73
73
  Examples:
@@ -83,7 +83,7 @@ if _HAS_CLICK:
83
83
  # Send with JSON attributes
84
84
  foundation logs send -m "Error occurred" -j '{"error_code": 500, "path": "/api/users"}'
85
85
  """
86
- from provide.foundation.observability.openobserve.otlp import send_log
86
+ from provide.foundation.integrations.openobserve.otlp import send_log
87
87
 
88
88
  # Get message from stdin if not provided
89
89
  if not message:
@@ -50,7 +50,7 @@ if _HAS_CLICK:
50
50
  help="Output format",
51
51
  )
52
52
  @click.pass_context
53
- def tail_command(ctx, stream, filter_sql, lines, follow, format):
53
+ def tail_command(ctx: click.Context, stream: str, filter_sql: str | None, lines: int, follow: bool, format: str) -> int | None:
54
54
  """Tail logs in real-time (like 'tail -f').
55
55
 
56
56
  Examples:
@@ -69,7 +69,7 @@ if _HAS_CLICK:
69
69
  # Tail with JSON output
70
70
  foundation logs tail --format json
71
71
  """
72
- from provide.foundation.observability.openobserve import (
72
+ from provide.foundation.integrations.openobserve import (
73
73
  format_output,
74
74
  tail_logs,
75
75
  )
@@ -104,7 +104,7 @@ if _HAS_CLICK:
104
104
 
105
105
  else:
106
106
 
107
- def tail_command(*args, **kwargs):
107
+ def tail_command(*args: object, **kwargs: object) -> None:
108
108
  """Tail command stub when click is not available."""
109
109
  raise ImportError(
110
110
  "CLI commands require optional dependencies. "
@@ -3,7 +3,6 @@
3
3
  from collections.abc import Callable
4
4
  import functools
5
5
  from pathlib import Path
6
- import sys
7
6
  from typing import Any, TypeVar
8
7
 
9
8
  try:
@@ -11,7 +10,8 @@ try:
11
10
  except ImportError:
12
11
  click = None
13
12
 
14
- from provide.foundation.context import Context
13
+ from provide.foundation.context import CLIContext
14
+ from provide.foundation.process import exit_error, exit_interrupted
15
15
 
16
16
  F = TypeVar("F", bound=Callable[..., Any])
17
17
 
@@ -162,7 +162,7 @@ def error_handler(f: F) -> F:
162
162
  except KeyboardInterrupt:
163
163
  if not json_output:
164
164
  click.secho("\nInterrupted by user", fg="yellow", err=True)
165
- sys.exit(130) # Standard exit code for SIGINT
165
+ exit_interrupted()
166
166
  except Exception as e:
167
167
  if debug:
168
168
  # In debug mode, show full traceback
@@ -179,16 +179,16 @@ def error_handler(f: F) -> F:
179
179
  else:
180
180
  click.secho(f"Error: {e}", fg="red", err=True)
181
181
 
182
- sys.exit(1)
182
+ exit_error(f"Command failed: {str(e)}")
183
183
 
184
184
  return wrapper
185
185
 
186
186
 
187
187
  def pass_context(f: F) -> F:
188
188
  """
189
- Decorator to pass the foundation Context to a command.
189
+ Decorator to pass the foundation CLIContext to a command.
190
190
 
191
- Creates or retrieves a Context from Click's context object
191
+ Creates or retrieves a CLIContext from Click's context object
192
192
  and passes it as the first argument to the decorated function.
193
193
  """
194
194
 
@@ -197,15 +197,15 @@ def pass_context(f: F) -> F:
197
197
  def wrapper(ctx: click.Context, *args, **kwargs):
198
198
  # Get or create foundation context
199
199
  if not hasattr(ctx, "obj") or ctx.obj is None:
200
- ctx.obj = Context()
201
- elif not isinstance(ctx.obj, Context):
200
+ ctx.obj = CLIContext()
201
+ elif not isinstance(ctx.obj, CLIContext):
202
202
  # If obj exists but isn't a Context, wrap it
203
203
  if isinstance(ctx.obj, dict):
204
- ctx.obj = Context.from_dict(ctx.obj)
204
+ ctx.obj = CLIContext.from_dict(ctx.obj)
205
205
  else:
206
- # Store existing obj and create new Context
206
+ # Store existing obj and create new CLIContext
207
207
  old_obj = ctx.obj
208
- ctx.obj = Context()
208
+ ctx.obj = CLIContext()
209
209
  ctx.obj._cli_data = old_obj
210
210
 
211
211
  # Update context from command options
@@ -46,7 +46,7 @@ if _HAS_CLICK:
46
46
 
47
47
  # Register OpenObserve commands if available
48
48
  try:
49
- from provide.foundation.observability.openobserve.commands import (
49
+ from provide.foundation.integrations.openobserve.commands import (
50
50
  openobserve_group,
51
51
  )
52
52