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.
- provide/__init__.py +15 -0
- provide/foundation/__init__.py +155 -0
- provide/foundation/_version.py +58 -0
- provide/foundation/cli/__init__.py +67 -0
- provide/foundation/cli/commands/__init__.py +3 -0
- provide/foundation/cli/commands/deps.py +71 -0
- provide/foundation/cli/commands/logs/__init__.py +63 -0
- provide/foundation/cli/commands/logs/generate.py +357 -0
- provide/foundation/cli/commands/logs/generate_old.py +569 -0
- provide/foundation/cli/commands/logs/query.py +174 -0
- provide/foundation/cli/commands/logs/send.py +166 -0
- provide/foundation/cli/commands/logs/tail.py +112 -0
- provide/foundation/cli/decorators.py +262 -0
- provide/foundation/cli/main.py +65 -0
- provide/foundation/cli/testing.py +220 -0
- provide/foundation/cli/utils.py +210 -0
- provide/foundation/config/__init__.py +106 -0
- provide/foundation/config/base.py +295 -0
- provide/foundation/config/env.py +369 -0
- provide/foundation/config/loader.py +311 -0
- provide/foundation/config/manager.py +387 -0
- provide/foundation/config/schema.py +284 -0
- provide/foundation/config/sync.py +281 -0
- provide/foundation/config/types.py +78 -0
- provide/foundation/config/validators.py +80 -0
- provide/foundation/console/__init__.py +29 -0
- provide/foundation/console/input.py +364 -0
- provide/foundation/console/output.py +178 -0
- provide/foundation/context/__init__.py +12 -0
- provide/foundation/context/core.py +356 -0
- provide/foundation/core.py +20 -0
- provide/foundation/crypto/__init__.py +182 -0
- provide/foundation/crypto/algorithms.py +111 -0
- provide/foundation/crypto/certificates.py +896 -0
- provide/foundation/crypto/checksums.py +301 -0
- provide/foundation/crypto/constants.py +57 -0
- provide/foundation/crypto/hashing.py +265 -0
- provide/foundation/crypto/keys.py +188 -0
- provide/foundation/crypto/signatures.py +144 -0
- provide/foundation/crypto/utils.py +164 -0
- provide/foundation/errors/__init__.py +96 -0
- provide/foundation/errors/auth.py +73 -0
- provide/foundation/errors/base.py +81 -0
- provide/foundation/errors/config.py +103 -0
- provide/foundation/errors/context.py +299 -0
- provide/foundation/errors/decorators.py +484 -0
- provide/foundation/errors/handlers.py +360 -0
- provide/foundation/errors/integration.py +105 -0
- provide/foundation/errors/platform.py +37 -0
- provide/foundation/errors/process.py +140 -0
- provide/foundation/errors/resources.py +133 -0
- provide/foundation/errors/runtime.py +160 -0
- provide/foundation/errors/safe_decorators.py +133 -0
- provide/foundation/errors/types.py +276 -0
- provide/foundation/file/__init__.py +79 -0
- provide/foundation/file/atomic.py +157 -0
- provide/foundation/file/directory.py +134 -0
- provide/foundation/file/formats.py +236 -0
- provide/foundation/file/lock.py +175 -0
- provide/foundation/file/safe.py +179 -0
- provide/foundation/file/utils.py +170 -0
- provide/foundation/hub/__init__.py +88 -0
- provide/foundation/hub/click_builder.py +310 -0
- provide/foundation/hub/commands.py +42 -0
- provide/foundation/hub/components.py +640 -0
- provide/foundation/hub/decorators.py +244 -0
- provide/foundation/hub/info.py +32 -0
- provide/foundation/hub/manager.py +446 -0
- provide/foundation/hub/registry.py +279 -0
- provide/foundation/hub/type_mapping.py +54 -0
- provide/foundation/hub/types.py +28 -0
- provide/foundation/logger/__init__.py +41 -0
- provide/foundation/logger/base.py +22 -0
- provide/foundation/logger/config/__init__.py +16 -0
- provide/foundation/logger/config/base.py +40 -0
- provide/foundation/logger/config/logging.py +394 -0
- provide/foundation/logger/config/telemetry.py +188 -0
- provide/foundation/logger/core.py +239 -0
- provide/foundation/logger/custom_processors.py +172 -0
- provide/foundation/logger/emoji/__init__.py +44 -0
- provide/foundation/logger/emoji/matrix.py +209 -0
- provide/foundation/logger/emoji/sets.py +458 -0
- provide/foundation/logger/emoji/types.py +56 -0
- provide/foundation/logger/factories.py +56 -0
- provide/foundation/logger/processors/__init__.py +13 -0
- provide/foundation/logger/processors/main.py +254 -0
- provide/foundation/logger/processors/trace.py +113 -0
- provide/foundation/logger/ratelimit/__init__.py +31 -0
- provide/foundation/logger/ratelimit/limiters.py +294 -0
- provide/foundation/logger/ratelimit/processor.py +203 -0
- provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
- provide/foundation/logger/setup/__init__.py +29 -0
- provide/foundation/logger/setup/coordinator.py +138 -0
- provide/foundation/logger/setup/emoji_resolver.py +64 -0
- provide/foundation/logger/setup/processors.py +85 -0
- provide/foundation/logger/setup/testing.py +39 -0
- provide/foundation/logger/trace.py +38 -0
- provide/foundation/metrics/__init__.py +119 -0
- provide/foundation/metrics/otel.py +122 -0
- provide/foundation/metrics/simple.py +165 -0
- provide/foundation/observability/__init__.py +53 -0
- provide/foundation/observability/openobserve/__init__.py +79 -0
- provide/foundation/observability/openobserve/auth.py +72 -0
- provide/foundation/observability/openobserve/client.py +307 -0
- provide/foundation/observability/openobserve/commands.py +357 -0
- provide/foundation/observability/openobserve/exceptions.py +41 -0
- provide/foundation/observability/openobserve/formatters.py +298 -0
- provide/foundation/observability/openobserve/models.py +134 -0
- provide/foundation/observability/openobserve/otlp.py +320 -0
- provide/foundation/observability/openobserve/search.py +222 -0
- provide/foundation/observability/openobserve/streaming.py +235 -0
- provide/foundation/platform/__init__.py +44 -0
- provide/foundation/platform/detection.py +193 -0
- provide/foundation/platform/info.py +157 -0
- provide/foundation/process/__init__.py +39 -0
- provide/foundation/process/async_runner.py +373 -0
- provide/foundation/process/lifecycle.py +406 -0
- provide/foundation/process/runner.py +390 -0
- provide/foundation/setup/__init__.py +101 -0
- provide/foundation/streams/__init__.py +44 -0
- provide/foundation/streams/console.py +57 -0
- provide/foundation/streams/core.py +65 -0
- provide/foundation/streams/file.py +104 -0
- provide/foundation/testing/__init__.py +166 -0
- provide/foundation/testing/cli.py +227 -0
- provide/foundation/testing/crypto.py +163 -0
- provide/foundation/testing/fixtures.py +49 -0
- provide/foundation/testing/hub.py +23 -0
- provide/foundation/testing/logger.py +106 -0
- provide/foundation/testing/streams.py +54 -0
- provide/foundation/tracer/__init__.py +49 -0
- provide/foundation/tracer/context.py +115 -0
- provide/foundation/tracer/otel.py +135 -0
- provide/foundation/tracer/spans.py +174 -0
- provide/foundation/types.py +32 -0
- provide/foundation/utils/__init__.py +97 -0
- provide/foundation/utils/deps.py +195 -0
- provide/foundation/utils/env.py +491 -0
- provide/foundation/utils/formatting.py +483 -0
- provide/foundation/utils/parsing.py +235 -0
- provide/foundation/utils/rate_limiting.py +112 -0
- provide/foundation/utils/streams.py +67 -0
- provide/foundation/utils/timing.py +93 -0
- provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
- provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
- provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
- provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
- 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
|
+
]
|