sixseven 0.1.0__py3-none-macosx_11_0_arm64.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.
- sixseven-0.1.0.data/purelib/sixtyseven/__init__.py +36 -0
- sixseven-0.1.0.data/purelib/sixtyseven/cli.py +64 -0
- sixseven-0.1.0.data/purelib/sixtyseven/client.py +190 -0
- sixseven-0.1.0.data/purelib/sixtyseven/config.py +161 -0
- sixseven-0.1.0.data/purelib/sixtyseven/exceptions.py +40 -0
- sixseven-0.1.0.data/purelib/sixtyseven/local.py +335 -0
- sixseven-0.1.0.data/purelib/sixtyseven/metrics.py +157 -0
- sixseven-0.1.0.data/purelib/sixtyseven/run.py +445 -0
- sixseven-0.1.0.data/purelib/sixtyseven/server.py +383 -0
- sixseven-0.1.0.data/purelib/sixtyseven/utils.py +171 -0
- sixseven-0.1.0.dist-info/METADATA +84 -0
- sixseven-0.1.0.dist-info/RECORD +15 -0
- sixseven-0.1.0.dist-info/WHEEL +5 -0
- sixseven-0.1.0.dist-info/entry_points.txt +2 -0
- sixseven-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"""Server management for automatically starting the Sixtyseven viewer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import shutil
|
|
9
|
+
import socket
|
|
10
|
+
import subprocess
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from sixtyseven.exceptions import ServerError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ServerManager:
|
|
19
|
+
"""
|
|
20
|
+
Manages the Sixtyseven server lifecycle.
|
|
21
|
+
|
|
22
|
+
Handles discovering, starting, and stopping the sixtyseven binary
|
|
23
|
+
for viewing training metrics in real-time.
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
manager = ServerManager(logdir="./logs")
|
|
27
|
+
manager.start()
|
|
28
|
+
# ... do training ...
|
|
29
|
+
manager.stop() # or let it auto-cleanup on exit
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
BINARY_NAME = "sixtyseven"
|
|
33
|
+
DEFAULT_PORT = 6767
|
|
34
|
+
DEFAULT_HOST = "localhost"
|
|
35
|
+
HEALTH_CHECK_TIMEOUT = 10 # seconds
|
|
36
|
+
HEALTH_CHECK_INTERVAL = 0.2 # seconds
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
logdir: str,
|
|
41
|
+
port: int = DEFAULT_PORT,
|
|
42
|
+
host: str = DEFAULT_HOST,
|
|
43
|
+
open_browser: bool = True,
|
|
44
|
+
binary_path: Optional[str] = None,
|
|
45
|
+
keep_running: bool = False,
|
|
46
|
+
project: Optional[str] = None,
|
|
47
|
+
run_id: Optional[str] = None,
|
|
48
|
+
):
|
|
49
|
+
"""
|
|
50
|
+
Initialize the server manager.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
logdir: Directory containing the logs to serve
|
|
54
|
+
port: Port to run the server on
|
|
55
|
+
host: Host to bind the server to
|
|
56
|
+
open_browser: Whether to open the browser automatically
|
|
57
|
+
binary_path: Explicit path to the sixtyseven binary (auto-discovered if not provided)
|
|
58
|
+
keep_running: If True, don't stop the server when the manager is garbage collected
|
|
59
|
+
project: Project name (used as fallback if run_id not provided)
|
|
60
|
+
run_id: Run ID to open in the browser (opens specific run view)
|
|
61
|
+
"""
|
|
62
|
+
self.logdir = logdir
|
|
63
|
+
self.port = port
|
|
64
|
+
self.host = host
|
|
65
|
+
self.open_browser = open_browser
|
|
66
|
+
self.binary_path = binary_path
|
|
67
|
+
self.keep_running = keep_running
|
|
68
|
+
self.project = project
|
|
69
|
+
self.run_id = run_id
|
|
70
|
+
|
|
71
|
+
self._process: Optional[subprocess.Popen] = None
|
|
72
|
+
self._started = False
|
|
73
|
+
self._we_started_server = False # Track if we started it vs reusing existing
|
|
74
|
+
|
|
75
|
+
def start(self) -> str:
|
|
76
|
+
"""
|
|
77
|
+
Start the server.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The URL where the server is running
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
ServerError: If the binary cannot be found or the server fails to start
|
|
84
|
+
"""
|
|
85
|
+
if self._started:
|
|
86
|
+
return self.url
|
|
87
|
+
|
|
88
|
+
# Check if a server is already running on this port
|
|
89
|
+
if self._is_port_in_use():
|
|
90
|
+
# Server already running - just set the active run so UI navigates
|
|
91
|
+
self._set_active_run()
|
|
92
|
+
print(f"Sixtyseven: Navigating to run in existing viewer at {self.url}")
|
|
93
|
+
self._started = True
|
|
94
|
+
return self.url
|
|
95
|
+
|
|
96
|
+
# Find the binary
|
|
97
|
+
binary = self._find_binary()
|
|
98
|
+
if not binary:
|
|
99
|
+
raise ServerError(
|
|
100
|
+
"Could not find 'sixtyseven' binary. Please ensure it's installed and in your PATH, "
|
|
101
|
+
"or specify the path explicitly with server_binary='/path/to/sixtyseven'"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Build the command (we handle browser opening ourselves for project-specific URLs)
|
|
105
|
+
cmd = [
|
|
106
|
+
binary,
|
|
107
|
+
"serve",
|
|
108
|
+
f"--logdir={self.logdir}",
|
|
109
|
+
f"--port={self.port}",
|
|
110
|
+
f"--host={self.host}",
|
|
111
|
+
"--open=false", # We'll open the browser ourselves with the right URL
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
# Start the server process
|
|
115
|
+
try:
|
|
116
|
+
# Use DEVNULL for stdin to prevent the process from waiting for input
|
|
117
|
+
# Redirect stdout/stderr to suppress server logs in the training output
|
|
118
|
+
self._process = subprocess.Popen(
|
|
119
|
+
cmd,
|
|
120
|
+
stdin=subprocess.DEVNULL,
|
|
121
|
+
stdout=subprocess.PIPE,
|
|
122
|
+
stderr=subprocess.PIPE,
|
|
123
|
+
start_new_session=True, # Detach from parent process group
|
|
124
|
+
)
|
|
125
|
+
except OSError as e:
|
|
126
|
+
raise ServerError(f"Failed to start server: {e}")
|
|
127
|
+
|
|
128
|
+
# Wait for the server to be ready
|
|
129
|
+
if not self._wait_for_health():
|
|
130
|
+
# Server didn't start properly - get error output
|
|
131
|
+
if self._process.poll() is not None:
|
|
132
|
+
_, stderr = self._process.communicate()
|
|
133
|
+
error_msg = stderr.decode().strip() if stderr else "Unknown error"
|
|
134
|
+
raise ServerError(f"Server failed to start: {error_msg}")
|
|
135
|
+
else:
|
|
136
|
+
self._process.terminate()
|
|
137
|
+
raise ServerError(
|
|
138
|
+
f"Server did not respond within {self.HEALTH_CHECK_TIMEOUT}s"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
self._started = True
|
|
142
|
+
self._we_started_server = True
|
|
143
|
+
|
|
144
|
+
# Register cleanup handler (unless keep_running is True)
|
|
145
|
+
if not self.keep_running:
|
|
146
|
+
atexit.register(self.stop)
|
|
147
|
+
|
|
148
|
+
# Set active run so UI navigates to it
|
|
149
|
+
self._set_active_run()
|
|
150
|
+
|
|
151
|
+
# Open browser to the project-specific URL
|
|
152
|
+
if self.open_browser:
|
|
153
|
+
self._open_browser()
|
|
154
|
+
|
|
155
|
+
print(f"Sixtyseven: Server started at {self.url}")
|
|
156
|
+
return self.url
|
|
157
|
+
|
|
158
|
+
def stop(self) -> None:
|
|
159
|
+
"""Stop the server if it was started by this manager."""
|
|
160
|
+
if not self._we_started_server:
|
|
161
|
+
return # Don't stop a server we didn't start
|
|
162
|
+
|
|
163
|
+
if self._process is not None and self._process.poll() is None:
|
|
164
|
+
self._process.terminate()
|
|
165
|
+
try:
|
|
166
|
+
self._process.wait(timeout=5)
|
|
167
|
+
except subprocess.TimeoutExpired:
|
|
168
|
+
self._process.kill()
|
|
169
|
+
self._process = None
|
|
170
|
+
self._we_started_server = False
|
|
171
|
+
print("Sixtyseven: Server stopped")
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def url(self) -> str:
|
|
175
|
+
"""Return the server URL."""
|
|
176
|
+
return f"http://{self.host}:{self.port}"
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def run_url(self) -> str:
|
|
180
|
+
"""Return the URL to open in the browser."""
|
|
181
|
+
if self.run_id:
|
|
182
|
+
return f"{self.url}/runs/{self.run_id}"
|
|
183
|
+
if self.project:
|
|
184
|
+
return f"{self.url}/projects/{self.project}"
|
|
185
|
+
return f"{self.url}/projects"
|
|
186
|
+
|
|
187
|
+
def _open_browser(self) -> None:
|
|
188
|
+
"""Open the browser to the run URL."""
|
|
189
|
+
import webbrowser
|
|
190
|
+
|
|
191
|
+
webbrowser.open(self.run_url)
|
|
192
|
+
|
|
193
|
+
def _set_active_run(self) -> None:
|
|
194
|
+
"""Tell the server about the active run so the UI can navigate to it."""
|
|
195
|
+
if not self.run_id:
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
import urllib.request
|
|
199
|
+
import urllib.error
|
|
200
|
+
import json
|
|
201
|
+
|
|
202
|
+
url = f"{self.url}/api/v1/active-run"
|
|
203
|
+
data = json.dumps({"run_id": self.run_id}).encode("utf-8")
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
req = urllib.request.Request(
|
|
207
|
+
url,
|
|
208
|
+
data=data,
|
|
209
|
+
headers={"Content-Type": "application/json"},
|
|
210
|
+
method="POST",
|
|
211
|
+
)
|
|
212
|
+
with urllib.request.urlopen(req, timeout=2):
|
|
213
|
+
pass
|
|
214
|
+
except (urllib.error.URLError, OSError):
|
|
215
|
+
# Server might not support this endpoint yet, ignore
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def is_running(self) -> bool:
|
|
220
|
+
"""Check if the server is running."""
|
|
221
|
+
if self._process is not None:
|
|
222
|
+
return self._process.poll() is None
|
|
223
|
+
# Check if something else is running on the port
|
|
224
|
+
return self._is_port_in_use()
|
|
225
|
+
|
|
226
|
+
def _find_binary(self) -> Optional[str]:
|
|
227
|
+
"""
|
|
228
|
+
Find the sixtyseven binary.
|
|
229
|
+
|
|
230
|
+
Search order:
|
|
231
|
+
1. Explicit binary_path if provided
|
|
232
|
+
2. SIXTYSEVEN_BINARY environment variable
|
|
233
|
+
3. Bundled binary inside the Python package
|
|
234
|
+
4. System PATH
|
|
235
|
+
5. Common installation locations
|
|
236
|
+
"""
|
|
237
|
+
# Check explicit path
|
|
238
|
+
if self.binary_path:
|
|
239
|
+
if os.path.isfile(self.binary_path) and os.access(
|
|
240
|
+
self.binary_path, os.X_OK
|
|
241
|
+
):
|
|
242
|
+
return self.binary_path
|
|
243
|
+
raise ServerError(
|
|
244
|
+
f"Specified binary not found or not executable: {self.binary_path}"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Check environment variable
|
|
248
|
+
env_binary = os.environ.get("SIXTYSEVEN_BINARY")
|
|
249
|
+
if env_binary:
|
|
250
|
+
if os.path.isfile(env_binary) and os.access(env_binary, os.X_OK):
|
|
251
|
+
return env_binary
|
|
252
|
+
raise ServerError(f"SIXTYSEVEN_BINARY points to invalid path: {env_binary}")
|
|
253
|
+
|
|
254
|
+
# Check bundled binary in the package
|
|
255
|
+
bundled_binary = self._bundled_binary_path()
|
|
256
|
+
if bundled_binary:
|
|
257
|
+
return bundled_binary
|
|
258
|
+
|
|
259
|
+
# Check system PATH
|
|
260
|
+
binary_name = (
|
|
261
|
+
f"{self.BINARY_NAME}.exe"
|
|
262
|
+
if platform.system() == "Windows"
|
|
263
|
+
else self.BINARY_NAME
|
|
264
|
+
)
|
|
265
|
+
path_binary = shutil.which(binary_name)
|
|
266
|
+
if path_binary:
|
|
267
|
+
return path_binary
|
|
268
|
+
|
|
269
|
+
# Check current working directory (for local development)
|
|
270
|
+
cwd_binary = os.path.join(os.getcwd(), binary_name)
|
|
271
|
+
if os.path.isfile(cwd_binary) and os.access(cwd_binary, os.X_OK):
|
|
272
|
+
return cwd_binary
|
|
273
|
+
|
|
274
|
+
# Check common installation locations
|
|
275
|
+
common_paths = self._get_common_paths()
|
|
276
|
+
for path in common_paths:
|
|
277
|
+
if os.path.isfile(path) and os.access(path, os.X_OK):
|
|
278
|
+
return path
|
|
279
|
+
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
def _bundled_binary_path(self) -> Optional[str]:
|
|
283
|
+
"""Return the path to a bundled binary inside the package, if present."""
|
|
284
|
+
platform_id = self._platform_id()
|
|
285
|
+
if not platform_id:
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
binary_name = (
|
|
289
|
+
f"{self.BINARY_NAME}.exe"
|
|
290
|
+
if platform.system() == "Windows"
|
|
291
|
+
else self.BINARY_NAME
|
|
292
|
+
)
|
|
293
|
+
base_dir = Path(__file__).resolve().parent
|
|
294
|
+
candidate = base_dir / "bin" / platform_id / binary_name
|
|
295
|
+
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
296
|
+
return str(candidate)
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
def _platform_id(self) -> Optional[str]:
|
|
300
|
+
"""Return platform identifier used for bundled binaries."""
|
|
301
|
+
system = platform.system().lower()
|
|
302
|
+
machine = platform.machine().lower()
|
|
303
|
+
|
|
304
|
+
if machine in {"x86_64", "amd64"}:
|
|
305
|
+
arch = "amd64"
|
|
306
|
+
elif machine in {"aarch64", "arm64"}:
|
|
307
|
+
arch = "arm64"
|
|
308
|
+
else:
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
if system == "darwin":
|
|
312
|
+
return f"darwin-{arch}"
|
|
313
|
+
if system == "linux":
|
|
314
|
+
return f"linux-{arch}"
|
|
315
|
+
if system == "windows":
|
|
316
|
+
return f"windows-{arch}"
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
def _get_common_paths(self) -> list[str]:
|
|
320
|
+
"""Get common installation paths based on platform."""
|
|
321
|
+
system = platform.system()
|
|
322
|
+
home = Path.home()
|
|
323
|
+
|
|
324
|
+
if system == "Darwin":
|
|
325
|
+
return [
|
|
326
|
+
"/usr/local/bin/sixtyseven",
|
|
327
|
+
"/opt/homebrew/bin/sixtyseven",
|
|
328
|
+
str(home / ".local" / "bin" / "sixtyseven"),
|
|
329
|
+
str(home / "bin" / "sixtyseven"),
|
|
330
|
+
]
|
|
331
|
+
elif system == "Windows":
|
|
332
|
+
return [
|
|
333
|
+
str(home / "AppData" / "Local" / "sixtyseven" / "sixtyseven.exe"),
|
|
334
|
+
str(home / "scoop" / "shims" / "sixtyseven.exe"),
|
|
335
|
+
"C:\\Program Files\\sixtyseven\\sixtyseven.exe",
|
|
336
|
+
]
|
|
337
|
+
else: # Linux
|
|
338
|
+
return [
|
|
339
|
+
"/usr/local/bin/sixtyseven",
|
|
340
|
+
"/usr/bin/sixtyseven",
|
|
341
|
+
str(home / ".local" / "bin" / "sixtyseven"),
|
|
342
|
+
str(home / "bin" / "sixtyseven"),
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
def _is_port_in_use(self) -> bool:
|
|
346
|
+
"""Check if something is actually listening on the port."""
|
|
347
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
348
|
+
s.settimeout(1)
|
|
349
|
+
try:
|
|
350
|
+
s.connect((self.host, self.port))
|
|
351
|
+
return True # Connection succeeded, something is listening
|
|
352
|
+
except (OSError, ConnectionRefusedError):
|
|
353
|
+
return False # Nothing listening
|
|
354
|
+
|
|
355
|
+
def _wait_for_health(self) -> bool:
|
|
356
|
+
"""Wait for the server to respond to health checks."""
|
|
357
|
+
import urllib.request
|
|
358
|
+
import urllib.error
|
|
359
|
+
|
|
360
|
+
health_url = f"{self.url}/health"
|
|
361
|
+
start_time = time.time()
|
|
362
|
+
|
|
363
|
+
while time.time() - start_time < self.HEALTH_CHECK_TIMEOUT:
|
|
364
|
+
# Check if process died
|
|
365
|
+
if self._process.poll() is not None:
|
|
366
|
+
return False
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
req = urllib.request.Request(health_url, method="GET")
|
|
370
|
+
with urllib.request.urlopen(req, timeout=1) as response:
|
|
371
|
+
if response.status == 200:
|
|
372
|
+
return True
|
|
373
|
+
except (urllib.error.URLError, OSError):
|
|
374
|
+
pass
|
|
375
|
+
|
|
376
|
+
time.sleep(self.HEALTH_CHECK_INTERVAL)
|
|
377
|
+
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
def __del__(self):
|
|
381
|
+
"""Cleanup on garbage collection."""
|
|
382
|
+
if not self.keep_running and self._we_started_server:
|
|
383
|
+
self.stop()
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Utility functions for the Sixtyseven SDK."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_git_info() -> Optional[Dict[str, Any]]:
|
|
10
|
+
"""
|
|
11
|
+
Capture git repository information.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Dictionary with commit, branch, remote, dirty status, and message.
|
|
15
|
+
None if not in a git repository.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
# Check if in git repo
|
|
19
|
+
subprocess.run(
|
|
20
|
+
["git", "rev-parse", "--git-dir"],
|
|
21
|
+
capture_output=True,
|
|
22
|
+
check=True,
|
|
23
|
+
timeout=5,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Get commit hash
|
|
27
|
+
commit = subprocess.run(
|
|
28
|
+
["git", "rev-parse", "HEAD"],
|
|
29
|
+
capture_output=True,
|
|
30
|
+
text=True,
|
|
31
|
+
timeout=5,
|
|
32
|
+
).stdout.strip()
|
|
33
|
+
|
|
34
|
+
# Get branch name
|
|
35
|
+
branch = subprocess.run(
|
|
36
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
37
|
+
capture_output=True,
|
|
38
|
+
text=True,
|
|
39
|
+
timeout=5,
|
|
40
|
+
).stdout.strip()
|
|
41
|
+
|
|
42
|
+
# Get remote URL
|
|
43
|
+
remote = subprocess.run(
|
|
44
|
+
["git", "config", "--get", "remote.origin.url"],
|
|
45
|
+
capture_output=True,
|
|
46
|
+
text=True,
|
|
47
|
+
timeout=5,
|
|
48
|
+
).stdout.strip()
|
|
49
|
+
|
|
50
|
+
# Check for uncommitted changes
|
|
51
|
+
status = subprocess.run(
|
|
52
|
+
["git", "status", "--porcelain"],
|
|
53
|
+
capture_output=True,
|
|
54
|
+
text=True,
|
|
55
|
+
timeout=5,
|
|
56
|
+
)
|
|
57
|
+
dirty = bool(status.stdout.strip())
|
|
58
|
+
|
|
59
|
+
# Get commit message
|
|
60
|
+
message = subprocess.run(
|
|
61
|
+
["git", "log", "-1", "--pretty=%B"],
|
|
62
|
+
capture_output=True,
|
|
63
|
+
text=True,
|
|
64
|
+
timeout=5,
|
|
65
|
+
).stdout.strip()
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
"commit": commit,
|
|
69
|
+
"branch": branch,
|
|
70
|
+
"remote": remote,
|
|
71
|
+
"dirty": dirty,
|
|
72
|
+
"message": message[:200] if message else None, # Truncate long messages
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
except (
|
|
76
|
+
subprocess.CalledProcessError,
|
|
77
|
+
subprocess.TimeoutExpired,
|
|
78
|
+
FileNotFoundError,
|
|
79
|
+
):
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_system_info() -> Dict[str, Any]:
|
|
84
|
+
"""
|
|
85
|
+
Capture system and hardware information.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Dictionary with hostname, OS, Python version, CPU, memory, and GPU info.
|
|
89
|
+
"""
|
|
90
|
+
import sys
|
|
91
|
+
|
|
92
|
+
info = {
|
|
93
|
+
"hostname": platform.node(),
|
|
94
|
+
"os": f"{platform.system()} {platform.release()}",
|
|
95
|
+
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
|
96
|
+
"cpu_count": os.cpu_count(),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Try to get memory info
|
|
100
|
+
try:
|
|
101
|
+
import psutil
|
|
102
|
+
|
|
103
|
+
mem = psutil.virtual_memory()
|
|
104
|
+
info["memory_gb"] = round(mem.total / (1024**3), 2)
|
|
105
|
+
except ImportError:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
# Try to get GPU info
|
|
109
|
+
gpu_info = get_gpu_info()
|
|
110
|
+
if gpu_info:
|
|
111
|
+
info["gpu_info"] = gpu_info
|
|
112
|
+
|
|
113
|
+
return info
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_gpu_info() -> Optional[List[str]]:
|
|
117
|
+
"""
|
|
118
|
+
Get GPU information using nvidia-smi.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
List of GPU names, or None if no NVIDIA GPUs found.
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
result = subprocess.run(
|
|
125
|
+
["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"],
|
|
126
|
+
capture_output=True,
|
|
127
|
+
text=True,
|
|
128
|
+
timeout=10,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if result.returncode == 0:
|
|
132
|
+
gpus = [
|
|
133
|
+
line.strip()
|
|
134
|
+
for line in result.stdout.strip().split("\n")
|
|
135
|
+
if line.strip()
|
|
136
|
+
]
|
|
137
|
+
return gpus if gpus else None
|
|
138
|
+
|
|
139
|
+
except (
|
|
140
|
+
subprocess.CalledProcessError,
|
|
141
|
+
subprocess.TimeoutExpired,
|
|
142
|
+
FileNotFoundError,
|
|
143
|
+
):
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
# Try PyTorch
|
|
147
|
+
try:
|
|
148
|
+
import torch
|
|
149
|
+
|
|
150
|
+
if torch.cuda.is_available():
|
|
151
|
+
return [
|
|
152
|
+
torch.cuda.get_device_name(i) for i in range(torch.cuda.device_count())
|
|
153
|
+
]
|
|
154
|
+
except ImportError:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def generate_run_name() -> str:
|
|
161
|
+
"""Generate a unique run name."""
|
|
162
|
+
import time
|
|
163
|
+
from uuid import uuid4
|
|
164
|
+
|
|
165
|
+
adjectives = ["swift", "bright", "calm", "bold", "keen", "wise", "pure", "warm"]
|
|
166
|
+
nouns = ["falcon", "river", "forest", "peak", "star", "wave", "cloud", "dawn"]
|
|
167
|
+
|
|
168
|
+
adj = adjectives[int(time.time() * 1000) % len(adjectives)]
|
|
169
|
+
noun = nouns[int(time.time() * 1000 // 7) % len(nouns)]
|
|
170
|
+
|
|
171
|
+
return f"{adj}-{noun}-{uuid4().hex[:8]}"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: sixseven
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Sixtyseven ML experiment tracking
|
|
5
|
+
Author: Sixtyseven Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sixtyseven/sixtyseven
|
|
8
|
+
Project-URL: Documentation, https://docs.sixtyseven.ai
|
|
9
|
+
Project-URL: Repository, https://github.com/sixtyseven/sixtyseven
|
|
10
|
+
Keywords: ml,machine-learning,experiment-tracking,metrics,training
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
Requires-Dist: requests>=2.28.0
|
|
28
|
+
Requires-Dist: websocket-client>=1.4.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
32
|
+
Requires-Dist: responses>=0.23.0; extra == "dev"
|
|
33
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
35
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
36
|
+
|
|
37
|
+
# Sixtyseven Python SDK
|
|
38
|
+
|
|
39
|
+
Track ML experiments locally. No server setup required.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install sixtyseven
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from sixtyseven import Run
|
|
51
|
+
|
|
52
|
+
with Run(project="my-project", name="experiment-1") as run:
|
|
53
|
+
run.log_config({"learning_rate": 0.001, "epochs": 10})
|
|
54
|
+
|
|
55
|
+
for epoch in range(10):
|
|
56
|
+
loss = train_one_epoch()
|
|
57
|
+
run.log_metrics({"loss": loss}, step=epoch)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## View Results
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
sixtyseven --logdir ~/.sixtyseven/logs
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Opens a dashboard at http://localhost:6767
|
|
67
|
+
|
|
68
|
+
## API
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
run.log_metrics({"loss": 0.5, "accuracy": 0.85}, step=epoch) # Log metrics
|
|
72
|
+
run.log_config({"lr": 0.001}) # Log config
|
|
73
|
+
run.add_tags(["baseline"]) # Add tags
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Environment Variables
|
|
77
|
+
|
|
78
|
+
| Variable | Description | Default |
|
|
79
|
+
| ------------------- | ------------------ | -------------------- |
|
|
80
|
+
| `SIXTYSEVEN_LOGDIR` | Where to save logs | `~/.sixtyseven/logs` |
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
sixseven-0.1.0.data/purelib/sixtyseven/__init__.py,sha256=ULLmUAn4APB0YFDG6RJogbPo5nV4i1ISDgWdkuo0Dz8,773
|
|
2
|
+
sixseven-0.1.0.data/purelib/sixtyseven/cli.py,sha256=Fmd3IpOuZleoZZwhp6gE8ja2jFTamK1wBQIRHttudv0,1634
|
|
3
|
+
sixseven-0.1.0.data/purelib/sixtyseven/client.py,sha256=DQNuZjfqjveSuvSKySBOICig76mcBBtNGZ-0MlogwCg,5583
|
|
4
|
+
sixseven-0.1.0.data/purelib/sixtyseven/config.py,sha256=SAPs3kIlPYQtuYxbu7zZG0-PFI_n0f_NncMYGQsq8uM,4942
|
|
5
|
+
sixseven-0.1.0.data/purelib/sixtyseven/exceptions.py,sha256=OOaouNgOhm2ki4rqoBs7Uf6L5XpoKEorGcEvZPUtF04,853
|
|
6
|
+
sixseven-0.1.0.data/purelib/sixtyseven/local.py,sha256=QHY-UJi7qIfd0f9NtulWBeS0OIDCEyRESiexNxCAgy8,10490
|
|
7
|
+
sixseven-0.1.0.data/purelib/sixtyseven/metrics.py,sha256=qPczP_5IHiY3fEY7oHVvRpASY-0cDe309wG6GmZah38,4311
|
|
8
|
+
sixseven-0.1.0.data/purelib/sixtyseven/run.py,sha256=S8tp_7i9upz9mZ_psfgDrEzuo3aTI8lEVn0VWNr0dUw,15554
|
|
9
|
+
sixseven-0.1.0.data/purelib/sixtyseven/server.py,sha256=mqi8wb41B4QjnCgMvBUKEroCw75jhRZT43KpNBNh_ds,13004
|
|
10
|
+
sixseven-0.1.0.data/purelib/sixtyseven/utils.py,sha256=R8LzO3JmrF3NV4gCva1kWGziBX506kQFC0ag-QqXEtI,4339
|
|
11
|
+
sixseven-0.1.0.dist-info/METADATA,sha256=6vDWYA4mrnTMh2GKV2L1z_Cwb2xuCJQrfEsN7hNc7Os,2572
|
|
12
|
+
sixseven-0.1.0.dist-info/WHEEL,sha256=xyqHPmSoHXK0MJUv3A-_FHIwrnfm3tpfRTgF0R83pbg,106
|
|
13
|
+
sixseven-0.1.0.dist-info/entry_points.txt,sha256=VpX3aOASz6suw0kLHKheyk7dkXtXoL82YjUWE8aocb8,51
|
|
14
|
+
sixseven-0.1.0.dist-info/top_level.txt,sha256=MIYYmA6idrsBSzSAmCOZIODFwGl-dbWBjcii3eMXfTY,11
|
|
15
|
+
sixseven-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sixtyseven
|