sixtyseven 0.1.0__cp312-cp312-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.
@@ -0,0 +1,36 @@
1
+ """
2
+ Sixtyseven - ML Experiment Tracking SDK
3
+
4
+ A Python SDK for tracking machine learning experiments with Sixtyseven.
5
+
6
+ Example usage:
7
+ from sixtyseven import Run
8
+
9
+ with Run(project="my-team/image-classifier") as run:
10
+ run.log_config({"learning_rate": 0.001})
11
+
12
+ for epoch in range(100):
13
+ loss = train()
14
+ run.log_metrics({"loss": loss}, step=epoch)
15
+ """
16
+
17
+ from sixtyseven.config import configure
18
+ from sixtyseven.exceptions import (
19
+ APIError,
20
+ AuthenticationError,
21
+ ServerError,
22
+ SixtySevenError,
23
+ ValidationError,
24
+ )
25
+ from sixtyseven.run import Run
26
+
27
+ __version__ = "0.1.0"
28
+ __all__ = [
29
+ "Run",
30
+ "configure",
31
+ "SixtySevenError",
32
+ "AuthenticationError",
33
+ "APIError",
34
+ "ValidationError",
35
+ "ServerError",
36
+ ]
@@ -0,0 +1,64 @@
1
+ """Console entrypoint for the bundled sixtyseven CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import platform
7
+ import shutil
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+
13
+ def _platform_id() -> Optional[str]:
14
+ system = platform.system().lower()
15
+ machine = platform.machine().lower()
16
+
17
+ if machine in {"x86_64", "amd64"}:
18
+ arch = "amd64"
19
+ elif machine in {"aarch64", "arm64"}:
20
+ arch = "arm64"
21
+ else:
22
+ return None
23
+
24
+ if system == "darwin":
25
+ return f"darwin-{arch}"
26
+ if system == "linux":
27
+ return f"linux-{arch}"
28
+ if system == "windows":
29
+ return f"windows-{arch}"
30
+ return None
31
+
32
+
33
+ def _bundled_binary_path() -> Optional[str]:
34
+ platform_id = _platform_id()
35
+ if not platform_id:
36
+ return None
37
+
38
+ binary_name = "sixtyseven.exe" if platform.system() == "Windows" else "sixtyseven"
39
+ base_dir = Path(__file__).resolve().parent
40
+ candidate = base_dir / "bin" / platform_id / binary_name
41
+ if candidate.is_file() and os.access(candidate, os.X_OK):
42
+ return str(candidate)
43
+ return None
44
+
45
+
46
+ def _find_binary() -> Optional[str]:
47
+ bundled = _bundled_binary_path()
48
+ if bundled:
49
+ return bundled
50
+
51
+ binary_name = "sixtyseven.exe" if platform.system() == "Windows" else "sixtyseven"
52
+ return shutil.which(binary_name)
53
+
54
+
55
+ def main() -> None:
56
+ binary = _find_binary()
57
+ if not binary:
58
+ print(
59
+ "Could not find 'sixtyseven' binary. Reinstall the package or set SIXTYSEVEN_BINARY.",
60
+ file=sys.stderr,
61
+ )
62
+ raise SystemExit(1)
63
+
64
+ os.execv(binary, [binary, *sys.argv[1:]])
@@ -0,0 +1,190 @@
1
+ """HTTP client for communicating with the Sixtyseven API."""
2
+
3
+ import time
4
+ from typing import Any, Dict, List, Optional
5
+ from urllib.parse import urljoin
6
+
7
+ import requests
8
+
9
+ from sixtyseven.config import SDKConfig
10
+ from sixtyseven.exceptions import APIError, AuthenticationError
11
+
12
+
13
+ class SixtySevenClient:
14
+ """HTTP client for the Sixtyseven API."""
15
+
16
+ def __init__(self, config: SDKConfig, api_key: Optional[str] = None):
17
+ """
18
+ Initialize the client.
19
+
20
+ Args:
21
+ config: SDK configuration
22
+ api_key: API key (overrides config)
23
+ """
24
+ self.config = config
25
+ self.api_key = api_key or config.api_key
26
+ self.session = requests.Session()
27
+
28
+ if self.api_key:
29
+ self.session.headers["Authorization"] = f"Bearer {self.api_key}"
30
+
31
+ self.session.headers["Content-Type"] = "application/json"
32
+ self.session.headers["User-Agent"] = "sixtyseven-python/0.1.0"
33
+
34
+ def _url(self, path: str) -> str:
35
+ """Build full URL for an API endpoint."""
36
+ return urljoin(self.config.base_url, f"/api/v1{path}")
37
+
38
+ def _request(
39
+ self,
40
+ method: str,
41
+ path: str,
42
+ data: Optional[Dict[str, Any]] = None,
43
+ params: Optional[Dict[str, Any]] = None,
44
+ ) -> Dict[str, Any]:
45
+ """
46
+ Make an HTTP request with retry logic.
47
+
48
+ Args:
49
+ method: HTTP method
50
+ path: API path
51
+ data: Request body data
52
+ params: Query parameters
53
+
54
+ Returns:
55
+ Response JSON data
56
+
57
+ Raises:
58
+ AuthenticationError: If authentication fails
59
+ APIError: If the request fails
60
+ """
61
+ url = self._url(path)
62
+ last_error = None
63
+
64
+ for attempt in range(self.config.retry_count):
65
+ try:
66
+ response = self.session.request(
67
+ method=method,
68
+ url=url,
69
+ json=data,
70
+ params=params,
71
+ timeout=self.config.timeout,
72
+ )
73
+
74
+ if response.status_code == 401:
75
+ raise AuthenticationError("Invalid API key")
76
+
77
+ if response.status_code == 403:
78
+ raise AuthenticationError("Insufficient permissions")
79
+
80
+ if response.status_code >= 400:
81
+ error_data = response.json() if response.text else {}
82
+ raise APIError(
83
+ error_data.get(
84
+ "error",
85
+ f"Request failed with status {response.status_code}",
86
+ ),
87
+ status_code=response.status_code,
88
+ response=error_data,
89
+ )
90
+
91
+ if response.text:
92
+ return response.json()
93
+ return {}
94
+
95
+ except requests.exceptions.RequestException as e:
96
+ last_error = e
97
+ if attempt < self.config.retry_count - 1:
98
+ time.sleep(self.config.retry_delay * (attempt + 1))
99
+ continue
100
+
101
+ raise APIError(
102
+ f"Request failed after {self.config.retry_count} attempts: {last_error}"
103
+ )
104
+
105
+ def create_run(
106
+ self,
107
+ team_slug: str,
108
+ app_slug: str,
109
+ name: Optional[str] = None,
110
+ tags: Optional[List[str]] = None,
111
+ config: Optional[Dict[str, Any]] = None,
112
+ git_info: Optional[Dict[str, Any]] = None,
113
+ system_info: Optional[Dict[str, Any]] = None,
114
+ ) -> str:
115
+ """
116
+ Create a new run.
117
+
118
+ Returns:
119
+ The run ID
120
+ """
121
+ data = {
122
+ "name": name,
123
+ "tags": tags or [],
124
+ "config": config or {},
125
+ }
126
+
127
+ if git_info:
128
+ data["git_info"] = git_info
129
+ if system_info:
130
+ data["system_info"] = system_info
131
+
132
+ response = self._request(
133
+ "POST",
134
+ f"/teams/{team_slug}/apps/{app_slug}/runs",
135
+ data=data,
136
+ )
137
+
138
+ return response["id"]
139
+
140
+ def update_run_status(
141
+ self,
142
+ run_id: str,
143
+ status: str,
144
+ error: Optional[str] = None,
145
+ ) -> None:
146
+ """Update run status (completed, failed, aborted)."""
147
+ data = {"status": status}
148
+ if error:
149
+ data["error_message"] = error
150
+
151
+ self._request("PUT", f"/runs/{run_id}/status", data=data)
152
+
153
+ def update_run_config(self, run_id: str, config: Dict[str, Any]) -> None:
154
+ """Merge new config with existing run config."""
155
+ self._request("PUT", f"/runs/{run_id}/config", data=config)
156
+
157
+ def batch_log_metrics(
158
+ self,
159
+ run_id: str,
160
+ metrics: List[Dict[str, Any]],
161
+ ) -> None:
162
+ """
163
+ Log a batch of metrics.
164
+
165
+ Args:
166
+ run_id: The run ID
167
+ metrics: List of metric dictionaries with name, value, step, timestamp
168
+ """
169
+ self._request(
170
+ "POST",
171
+ f"/runs/{run_id}/metrics",
172
+ data={"metrics": metrics},
173
+ )
174
+
175
+ def add_run_tags(self, run_id: str, tags: List[str]) -> None:
176
+ """Add tags to a run."""
177
+ # This would need to be implemented via the update endpoint
178
+ pass
179
+
180
+ def upload_artifact(
181
+ self,
182
+ run_id: str,
183
+ path: str,
184
+ name: Optional[str] = None,
185
+ metadata: Optional[Dict[str, Any]] = None,
186
+ ) -> None:
187
+ """Upload an artifact file."""
188
+ # Artifact upload would need multipart form handling
189
+ # For now, this is a placeholder
190
+ pass
@@ -0,0 +1,161 @@
1
+ """Configuration management for the Sixtyseven SDK."""
2
+
3
+ import os
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Literal, Optional
7
+
8
+
9
+ def _default_logdir() -> str:
10
+ """Get the default log directory based on platform."""
11
+ import platform
12
+
13
+ home = Path.home()
14
+ system = platform.system()
15
+
16
+ if system == "Darwin":
17
+ return str(home / "Library" / "Application Support" / "sixtyseven" / "logs")
18
+ elif system == "Windows":
19
+ appdata = os.environ.get("LOCALAPPDATA", str(home / "AppData" / "Local"))
20
+ return str(Path(appdata) / "sixtyseven" / "logs")
21
+ else: # Linux and others
22
+ xdg_data = os.environ.get("XDG_DATA_HOME", str(home / ".local" / "share"))
23
+ return str(Path(xdg_data) / "sixtyseven" / "logs")
24
+
25
+
26
+ @dataclass
27
+ class SDKConfig:
28
+ """SDK configuration settings."""
29
+
30
+ # Mode: "local" or "remote"
31
+ mode: Literal["local", "remote"] = "local"
32
+
33
+ # Remote mode settings
34
+ base_url: str = "http://localhost:8080"
35
+ api_key: Optional[str] = None
36
+
37
+ # Local mode settings
38
+ logdir: str = field(default_factory=_default_logdir)
39
+
40
+ # Common settings
41
+ batch_size: int = 100
42
+ flush_interval: float = 5.0
43
+ capture_git: bool = True
44
+ capture_system: bool = True
45
+
46
+ # Remote-only settings
47
+ timeout: int = 30
48
+ retry_count: int = 3
49
+ retry_delay: float = 1.0
50
+
51
+
52
+ # Global configuration instance
53
+ _config = SDKConfig()
54
+
55
+
56
+ def configure(
57
+ mode: Optional[Literal["local", "remote"]] = None,
58
+ base_url: Optional[str] = None,
59
+ api_key: Optional[str] = None,
60
+ logdir: Optional[str] = None,
61
+ batch_size: Optional[int] = None,
62
+ flush_interval: Optional[float] = None,
63
+ capture_git: Optional[bool] = None,
64
+ capture_system: Optional[bool] = None,
65
+ timeout: Optional[int] = None,
66
+ retry_count: Optional[int] = None,
67
+ retry_delay: Optional[float] = None,
68
+ ) -> SDKConfig:
69
+ """
70
+ Configure the SDK globally.
71
+
72
+ Args:
73
+ mode: Operating mode ("local" for file-based, "remote" for API server)
74
+ base_url: Base URL for the Sixtyseven API server (remote mode)
75
+ api_key: API key for authentication (remote mode)
76
+ logdir: Directory for storing logs (local mode)
77
+ batch_size: Number of metrics to batch before sending/writing
78
+ flush_interval: Seconds between automatic flushes
79
+ capture_git: Whether to capture git information
80
+ capture_system: Whether to capture system information
81
+ timeout: Request timeout in seconds (remote mode)
82
+ retry_count: Number of retries for failed requests (remote mode)
83
+ retry_delay: Delay between retries in seconds (remote mode)
84
+
85
+ Returns:
86
+ The updated configuration
87
+
88
+ Example (local mode - default):
89
+ from sixtyseven import configure
90
+
91
+ configure(logdir="./logs") # Use local file storage
92
+
93
+ Example (remote mode):
94
+ from sixtyseven import configure
95
+
96
+ configure(
97
+ mode="remote",
98
+ base_url="https://api.sixtyseven.ai",
99
+ api_key="ss67_xxxx",
100
+ )
101
+ """
102
+ global _config
103
+
104
+ if mode is not None:
105
+ _config.mode = mode
106
+ if base_url is not None:
107
+ _config.base_url = base_url
108
+ if api_key is not None:
109
+ _config.api_key = api_key
110
+ if logdir is not None:
111
+ _config.logdir = logdir
112
+ if batch_size is not None:
113
+ _config.batch_size = batch_size
114
+ if flush_interval is not None:
115
+ _config.flush_interval = flush_interval
116
+ if capture_git is not None:
117
+ _config.capture_git = capture_git
118
+ if capture_system is not None:
119
+ _config.capture_system = capture_system
120
+ if timeout is not None:
121
+ _config.timeout = timeout
122
+ if retry_count is not None:
123
+ _config.retry_count = retry_count
124
+ if retry_delay is not None:
125
+ _config.retry_delay = retry_delay
126
+
127
+ return _config
128
+
129
+
130
+ def _detect_mode() -> Literal["local", "remote"]:
131
+ """
132
+ Auto-detect the operating mode based on environment variables.
133
+
134
+ Priority:
135
+ 1. SIXTYSEVEN_LOGDIR set -> local mode
136
+ 2. SIXTYSEVEN_URL or SIXTYSEVEN_API_KEY set -> remote mode
137
+ 3. Default -> local mode (zero config experience)
138
+ """
139
+ if os.environ.get("SIXTYSEVEN_LOGDIR"):
140
+ return "local"
141
+ if os.environ.get("SIXTYSEVEN_URL") or os.environ.get("SIXTYSEVEN_API_KEY"):
142
+ return "remote"
143
+ return "local"
144
+
145
+
146
+ def get_config() -> SDKConfig:
147
+ """Get the current SDK configuration with environment variable overrides."""
148
+ global _config
149
+
150
+ # Auto-detect mode from environment
151
+ _config.mode = _detect_mode()
152
+
153
+ # Override with environment variables
154
+ if os.environ.get("SIXTYSEVEN_LOGDIR"):
155
+ _config.logdir = os.environ["SIXTYSEVEN_LOGDIR"]
156
+ if os.environ.get("SIXTYSEVEN_URL"):
157
+ _config.base_url = os.environ["SIXTYSEVEN_URL"]
158
+ if os.environ.get("SIXTYSEVEN_API_KEY"):
159
+ _config.api_key = os.environ["SIXTYSEVEN_API_KEY"]
160
+
161
+ return _config
@@ -0,0 +1,40 @@
1
+ """Custom exceptions for the Sixtyseven SDK."""
2
+
3
+
4
+ class SixtySevenError(Exception):
5
+ """Base exception for Sixtyseven SDK errors."""
6
+
7
+ pass
8
+
9
+
10
+ class AuthenticationError(SixtySevenError):
11
+ """Raised when authentication fails."""
12
+
13
+ pass
14
+
15
+
16
+ class APIError(SixtySevenError):
17
+ """Raised when an API request fails."""
18
+
19
+ def __init__(self, message: str, status_code: int = None, response: dict = None):
20
+ super().__init__(message)
21
+ self.status_code = status_code
22
+ self.response = response
23
+
24
+
25
+ class ValidationError(SixtySevenError):
26
+ """Raised when validation fails."""
27
+
28
+ pass
29
+
30
+
31
+ class ConnectionError(SixtySevenError):
32
+ """Raised when connection to the server fails."""
33
+
34
+ pass
35
+
36
+
37
+ class ServerError(SixtySevenError):
38
+ """Raised when server management fails (e.g., binary not found, failed to start)."""
39
+
40
+ pass