provide-foundation 0.0.0.dev0__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 (149) hide show
  1. provide/__init__.py +15 -0
  2. provide/foundation/__init__.py +155 -0
  3. provide/foundation/_version.py +58 -0
  4. provide/foundation/cli/__init__.py +67 -0
  5. provide/foundation/cli/commands/__init__.py +3 -0
  6. provide/foundation/cli/commands/deps.py +71 -0
  7. provide/foundation/cli/commands/logs/__init__.py +63 -0
  8. provide/foundation/cli/commands/logs/generate.py +357 -0
  9. provide/foundation/cli/commands/logs/generate_old.py +569 -0
  10. provide/foundation/cli/commands/logs/query.py +174 -0
  11. provide/foundation/cli/commands/logs/send.py +166 -0
  12. provide/foundation/cli/commands/logs/tail.py +112 -0
  13. provide/foundation/cli/decorators.py +262 -0
  14. provide/foundation/cli/main.py +65 -0
  15. provide/foundation/cli/testing.py +220 -0
  16. provide/foundation/cli/utils.py +210 -0
  17. provide/foundation/config/__init__.py +106 -0
  18. provide/foundation/config/base.py +295 -0
  19. provide/foundation/config/env.py +369 -0
  20. provide/foundation/config/loader.py +311 -0
  21. provide/foundation/config/manager.py +387 -0
  22. provide/foundation/config/schema.py +284 -0
  23. provide/foundation/config/sync.py +281 -0
  24. provide/foundation/config/types.py +78 -0
  25. provide/foundation/config/validators.py +80 -0
  26. provide/foundation/console/__init__.py +29 -0
  27. provide/foundation/console/input.py +364 -0
  28. provide/foundation/console/output.py +178 -0
  29. provide/foundation/context/__init__.py +12 -0
  30. provide/foundation/context/core.py +356 -0
  31. provide/foundation/core.py +20 -0
  32. provide/foundation/crypto/__init__.py +182 -0
  33. provide/foundation/crypto/algorithms.py +111 -0
  34. provide/foundation/crypto/certificates.py +896 -0
  35. provide/foundation/crypto/checksums.py +301 -0
  36. provide/foundation/crypto/constants.py +57 -0
  37. provide/foundation/crypto/hashing.py +265 -0
  38. provide/foundation/crypto/keys.py +188 -0
  39. provide/foundation/crypto/signatures.py +144 -0
  40. provide/foundation/crypto/utils.py +164 -0
  41. provide/foundation/errors/__init__.py +96 -0
  42. provide/foundation/errors/auth.py +73 -0
  43. provide/foundation/errors/base.py +81 -0
  44. provide/foundation/errors/config.py +103 -0
  45. provide/foundation/errors/context.py +299 -0
  46. provide/foundation/errors/decorators.py +484 -0
  47. provide/foundation/errors/handlers.py +360 -0
  48. provide/foundation/errors/integration.py +105 -0
  49. provide/foundation/errors/platform.py +37 -0
  50. provide/foundation/errors/process.py +140 -0
  51. provide/foundation/errors/resources.py +133 -0
  52. provide/foundation/errors/runtime.py +160 -0
  53. provide/foundation/errors/safe_decorators.py +133 -0
  54. provide/foundation/errors/types.py +276 -0
  55. provide/foundation/file/__init__.py +79 -0
  56. provide/foundation/file/atomic.py +157 -0
  57. provide/foundation/file/directory.py +134 -0
  58. provide/foundation/file/formats.py +236 -0
  59. provide/foundation/file/lock.py +175 -0
  60. provide/foundation/file/safe.py +179 -0
  61. provide/foundation/file/utils.py +170 -0
  62. provide/foundation/hub/__init__.py +88 -0
  63. provide/foundation/hub/click_builder.py +310 -0
  64. provide/foundation/hub/commands.py +42 -0
  65. provide/foundation/hub/components.py +640 -0
  66. provide/foundation/hub/decorators.py +244 -0
  67. provide/foundation/hub/info.py +32 -0
  68. provide/foundation/hub/manager.py +446 -0
  69. provide/foundation/hub/registry.py +279 -0
  70. provide/foundation/hub/type_mapping.py +54 -0
  71. provide/foundation/hub/types.py +28 -0
  72. provide/foundation/logger/__init__.py +41 -0
  73. provide/foundation/logger/base.py +22 -0
  74. provide/foundation/logger/config/__init__.py +16 -0
  75. provide/foundation/logger/config/base.py +40 -0
  76. provide/foundation/logger/config/logging.py +394 -0
  77. provide/foundation/logger/config/telemetry.py +188 -0
  78. provide/foundation/logger/core.py +239 -0
  79. provide/foundation/logger/custom_processors.py +172 -0
  80. provide/foundation/logger/emoji/__init__.py +44 -0
  81. provide/foundation/logger/emoji/matrix.py +209 -0
  82. provide/foundation/logger/emoji/sets.py +458 -0
  83. provide/foundation/logger/emoji/types.py +56 -0
  84. provide/foundation/logger/factories.py +56 -0
  85. provide/foundation/logger/processors/__init__.py +13 -0
  86. provide/foundation/logger/processors/main.py +254 -0
  87. provide/foundation/logger/processors/trace.py +113 -0
  88. provide/foundation/logger/ratelimit/__init__.py +31 -0
  89. provide/foundation/logger/ratelimit/limiters.py +294 -0
  90. provide/foundation/logger/ratelimit/processor.py +203 -0
  91. provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
  92. provide/foundation/logger/setup/__init__.py +29 -0
  93. provide/foundation/logger/setup/coordinator.py +138 -0
  94. provide/foundation/logger/setup/emoji_resolver.py +64 -0
  95. provide/foundation/logger/setup/processors.py +85 -0
  96. provide/foundation/logger/setup/testing.py +39 -0
  97. provide/foundation/logger/trace.py +38 -0
  98. provide/foundation/metrics/__init__.py +119 -0
  99. provide/foundation/metrics/otel.py +122 -0
  100. provide/foundation/metrics/simple.py +165 -0
  101. provide/foundation/observability/__init__.py +53 -0
  102. provide/foundation/observability/openobserve/__init__.py +79 -0
  103. provide/foundation/observability/openobserve/auth.py +72 -0
  104. provide/foundation/observability/openobserve/client.py +307 -0
  105. provide/foundation/observability/openobserve/commands.py +357 -0
  106. provide/foundation/observability/openobserve/exceptions.py +41 -0
  107. provide/foundation/observability/openobserve/formatters.py +298 -0
  108. provide/foundation/observability/openobserve/models.py +134 -0
  109. provide/foundation/observability/openobserve/otlp.py +320 -0
  110. provide/foundation/observability/openobserve/search.py +222 -0
  111. provide/foundation/observability/openobserve/streaming.py +235 -0
  112. provide/foundation/platform/__init__.py +44 -0
  113. provide/foundation/platform/detection.py +193 -0
  114. provide/foundation/platform/info.py +157 -0
  115. provide/foundation/process/__init__.py +39 -0
  116. provide/foundation/process/async_runner.py +373 -0
  117. provide/foundation/process/lifecycle.py +406 -0
  118. provide/foundation/process/runner.py +390 -0
  119. provide/foundation/setup/__init__.py +101 -0
  120. provide/foundation/streams/__init__.py +44 -0
  121. provide/foundation/streams/console.py +57 -0
  122. provide/foundation/streams/core.py +65 -0
  123. provide/foundation/streams/file.py +104 -0
  124. provide/foundation/testing/__init__.py +166 -0
  125. provide/foundation/testing/cli.py +227 -0
  126. provide/foundation/testing/crypto.py +163 -0
  127. provide/foundation/testing/fixtures.py +49 -0
  128. provide/foundation/testing/hub.py +23 -0
  129. provide/foundation/testing/logger.py +106 -0
  130. provide/foundation/testing/streams.py +54 -0
  131. provide/foundation/tracer/__init__.py +49 -0
  132. provide/foundation/tracer/context.py +115 -0
  133. provide/foundation/tracer/otel.py +135 -0
  134. provide/foundation/tracer/spans.py +174 -0
  135. provide/foundation/types.py +32 -0
  136. provide/foundation/utils/__init__.py +97 -0
  137. provide/foundation/utils/deps.py +195 -0
  138. provide/foundation/utils/env.py +491 -0
  139. provide/foundation/utils/formatting.py +483 -0
  140. provide/foundation/utils/parsing.py +235 -0
  141. provide/foundation/utils/rate_limiting.py +112 -0
  142. provide/foundation/utils/streams.py +67 -0
  143. provide/foundation/utils/timing.py +93 -0
  144. provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
  145. provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
  146. provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
  147. provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  148. provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
  149. provide_foundation-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,235 @@
