voice-mode-install 1.0.1__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.
Potentially problematic release.
This version of voice-mode-install might be problematic. Click here for more details.
- voice_mode_install-1.0.1.dist-info/METADATA +189 -0
- voice_mode_install-1.0.1.dist-info/RECORD +12 -0
- voice_mode_install-1.0.1.dist-info/WHEEL +4 -0
- voice_mode_install-1.0.1.dist-info/entry_points.txt +2 -0
- voicemode_install/__init__.py +3 -0
- voicemode_install/checker.py +191 -0
- voicemode_install/cli.py +297 -0
- voicemode_install/dependencies.yaml +359 -0
- voicemode_install/hardware.py +92 -0
- voicemode_install/installer.py +143 -0
- voicemode_install/logger.py +90 -0
- voicemode_install/system.py +141 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Hardware detection for service recommendations."""
|
|
2
|
+
|
|
3
|
+
import psutil
|
|
4
|
+
|
|
5
|
+
from .system import PlatformInfo
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HardwareInfo:
|
|
9
|
+
"""Detect and provide hardware recommendations."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, platform_info: PlatformInfo):
|
|
12
|
+
self.platform = platform_info
|
|
13
|
+
self.cpu_count = psutil.cpu_count(logical=False) or 1
|
|
14
|
+
self.total_ram_gb = psutil.virtual_memory().total / (1024 ** 3)
|
|
15
|
+
|
|
16
|
+
def is_apple_silicon(self) -> bool:
|
|
17
|
+
"""Check if running on Apple Silicon."""
|
|
18
|
+
return self.platform.os_type == 'darwin' and self.platform.architecture == 'arm64'
|
|
19
|
+
|
|
20
|
+
def is_arm64(self) -> bool:
|
|
21
|
+
"""Check if running on ARM64 architecture."""
|
|
22
|
+
return self.platform.architecture == 'arm64'
|
|
23
|
+
|
|
24
|
+
def get_ram_category(self) -> str:
|
|
25
|
+
"""Categorize RAM amount."""
|
|
26
|
+
if self.total_ram_gb < 4:
|
|
27
|
+
return 'low'
|
|
28
|
+
elif self.total_ram_gb < 8:
|
|
29
|
+
return 'medium'
|
|
30
|
+
elif self.total_ram_gb < 16:
|
|
31
|
+
return 'good'
|
|
32
|
+
else:
|
|
33
|
+
return 'excellent'
|
|
34
|
+
|
|
35
|
+
def should_recommend_local_services(self) -> bool:
|
|
36
|
+
"""Determine if local services should be recommended."""
|
|
37
|
+
# Apple Silicon is great for local services
|
|
38
|
+
if self.is_apple_silicon():
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
# Other ARM64 with good RAM
|
|
42
|
+
if self.is_arm64() and self.total_ram_gb >= 8:
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
# x86_64 with good specs
|
|
46
|
+
if self.total_ram_gb >= 8 and self.cpu_count >= 4:
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
def get_recommendation_message(self) -> str:
|
|
52
|
+
"""Get a recommendation message for local services."""
|
|
53
|
+
if self.is_apple_silicon():
|
|
54
|
+
return (
|
|
55
|
+
f"Your Apple Silicon Mac with {self.total_ram_gb:.1f}GB RAM is great for local services.\n"
|
|
56
|
+
f"Whisper and Kokoro will run fast and privately on your hardware."
|
|
57
|
+
)
|
|
58
|
+
elif self.is_arm64():
|
|
59
|
+
if self.total_ram_gb >= 8:
|
|
60
|
+
return (
|
|
61
|
+
f"Your ARM64 system with {self.total_ram_gb:.1f}GB RAM can run local services well.\n"
|
|
62
|
+
f"Recommended for privacy and offline use."
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
return (
|
|
66
|
+
f"Your ARM64 system has {self.total_ram_gb:.1f}GB RAM.\n"
|
|
67
|
+
f"Local services may work but cloud services might be more responsive."
|
|
68
|
+
)
|
|
69
|
+
else: # x86_64
|
|
70
|
+
if self.total_ram_gb >= 8 and self.cpu_count >= 4:
|
|
71
|
+
return (
|
|
72
|
+
f"Your system ({self.cpu_count} cores, {self.total_ram_gb:.1f}GB RAM) can run local services.\n"
|
|
73
|
+
f"Recommended for privacy and offline use."
|
|
74
|
+
)
|
|
75
|
+
elif self.total_ram_gb >= 4:
|
|
76
|
+
return (
|
|
77
|
+
f"Your system has {self.total_ram_gb:.1f}GB RAM.\n"
|
|
78
|
+
f"Local services will work but may be slower. Cloud services recommended for best performance."
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
return (
|
|
82
|
+
f"Your system has {self.total_ram_gb:.1f}GB RAM.\n"
|
|
83
|
+
f"Cloud services strongly recommended - local services may struggle."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def get_download_estimate(self) -> str:
|
|
87
|
+
"""Estimate download size for local services."""
|
|
88
|
+
# Rough estimates:
|
|
89
|
+
# Whisper: ~150MB (base model) to ~3GB (large model)
|
|
90
|
+
# Kokoro: ~500MB
|
|
91
|
+
# Total: ~2-4GB for full setup
|
|
92
|
+
return "~2-4GB total (Whisper models + Kokoro)"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""System package installation."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from .checker import PackageInfo
|
|
7
|
+
from .system import PlatformInfo, get_package_manager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PackageInstaller:
|
|
11
|
+
"""Install system packages using platform-specific package managers."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, platform_info: PlatformInfo, dry_run: bool = False):
|
|
14
|
+
self.platform = platform_info
|
|
15
|
+
self.dry_run = dry_run
|
|
16
|
+
self.package_manager = get_package_manager(platform_info.distribution)
|
|
17
|
+
|
|
18
|
+
def install_packages(self, packages: List[PackageInfo]) -> bool:
|
|
19
|
+
"""
|
|
20
|
+
Install a list of packages.
|
|
21
|
+
|
|
22
|
+
Returns True if all installations succeeded, False otherwise.
|
|
23
|
+
"""
|
|
24
|
+
if not packages:
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
package_names = [pkg.name for pkg in packages]
|
|
28
|
+
|
|
29
|
+
if self.dry_run:
|
|
30
|
+
print(f"[DRY RUN] Would install: {', '.join(package_names)}")
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
if self.platform.distribution == 'darwin':
|
|
35
|
+
return self._install_homebrew(package_names)
|
|
36
|
+
elif self.platform.distribution == 'debian':
|
|
37
|
+
return self._install_apt(package_names)
|
|
38
|
+
elif self.platform.distribution == 'fedora':
|
|
39
|
+
return self._install_dnf(package_names)
|
|
40
|
+
else:
|
|
41
|
+
print(f"Error: Unsupported distribution: {self.platform.distribution}")
|
|
42
|
+
return False
|
|
43
|
+
except Exception as e:
|
|
44
|
+
print(f"Error installing packages: {e}")
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
def _install_homebrew(self, packages: List[str]) -> bool:
|
|
48
|
+
"""Install packages using Homebrew."""
|
|
49
|
+
try:
|
|
50
|
+
cmd = ['brew', 'install'] + packages
|
|
51
|
+
result = subprocess.run(
|
|
52
|
+
cmd,
|
|
53
|
+
check=True,
|
|
54
|
+
capture_output=False # Show output to user
|
|
55
|
+
)
|
|
56
|
+
return result.returncode == 0
|
|
57
|
+
except subprocess.CalledProcessError as e:
|
|
58
|
+
print(f"Homebrew installation failed: {e}")
|
|
59
|
+
return False
|
|
60
|
+
except FileNotFoundError:
|
|
61
|
+
print("Error: Homebrew not found. Please install Homebrew first.")
|
|
62
|
+
print("Visit: https://brew.sh")
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def _install_apt(self, packages: List[str]) -> bool:
|
|
66
|
+
"""Install packages using apt."""
|
|
67
|
+
try:
|
|
68
|
+
# Update package lists first
|
|
69
|
+
print("Updating package lists...")
|
|
70
|
+
subprocess.run(
|
|
71
|
+
['sudo', 'apt', 'update'],
|
|
72
|
+
check=True,
|
|
73
|
+
capture_output=False
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Install packages
|
|
77
|
+
cmd = ['sudo', 'apt', 'install', '-y'] + packages
|
|
78
|
+
result = subprocess.run(
|
|
79
|
+
cmd,
|
|
80
|
+
check=True,
|
|
81
|
+
capture_output=False
|
|
82
|
+
)
|
|
83
|
+
return result.returncode == 0
|
|
84
|
+
except subprocess.CalledProcessError as e:
|
|
85
|
+
print(f"apt installation failed: {e}")
|
|
86
|
+
return False
|
|
87
|
+
except FileNotFoundError:
|
|
88
|
+
print("Error: apt not found")
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
def _install_dnf(self, packages: List[str]) -> bool:
|
|
92
|
+
"""Install packages using dnf."""
|
|
93
|
+
try:
|
|
94
|
+
cmd = ['sudo', 'dnf', 'install', '-y'] + packages
|
|
95
|
+
result = subprocess.run(
|
|
96
|
+
cmd,
|
|
97
|
+
check=True,
|
|
98
|
+
capture_output=False
|
|
99
|
+
)
|
|
100
|
+
return result.returncode == 0
|
|
101
|
+
except subprocess.CalledProcessError as e:
|
|
102
|
+
print(f"dnf installation failed: {e}")
|
|
103
|
+
return False
|
|
104
|
+
except FileNotFoundError:
|
|
105
|
+
print("Error: dnf not found")
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
def install_voicemode(self, version: Optional[str] = None) -> bool:
|
|
109
|
+
"""
|
|
110
|
+
Install voice-mode using uv tool install.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
version: Optional version to install (e.g., "5.1.3")
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
True if installation succeeded, False otherwise.
|
|
117
|
+
"""
|
|
118
|
+
if self.dry_run:
|
|
119
|
+
if version:
|
|
120
|
+
print(f"[DRY RUN] Would install: uv tool install voice-mode=={version}")
|
|
121
|
+
else:
|
|
122
|
+
print("[DRY RUN] Would install: uv tool install voice-mode")
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
if version:
|
|
127
|
+
cmd = ['uv', 'tool', 'install', f'voice-mode=={version}']
|
|
128
|
+
else:
|
|
129
|
+
cmd = ['uv', 'tool', 'install', 'voice-mode']
|
|
130
|
+
|
|
131
|
+
result = subprocess.run(
|
|
132
|
+
cmd,
|
|
133
|
+
check=True,
|
|
134
|
+
capture_output=False
|
|
135
|
+
)
|
|
136
|
+
return result.returncode == 0
|
|
137
|
+
except subprocess.CalledProcessError as e:
|
|
138
|
+
print(f"VoiceMode installation failed: {e}")
|
|
139
|
+
return False
|
|
140
|
+
except FileNotFoundError:
|
|
141
|
+
print("Error: uv not found. Please install uv first:")
|
|
142
|
+
print(" curl -LsSf https://astral.sh/uv/install.sh | sh")
|
|
143
|
+
return False
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Installation logging."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InstallLogger:
|
|
10
|
+
"""Log installation progress and results."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, log_path: Path = None):
|
|
13
|
+
if log_path is None:
|
|
14
|
+
voicemode_dir = Path.home() / '.voicemode'
|
|
15
|
+
voicemode_dir.mkdir(exist_ok=True)
|
|
16
|
+
log_path = voicemode_dir / 'install.log'
|
|
17
|
+
|
|
18
|
+
self.log_path = log_path
|
|
19
|
+
self.session_id = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
20
|
+
self.events = []
|
|
21
|
+
|
|
22
|
+
def log_event(self, event_type: str, message: str, details: Dict[str, Any] = None):
|
|
23
|
+
"""Log an installation event."""
|
|
24
|
+
event = {
|
|
25
|
+
'timestamp': datetime.now().isoformat(),
|
|
26
|
+
'session_id': self.session_id,
|
|
27
|
+
'type': event_type,
|
|
28
|
+
'message': message,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if details:
|
|
32
|
+
event['details'] = details
|
|
33
|
+
|
|
34
|
+
self.events.append(event)
|
|
35
|
+
|
|
36
|
+
# Append to log file
|
|
37
|
+
with open(self.log_path, 'a') as f:
|
|
38
|
+
f.write(json.dumps(event) + '\n')
|
|
39
|
+
|
|
40
|
+
def log_start(self, system_info: Dict[str, Any]):
|
|
41
|
+
"""Log installation start."""
|
|
42
|
+
self.log_event('start', 'Installation started', {'system': system_info})
|
|
43
|
+
|
|
44
|
+
def log_check(self, component: str, packages_found: int, packages_missing: int):
|
|
45
|
+
"""Log dependency check results."""
|
|
46
|
+
self.log_event(
|
|
47
|
+
'check',
|
|
48
|
+
f'Checked {component} dependencies',
|
|
49
|
+
{
|
|
50
|
+
'component': component,
|
|
51
|
+
'found': packages_found,
|
|
52
|
+
'missing': packages_missing
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def log_install(self, package_type: str, packages: list, success: bool):
|
|
57
|
+
"""Log package installation."""
|
|
58
|
+
self.log_event(
|
|
59
|
+
'install',
|
|
60
|
+
f'{"Successfully installed" if success else "Failed to install"} {package_type} packages',
|
|
61
|
+
{
|
|
62
|
+
'package_type': package_type,
|
|
63
|
+
'packages': packages,
|
|
64
|
+
'success': success
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def log_error(self, message: str, error: Exception = None):
|
|
69
|
+
"""Log an error."""
|
|
70
|
+
details = {'message': message}
|
|
71
|
+
if error:
|
|
72
|
+
details['error'] = str(error)
|
|
73
|
+
details['error_type'] = type(error).__name__
|
|
74
|
+
|
|
75
|
+
self.log_event('error', message, details)
|
|
76
|
+
|
|
77
|
+
def log_complete(self, success: bool, voicemode_installed: bool):
|
|
78
|
+
"""Log installation completion."""
|
|
79
|
+
self.log_event(
|
|
80
|
+
'complete',
|
|
81
|
+
'Installation completed' if success else 'Installation failed',
|
|
82
|
+
{
|
|
83
|
+
'success': success,
|
|
84
|
+
'voicemode_installed': voicemode_installed
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def get_log_path(self) -> str:
|
|
89
|
+
"""Get the path to the log file."""
|
|
90
|
+
return str(self.log_path)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""System detection and platform utilities."""
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class PlatformInfo:
|
|
13
|
+
"""Information about the current platform."""
|
|
14
|
+
|
|
15
|
+
os_type: str # darwin, linux, windows
|
|
16
|
+
os_name: str # macOS, Ubuntu, Fedora, etc.
|
|
17
|
+
distribution: str # debian, fedora, darwin, etc. (for yaml lookup)
|
|
18
|
+
architecture: str # arm64, x86_64
|
|
19
|
+
is_wsl: bool = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def detect_platform() -> PlatformInfo:
|
|
23
|
+
"""Detect the current platform and return platform information."""
|
|
24
|
+
os_type = platform.system().lower()
|
|
25
|
+
architecture = platform.machine().lower()
|
|
26
|
+
|
|
27
|
+
# Normalize architecture names
|
|
28
|
+
if architecture in ('amd64', 'x86_64', 'x64'):
|
|
29
|
+
architecture = 'x86_64'
|
|
30
|
+
elif architecture in ('aarch64', 'arm64'):
|
|
31
|
+
architecture = 'arm64'
|
|
32
|
+
|
|
33
|
+
# Detect WSL
|
|
34
|
+
is_wsl = False
|
|
35
|
+
if os_type == 'linux':
|
|
36
|
+
try:
|
|
37
|
+
with open('/proc/version', 'r') as f:
|
|
38
|
+
is_wsl = 'microsoft' in f.read().lower() or 'wsl' in f.read().lower()
|
|
39
|
+
except FileNotFoundError:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
if os_type == 'darwin':
|
|
43
|
+
return PlatformInfo(
|
|
44
|
+
os_type='darwin',
|
|
45
|
+
os_name='macOS',
|
|
46
|
+
distribution='darwin',
|
|
47
|
+
architecture=architecture,
|
|
48
|
+
is_wsl=False
|
|
49
|
+
)
|
|
50
|
+
elif os_type == 'linux':
|
|
51
|
+
# Detect Linux distribution
|
|
52
|
+
distro_info = _detect_linux_distro()
|
|
53
|
+
return PlatformInfo(
|
|
54
|
+
os_type='linux',
|
|
55
|
+
os_name=distro_info['name'],
|
|
56
|
+
distribution=distro_info['family'],
|
|
57
|
+
architecture=architecture,
|
|
58
|
+
is_wsl=is_wsl
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
raise RuntimeError(f"Unsupported operating system: {os_type}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _detect_linux_distro() -> dict:
|
|
65
|
+
"""Detect Linux distribution and family."""
|
|
66
|
+
# Try /etc/os-release first (most modern distros)
|
|
67
|
+
if Path('/etc/os-release').exists():
|
|
68
|
+
os_release = {}
|
|
69
|
+
with open('/etc/os-release') as f:
|
|
70
|
+
for line in f:
|
|
71
|
+
if '=' in line:
|
|
72
|
+
key, value = line.strip().split('=', 1)
|
|
73
|
+
os_release[key] = value.strip('"')
|
|
74
|
+
|
|
75
|
+
distro_id = os_release.get('ID', '').lower()
|
|
76
|
+
distro_id_like = os_release.get('ID_LIKE', '').lower()
|
|
77
|
+
distro_name = os_release.get('NAME', 'Linux')
|
|
78
|
+
|
|
79
|
+
# Determine family for yaml lookup
|
|
80
|
+
if distro_id in ('ubuntu', 'debian') or 'debian' in distro_id_like or 'ubuntu' in distro_id_like:
|
|
81
|
+
return {'name': distro_name, 'family': 'debian'}
|
|
82
|
+
elif distro_id in ('fedora', 'rhel', 'centos', 'rocky', 'alma') or 'fedora' in distro_id_like or 'rhel' in distro_id_like:
|
|
83
|
+
return {'name': distro_name, 'family': 'fedora'}
|
|
84
|
+
elif distro_id == 'arch' or 'arch' in distro_id_like:
|
|
85
|
+
return {'name': distro_name, 'family': 'arch'}
|
|
86
|
+
elif distro_id == 'alpine':
|
|
87
|
+
return {'name': distro_name, 'family': 'alpine'}
|
|
88
|
+
elif distro_id in ('opensuse', 'suse') or 'suse' in distro_id_like:
|
|
89
|
+
return {'name': distro_name, 'family': 'suse'}
|
|
90
|
+
else:
|
|
91
|
+
# Try to infer from ID_LIKE
|
|
92
|
+
if 'debian' in distro_id_like:
|
|
93
|
+
return {'name': distro_name, 'family': 'debian'}
|
|
94
|
+
elif 'fedora' in distro_id_like or 'rhel' in distro_id_like:
|
|
95
|
+
return {'name': distro_name, 'family': 'fedora'}
|
|
96
|
+
|
|
97
|
+
# Fallback: check for package managers
|
|
98
|
+
if Path('/usr/bin/apt').exists() or Path('/usr/bin/apt-get').exists():
|
|
99
|
+
return {'name': 'Debian-based Linux', 'family': 'debian'}
|
|
100
|
+
elif Path('/usr/bin/dnf').exists() or Path('/usr/bin/yum').exists():
|
|
101
|
+
return {'name': 'Fedora-based Linux', 'family': 'fedora'}
|
|
102
|
+
|
|
103
|
+
raise RuntimeError("Unable to detect Linux distribution")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_package_manager(distribution: str) -> str:
|
|
107
|
+
"""Get the package manager command for the distribution."""
|
|
108
|
+
if distribution == 'darwin':
|
|
109
|
+
return 'brew'
|
|
110
|
+
elif distribution == 'debian':
|
|
111
|
+
return 'apt'
|
|
112
|
+
elif distribution == 'fedora':
|
|
113
|
+
return 'dnf'
|
|
114
|
+
elif distribution == 'arch':
|
|
115
|
+
return 'pacman'
|
|
116
|
+
elif distribution == 'alpine':
|
|
117
|
+
return 'apk'
|
|
118
|
+
elif distribution == 'suse':
|
|
119
|
+
return 'zypper'
|
|
120
|
+
else:
|
|
121
|
+
raise ValueError(f"Unknown distribution: {distribution}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def check_command_exists(command: str) -> bool:
|
|
125
|
+
"""Check if a command exists in PATH."""
|
|
126
|
+
return shutil.which(command) is not None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_system_info() -> dict:
|
|
130
|
+
"""Get comprehensive system information for logging."""
|
|
131
|
+
platform_info = detect_platform()
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
'os_type': platform_info.os_type,
|
|
135
|
+
'os_name': platform_info.os_name,
|
|
136
|
+
'distribution': platform_info.distribution,
|
|
137
|
+
'architecture': platform_info.architecture,
|
|
138
|
+
'is_wsl': platform_info.is_wsl,
|
|
139
|
+
'python_version': platform.python_version(),
|
|
140
|
+
'platform': platform.platform(),
|
|
141
|
+
}
|