1
+ """
2
+ Streaming search operations for OpenObserve.
3
+ """
4
+
5
+ from collections.abc import Generator
6
+ import json
7
+ import time
8
+ from typing import Any
9
+
10
+ import requests
11
+
12
+ from provide.foundation.logger import get_logger
13
+ from provide.foundation.observability.openobserve.auth import get_auth_headers
14
+ from provide.foundation.observability.openobserve.client import OpenObserveClient
15
+ from provide.foundation.observability.openobserve.exceptions import (
16
+ OpenObserveStreamingError,
17
+ )
18
+ from provide.foundation.observability.openobserve.models import parse_relative_time
19
+
20
+ log = get_logger(__name__)
21
+
22
+
23
+ def stream_logs(
24
+ sql: str,
25
+ start_time: str | int | None = None,
26
+ poll_interval: int = 5,
27
+ client: OpenObserveClient | None = None,
28
+ ) -> Generator[dict[str, Any], None, None]:
29
+ """Stream logs from OpenObserve with polling.
30
+
31
+ Continuously polls for new logs and yields them as they arrive.
32
+
33
+ Args:
34
+ sql: SQL query to execute
35
+ start_time: Initial start time
36
+ poll_interval: Seconds between polls
37
+ client: OpenObserve client
38
+
39
+ Yields:
40
+ Log entries as they arrive
41
+ """
42
+ if client is None:
43
+ client = OpenObserveClient.from_config()
44
+
45
+ # Track the last seen timestamp to avoid duplicates
46
+ last_timestamp = (
47
+ parse_relative_time(start_time) if start_time else parse_relative_time("-1m")
48
+ )
49
+ seen_ids = set()
50
+
51
+ log.info(f"Starting log stream with query: {sql}")
52
+
53
+ while True:
54
+ try:
55
+ # Search for new logs since last timestamp
56
+ response = client.search(
57
+ sql=sql,
58
+ start_time=last_timestamp,
59
+ end_time="now",
60
+ size=1000,
61
+ )
62
+
63
+ # Process new logs
64
+ new_count = 0
65
+ for hit in response.hits:
66
+ # Create a unique ID for deduplication
67
+ # Use combination of timestamp and a hash of content
68
+ timestamp = hit.get("_timestamp", 0)
69
+ log_id = f"{timestamp}:{hash(json.dumps(hit, sort_keys=True))}"
70
+
71
+ if log_id not in seen_ids:
72
+ seen_ids.add(log_id)
73
+ new_count += 1
74
+ yield hit
75
+
76
+ # Update last timestamp
77
+ if timestamp > last_timestamp:
78
+ last_timestamp = (
79
+ timestamp + 1
80
+ ) # Add 1 microsecond to avoid duplicates
81
+
82
+ if new_count > 0:
83
+ log.debug(f"Streamed {new_count} new log entries")
84
+
85
+ # Clean up old seen IDs to prevent memory growth
86
+ # Keep only IDs from the last minute
87
+ cutoff_time = parse_relative_time("-1m")
88
+ seen_ids = {lid for lid in seen_ids if int(lid.split(":")[0]) > cutoff_time}
89
+
90
+ # Wait before next poll
91
+ time.sleep(poll_interval)
92
+
93
+ except KeyboardInterrupt:
94
+ log.info("Stream interrupted by user")
95
+ break
96
+ except Exception as e:
97
+ log.error(f"Error during streaming: {e}")
98
+ raise OpenObserveStreamingError(f"Streaming failed: {e}")
99
+
100
+
101
+ def stream_search_http2(
102
+ sql: str,
103
+ start_time: str | int | None = None,
104
+ end_time: str | int | None = None,
105
+ client: OpenObserveClient | None = None,
106
+ ) -> Generator[dict[str, Any], None, None]:
107
+ """Stream search results using HTTP/2 streaming endpoint.
108
+
109
+ This uses the native HTTP/2 streaming capability of OpenObserve
110
+ for real-time log streaming.
111
+
112
+ Args:
113
+ sql: SQL query to execute
114
+ start_time: Start time
115
+ end_time: End time
116
+ client: OpenObserve client
117
+
118
+ Yields:
119
+ Log entries as they stream
120
+ """
121
+ if client is None:
122
+ client = OpenObserveClient.from_config()
123
+
124
+ # Parse times
125
+ start_ts = (
126
+ parse_relative_time(start_time) if start_time else parse_relative_time("-1h")
127
+ )
128
+ end_ts = parse_relative_time(end_time) if end_time else parse_relative_time("now")
129
+
130
+ # Prepare request
131
+ url = f"{client.url}/api/{client.organization}/_search_stream"
132
+ params = {
133
+ "is_ui_histogram": "false",
134
+ "is_multi_stream_search": "false",
135
+ }
136
+ data = {
137
+ "sql": sql,
138
+ "start_time": start_ts,
139
+ "end_time": end_ts,
140
+ }
141
+
142
+ headers = get_auth_headers(client.username, client.password)
143
+
144
+ log.info(f"Starting HTTP/2 stream with query: {sql}")
145
+
146
+ try:
147
+ # Make streaming request
148
+ with requests.post(
149
+ url,
150
+ params=params,
151
+ json=data,
152
+ headers=headers,
153
+ stream=True,
154
+ timeout=client.timeout,
155
+ ) as response:
156
+ response.raise_for_status()
157
+
158
+ # Process streaming response
159
+ for line in response.iter_lines():
160
+ if line:
161
+ try:
162
+ # Decode and parse JSON line
163
+ data = json.loads(line.decode("utf-8"))
164
+
165
+ # Handle different response formats
166
+ if isinstance(data, dict):
167
+ if "hits" in data:
168
+ # Batch of results
169
+ for hit in data["hits"]:
170
+ yield hit
171
+ else:
172
+ # Single result
173
+ yield data
174
+ except json.JSONDecodeError as e:
175
+ log.warning(f"Failed to parse stream line: {e}")
176
+ continue
177
+
178
+ except requests.exceptions.RequestException as e:
179
+ raise OpenObserveStreamingError(f"HTTP/2 streaming failed: {e}")
180
+
181
+
182
+ def tail_logs(
183
+ stream: str = "default",
184
+ filter_sql: str | None = None,
185
+ follow: bool = True,
186
+ lines: int = 10,
187
+ client: OpenObserveClient | None = None,
188
+ ) -> Generator[dict[str, Any], None, None]:
189
+ """Tail logs similar to 'tail -f' command.
190
+
191
+ Args:
192
+ stream: Stream name to tail
193
+ filter_sql: Optional SQL WHERE clause for filtering
194
+ follow: If True, continue streaming new logs
195
+ lines: Number of initial lines to show
196
+ client: OpenObserve client
197
+
198
+ Yields:
199
+ Log entries
200
+ """
201
+ # Build SQL query
202
+ where_clause = f"WHERE {filter_sql}" if filter_sql else ""
203
+ sql = (
204
+ f"SELECT * FROM {stream} {where_clause} ORDER BY _timestamp DESC LIMIT {lines}"
205
+ )
206
+
207
+ if client is None:
208
+ client = OpenObserveClient.from_config()
209
+
210
+ # Get initial logs
211
+ log.info(f"Fetching last {lines} logs from {stream}")
212
+ response = client.search(sql=sql, start_time="-1h")
213
+
214
+ # Yield initial logs in reverse order (oldest first)
215
+ for hit in reversed(response.hits):
216
+ yield hit
217
+
218
+ # If follow mode, continue streaming
219
+ if follow:
220
+ # Get the latest timestamp from initial results
221
+ if response.hits:
222
+ last_timestamp = max(hit.get("_timestamp", 0) for hit in response.hits)
223
+ else:
224
+ last_timestamp = parse_relative_time("-1s")
225
+
226
+ # Build streaming query
227
+ stream_sql = f"SELECT * FROM {stream} {where_clause} ORDER BY _timestamp ASC"
228
+
229
+ # Stream new logs
230
+ for log_entry in stream_logs(
231
+ sql=stream_sql,
232
+ start_time=last_timestamp + 1,
233
+ client=client,
234
+ ):
235
+ yield log_entry
@@ -0,0 +1,44 @@
1
+ """Platform detection and information utilities.
2
+
3
+ Provides cross-platform detection and system information gathering.
4
+ """
5
+
6
+ from provide.foundation.platform.detection import (
7
+ PlatformError,
8
+ get_arch_name,
9
+ get_cpu_type,
10
+ get_os_name,
11
+ get_os_version,
12
+ get_platform_string,
13
+ normalize_platform_components,
14
+ )
15
+ from provide.foundation.platform.info import (
16
+ SystemInfo,
17
+ get_system_info,
18
+ is_64bit,
19
+ is_arm,
20
+ is_linux,
21
+ is_macos,
22
+ is_windows,
23
+ )
24
+
25
+ __all__ = [
26
+ # Classes
27
+ "PlatformError",
28
+ "SystemInfo",
29
+ # Detection functions
30
+ "get_arch_name",
31
+ "get_cpu_type",
32
+ "get_os_name",
33
+ "get_os_version",
34
+ "get_platform_string",
35
+ "get_system_info",
36
+ # Platform checks
37
+ "is_64bit",
38
+ "is_arm",
39
+ "is_linux",
40
+ "is_macos",
41
+ "is_windows",
42
+ # Utilities
43
+ "normalize_platform_components",
44
+ ]
@@ -0,0 +1,193 @@
1
+ """Core platform detection functions."""
2
+
3
+ import platform
4
+ import re
5
+
6
+ from provide.foundation.errors.platform import PlatformError
7
+ from provide.foundation.logger import get_logger
8
+
9
+ plog = get_logger(__name__)
10
+
11
+
12
+ def get_os_name() -> str:
13
+ """
14
+ Get normalized OS name.
15
+
16
+ Returns:
17
+ Normalized OS name (darwin, linux, windows)
18
+ """
19
+ try:
20
+ os_name = platform.system().lower()
21
+ if os_name in ("darwin", "macos"):
22
+ return "darwin"
23
+ return os_name
24
+ except Exception as e:
25
+ plog.error("Failed to detect OS", error=str(e))
26
+ raise PlatformError(
27
+ "Failed to detect operating system",
28
+ code="PLATFORM_OS_DETECTION_FAILED",
29
+ error=str(e),
30
+ ) from e
31
+
32
+
33
+ def get_arch_name() -> str:
34
+ """
35
+ Get normalized architecture name.
36
+
37
+ Returns:
38
+ Normalized architecture (amd64, arm64, x86, i386)
39
+ """
40
+ try:
41
+ arch = platform.machine().lower()
42
+ # Normalize common architectures
43
+ if arch in ["x86_64", "amd64"]:
44
+ return "amd64"
45
+ elif arch in ["aarch64", "arm64"]:
46
+ return "arm64"
47
+ elif arch in ["i686", "i586", "i486"]:
48
+ return "x86"
49
+ return arch
50
+ except Exception as e:
51
+ plog.error("Failed to detect architecture", error=str(e))
52
+ raise PlatformError(
53
+ "Failed to detect architecture",
54
+ code="PLATFORM_ARCH_DETECTION_FAILED",
55
+ error=str(e),
56
+ ) from e
57
+
58
+
59
+ def get_platform_string() -> str:
60
+ """
61
+ Get normalized platform string in format 'os_arch'.
62
+
63
+ Returns:
64
+ Platform string like 'darwin_arm64' or 'linux_amd64'
65
+ """
66
+ os_name = get_os_name()
67
+ arch = get_arch_name()
68
+ platform_str = f"{os_name}_{arch}"
69
+ plog.debug("Detected platform", platform=platform_str, os=os_name, arch=arch)
70
+ return platform_str
71
+
72
+
73
+ def get_os_version() -> str | None:
74
+ """
75
+ Get OS version information.
76
+
77
+ Returns:
78
+ OS version string or None if unavailable
79
+ """
80
+ try:
81
+ system = platform.system()
82
+
83
+ if system == "Darwin":
84
+ # macOS version
85
+ mac_ver = platform.mac_ver()
86
+ if mac_ver[0]:
87
+ return mac_ver[0]
88
+ elif system == "Linux":
89
+ # Linux kernel version
90
+ release = platform.release()
91
+ if release:
92
+ # Extract major.minor version
93
+ parts = release.split(".")
94
+ if len(parts) >= 2:
95
+ return f"{parts[0]}.{parts[1]}"
96
+ return release
97
+ elif system == "Windows":
98
+ # Windows version
99
+ version = platform.version()
100
+ if version:
101
+ return version
102
+
103
+ # Fallback to platform.release()
104
+ release = platform.release()
105
+ if release:
106
+ return release
107
+ except Exception as e:
108
+ plog.warning("Failed to detect OS version", error=str(e))
109
+
110
+ return None
111
+
112
+
113
+ def get_cpu_type() -> str | None:
114
+ """
115
+ Get CPU type/family information.
116
+
117
+ Returns:
118
+ CPU type string or None if unavailable
119
+ """
120
+ try:
121
+ processor = platform.processor()
122
+ if processor:
123
+ # Clean up common processor strings
124
+ if "Intel" in processor:
125
+ # Extract Intel CPU model
126
+ if "Core" in processor:
127
+ match = re.search(r"Core\(TM\)\s+(\w+)", processor)
128
+ if match:
129
+ return f"Intel Core {match.group(1)}"
130
+ return "Intel"
131
+ elif "AMD" in processor:
132
+ # Extract AMD CPU model
133
+ if "Ryzen" in processor:
134
+ match = re.search(r"Ryzen\s+(\d+)", processor)
135
+ if match:
136
+ return f"AMD Ryzen {match.group(1)}"
137
+ return "AMD"
138
+ elif (
139
+ "Apple" in processor
140
+ or "M1" in processor
141
+ or "M2" in processor
142
+ or "M3" in processor
143
+ ):
144
+ # Apple Silicon
145
+ match = re.search(r"(M\d+\w*)", processor)
146
+ if match:
147
+ return f"Apple {match.group(1)}"
148
+ return "Apple Silicon"
149
+ elif processor:
150
+ # Return cleaned processor string
151
+ return processor.strip()
152
+ except Exception as e:
153
+ plog.warning("Failed to detect CPU type", error=str(e))
154
+
155
+ return None
156
+
157
+
158
+ def normalize_platform_components(os_name: str, arch_name: str) -> tuple[str, str]:
159
+ """
160
+ Normalize OS and architecture names to standard format.
161
+
162
+ Args:
163
+ os_name: Operating system name
164
+ arch_name: Architecture name
165
+
166
+ Returns:
167
+ Tuple of (normalized_os, normalized_arch)
168
+ """
169
+ # Normalize OS names
170
+ os_map = {
171
+ "linux": "linux",
172
+ "darwin": "darwin",
173
+ "macos": "darwin",
174
+ "windows": "windows",
175
+ "win32": "windows",
176
+ }
177
+
178
+ # Normalize architecture names
179
+ arch_map = {
180
+ "x86_64": "amd64",
181
+ "amd64": "amd64",
182
+ "aarch64": "arm64",
183
+ "arm64": "arm64",
184
+ "i686": "x86",
185
+ "i586": "x86",
186
+ "i486": "x86",
187
+ "i386": "i386",
188
+ }
189
+
190
+ normalized_os = os_map.get(os_name.lower(), os_name.lower())
191
+ normalized_arch = arch_map.get(arch_name.lower(), arch_name.lower())
192
+
193
+ return normalized_os, normalized_arch
@@ -0,0 +1,157 @@
1
+ """System information gathering utilities."""
2
+
3
+ import contextlib
4
+ import os
5
+ import platform
6
+ import shutil
7
+ import sys
8
+
9
+ from attrs import define
10
+
11
+ from provide.foundation.logger import get_logger
12
+ from provide.foundation.platform.detection import (
13
+ get_arch_name,
14
+ get_cpu_type,
15
+ get_os_name,
16
+ get_os_version,
17
+ get_platform_string,
18
+ )
19
+
20
+ plog = get_logger(__name__)
21
+
22
+
23
+ @define
24
+ class SystemInfo:
25
+ """System information container."""
26
+
27
+ os_name: str
28
+ arch: str
29
+ platform: str
30
+ os_version: str | None
31
+ cpu_type: str | None
32
+ python_version: str
33
+ hostname: str | None
34
+ username: str | None
35
+ home_dir: str | None
36
+ temp_dir: str | None
37
+ num_cpus: int | None
38
+ total_memory: int | None
39
+ available_memory: int | None
40
+ disk_usage: dict[str, dict[str, int]] | None
41
+
42
+
43
+ def get_system_info() -> SystemInfo:
44
+ """
45
+ Gather comprehensive system information.
46
+
47
+ Returns:
48
+ SystemInfo object with all available system details
49
+ """
50
+ # Basic platform info
51
+ os_name = get_os_name()
52
+ arch = get_arch_name()
53
+ platform_str = get_platform_string()
54
+ os_version = get_os_version()
55
+ cpu_type = get_cpu_type()
56
+
57
+ # Python info
58
+ python_version = platform.python_version()
59
+
60
+ # System info
61
+ hostname = None
62
+ with contextlib.suppress(Exception):
63
+ hostname = platform.node()
64
+
65
+ # User info
66
+ username = os.environ.get("USER") or os.environ.get("USERNAME")
67
+ home_dir = os.path.expanduser("~")
68
+ temp_dir = os.environ.get("TMPDIR") or os.environ.get("TEMP") or "/tmp"
69
+
70
+ # CPU info
71
+ num_cpus = None
72
+ with contextlib.suppress(Exception):
73
+ num_cpus = os.cpu_count()
74
+
75
+ # Memory info (requires psutil for accurate values)
76
+ total_memory = None
77
+ available_memory = None
78
+ try:
79
+ import psutil
80
+
81
+ mem = psutil.virtual_memory()
82
+ total_memory = mem.total
83
+ available_memory = mem.available
84
+ except ImportError:
85
+ plog.debug("psutil not available, memory info limited")
86
+ except Exception as e:
87
+ plog.debug("Failed to get memory info", error=str(e))
88
+
89
+ # Disk usage
90
+ disk_usage = None
91
+ try:
92
+ disk_usage = {}
93
+ for path in ["/", home_dir, temp_dir]:
94
+ if os.path.exists(path):
95
+ usage = shutil.disk_usage(path)
96
+ disk_usage[path] = {
97
+ "total": usage.total,
98
+ "used": usage.used,
99
+ "free": usage.free,
100
+ }
101
+ except Exception as e:
102
+ plog.debug("Failed to get disk usage", error=str(e))
103
+
104
+ info = SystemInfo(
105
+ os_name=os_name,
106
+ arch=arch,
107
+ platform=platform_str,
108
+ os_version=os_version,
109
+ cpu_type=cpu_type,
110
+ python_version=python_version,
111
+ hostname=hostname,
112
+ username=username,
113
+ home_dir=home_dir,
114
+ temp_dir=temp_dir,
115
+ num_cpus=num_cpus,
116
+ total_memory=total_memory,
117
+ available_memory=available_memory,
118
+ disk_usage=disk_usage,
119
+ )
120
+
121
+ plog.debug(
122
+ "System information gathered",
123
+ platform=platform_str,
124
+ os=os_name,
125
+ arch=arch,
126
+ python=python_version,
127
+ cpus=num_cpus,
128
+ )
129
+
130
+ return info
131
+
132
+
133
+ # Platform detection functions
134
+ def is_windows() -> bool:
135
+ """Check if running on Windows."""
136
+ return sys.platform.startswith("win")
137
+
138
+
139
+ def is_macos() -> bool:
140
+ """Check if running on macOS."""
141
+ return sys.platform == "darwin"
142
+
143
+
144
+ def is_linux() -> bool:
145
+ """Check if running on Linux."""
146
+ return sys.platform.startswith("linux")
147
+
148
+
149
+ def is_arm() -> bool:
150
+ """Check if running on ARM architecture."""
151
+ machine = platform.machine().lower()
152
+ return "arm" in machine or "aarch" in machine
153
+
154
+
155
+ def is_64bit() -> bool:
156
+ """Check if running on 64-bit architecture."""
157
+ return platform.machine().endswith("64") or sys.maxsize > 2**32
@@ -0,0 +1,39 @@
1
+ """Process execution utilities.
2
+
3
+ Provides sync and async subprocess execution with consistent error handling,
4
+ and advanced process lifecycle management.
5
+ """
6
+
7
+ from provide.foundation.errors.runtime import ProcessError
8
+ from provide.foundation.process.async_runner import (
9
+ async_run_command,
10
+ async_run_shell,
11
+ async_stream_command,
12
+ )
13
+ from provide.foundation.process.lifecycle import (
14
+ ManagedProcess,
15
+ wait_for_process_output,
16
+ )
17
+ from provide.foundation.process.runner import (
18
+ CompletedProcess,
19
+ run_command,
20
+ run_shell,
21
+ stream_command,
22
+ )
23
+
24
+ __all__ = [
25
+ # Core types
26
+ "CompletedProcess",
27
+ "ProcessError",
28
+ # Sync execution
29
+ "run_command",
30
+ "run_shell",
31
+ "stream_command",
32
+ # Async execution
33
+ "async_run_command",
34
+ "async_run_shell",
35
+ "async_stream_command",
36
+ # Process lifecycle management
37
+ "ManagedProcess",
38
+ "wait_for_process_output",
39
+ ]