dataspires 0.1.0__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.
afrilink/__init__.py ADDED
@@ -0,0 +1,205 @@
1
+ """
2
+ DataSpires SDK — Train, finetune, and generate on GPUs from your notebook.
3
+
4
+ Quick start:
5
+ from dataspires import DataSpiresClient
6
+ client = DataSpiresClient()
7
+ client.authenticate()
8
+
9
+ ft = client.finetune(model="qwen2.5-0.5b", training_mode="low", data=my_dataset, gpus=1)
10
+ result = ft.run(wait=True)
11
+
12
+ User guide:
13
+ import dataspires
14
+ dataspires.docs("help")
15
+ """
16
+
17
+ __version__ = "0.1.0"
18
+
19
+ # Credentials provisioning (auto-runs in Colab)
20
+ from .credentials import (
21
+ provision_credentials,
22
+ get_cineca_credentials,
23
+ CredentialProvider,
24
+ OrgCredentials,
25
+ )
26
+
27
+ # Core authentication
28
+ from .auth import AfriLinkAuth, authenticate, AuthResult
29
+ from .cineca_auth import CinecaDirectAuth, authenticate_cineca, CinecaAuthResult
30
+
31
+ # Telemetry
32
+ from .telemetry import TelemetryClient, JobUsageTracker, track_cineca_jobs
33
+
34
+ # Finetuning API
35
+ from .finetune import finetune, FinetuneJob, FinetuneJobSpec, TrainingMode, TrainingConfig, SUPPORTED_BACKENDS, DEFAULT_BACKEND
36
+
37
+ # Generate API (frozen run)
38
+ from .generate import generate, GenerateJob, GenerateJobSpec, GENERATE_RUNNER_MODELS
39
+
40
+ # Pretrain API (managed recipes + architecture scratch)
41
+ from .pretrain import pretrain, PretrainJob, PretrainJobSpec
42
+
43
+ # Training API (deprecated — use pretrain)
44
+ from .train import train, TrainJob, TrainJobSpec, CONTAINER_REGISTRY
45
+
46
+ # Routing helpers
47
+ from .routing import KIND_REGISTRY, ARCHITECTURE_CATALOG, ArchitectureClass, get_kind
48
+
49
+ # SLURM job management
50
+ from .slurm import SlurmJobManager, SlurmConfig
51
+
52
+ # Distributed training
53
+ from .distributed import DistributedConfig, DistributedStrategy, get_optimal_strategy
54
+
55
+ # Model/dataset registry
56
+ from .registry import (
57
+ ModelRegistry,
58
+ ModelInfo,
59
+ DatasetInfo,
60
+ ModelSize,
61
+ ModelType,
62
+ get_registry,
63
+ list_models,
64
+ list_datasets,
65
+ )
66
+
67
+ # Vouchers
68
+ from .vouchers import generate_voucher, validate_voucher, generate_batch
69
+
70
+ # Data transfer
71
+ from .transfer import DataTransferManager, create_transfer_manager
72
+
73
+ # Session watchdog & recovery
74
+ from .watchdog import SessionWatchdog, EmailNotifier, JobRecoveryInfo
75
+
76
+ # HuggingFace inference
77
+ from .inference import HFInferenceClient, InferenceResult
78
+
79
+ # Custom-image build service (Cloud Build + Artifact Registry)
80
+ from .build import BuildSpec, ImageBuilder, ModelSource, PRESETS as BUILD_PRESETS
81
+ from .gcp_auth import GcpServiceAccountAuth
82
+
83
+ # Main client (recommended entry point)
84
+ from .client import AfriLinkClient, ClientConfig, create_client
85
+
86
+ # Public name for the same client class.
87
+ DataSpiresClient = AfriLinkClient
88
+
89
+ # Help / manual system — prefer: import afrilink; afrilink.docs("help")
90
+ from .help import help_system as _help_system, _resolve_topic as _resolve_help_topic
91
+
92
+ import sys as _sys
93
+ import types as _types
94
+
95
+
96
+ class _AfriLinkModule(_types.ModuleType):
97
+ """Module subclass that implements afrilink / \"topic\" slash help."""
98
+
99
+ def __truediv__(self, topic):
100
+ _help_system._show(_resolve_help_topic(topic))
101
+ return self
102
+
103
+ def docs(self, topic: str = "help"):
104
+ """Print an inline help page. Preferred over slash syntax."""
105
+ _help_system._show(topic)
106
+ return None
107
+
108
+ def __repr__(self):
109
+ return (
110
+ f"<module 'afrilink' v{__version__}> — "
111
+ 'run afrilink.docs("help") for the user guide.'
112
+ )
113
+
114
+
115
+ # Replace this module with the instrumented subclass so that
116
+ # `afrilink / "specs"` and `afrilink.docs("specs")` work at module level.
117
+ _current = _sys.modules[__name__]
118
+ _upgraded = _AfriLinkModule(__name__, __doc__)
119
+ _upgraded.__dict__.update({k: v for k, v in _current.__dict__.items()
120
+ if not k.startswith("_upgraded")})
121
+ _sys.modules[__name__] = _upgraded
122
+
123
+
124
+ __all__ = [
125
+ "__version__",
126
+ # Main client
127
+ "AfriLinkClient",
128
+ "DataSpiresClient",
129
+ "ClientConfig",
130
+ "create_client",
131
+ # Credentials
132
+ "provision_credentials",
133
+ "get_cineca_credentials",
134
+ "CredentialProvider",
135
+ "OrgCredentials",
136
+ # Authentication
137
+ "AfriLinkAuth",
138
+ "authenticate",
139
+ "AuthResult",
140
+ "CinecaDirectAuth",
141
+ "authenticate_cineca",
142
+ "CinecaAuthResult",
143
+ # Finetuning
144
+ "finetune",
145
+ "FinetuneJob",
146
+ "FinetuneJobSpec",
147
+ "TrainingMode",
148
+ "TrainingConfig",
149
+ "SUPPORTED_BACKENDS",
150
+ "DEFAULT_BACKEND",
151
+ # Generate (frozen run)
152
+ "generate",
153
+ "GenerateJob",
154
+ "GenerateJobSpec",
155
+ "GENERATE_RUNNER_MODELS",
156
+ # Pretrain (managed recipes + architecture scratch)
157
+ "pretrain",
158
+ "PretrainJob",
159
+ "PretrainJobSpec",
160
+ "KIND_REGISTRY",
161
+ "ARCHITECTURE_CATALOG",
162
+ "ArchitectureClass",
163
+ "get_kind",
164
+ # Training (deprecated — use pretrain)
165
+ "train",
166
+ "TrainJob",
167
+ "TrainJobSpec",
168
+ "CONTAINER_REGISTRY",
169
+ # SLURM
170
+ "SlurmJobManager",
171
+ "SlurmConfig",
172
+ # Distributed
173
+ "DistributedConfig",
174
+ "DistributedStrategy",
175
+ "get_optimal_strategy",
176
+ # Registry
177
+ "ModelRegistry",
178
+ "ModelInfo",
179
+ "DatasetInfo",
180
+ "ModelSize",
181
+ "ModelType",
182
+ "get_registry",
183
+ "list_models",
184
+ "list_datasets",
185
+ # Vouchers
186
+ "generate_voucher",
187
+ "validate_voucher",
188
+ "generate_batch",
189
+ # Transfer
190
+ "DataTransferManager",
191
+ "create_transfer_manager",
192
+ # Telemetry
193
+ "TelemetryClient",
194
+ "JobUsageTracker",
195
+ "track_cineca_jobs",
196
+ # Watchdog & recovery
197
+ "SessionWatchdog",
198
+ "EmailNotifier",
199
+ "JobRecoveryInfo",
200
+ # HuggingFace inference
201
+ "HFInferenceClient",
202
+ "InferenceResult",
203
+ # Help system
204
+ "help_system",
205
+ ]
afrilink/adapt.py ADDED
@@ -0,0 +1,60 @@
1
+ """
2
+ Deprecated adapt() — use pretrain(kind=..., pretrained=True).
3
+
4
+ Kept as a thin wrapper for one release; removed in 1.0.0.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import warnings
10
+ from typing import Any, Dict, Optional
11
+
12
+ from .train import TrainJob, TrainJobSpec
13
+
14
+ AdaptJob = TrainJob
15
+ AdaptJobSpec = TrainJobSpec
16
+
17
+
18
+ def adapt(
19
+ recipe: Optional[str] = None,
20
+ data: Any = None,
21
+ weights: Optional[str] = None,
22
+ model: Optional[str] = None,
23
+ data_config: Optional[str] = None,
24
+ params: Optional[Dict[str, Any]] = None,
25
+ gpus: int = 1,
26
+ time_limit: str = "04:00:00",
27
+ backend: str = None,
28
+ output_dir: str = None,
29
+ script: Optional[str] = None,
30
+ script_content: Optional[str] = None,
31
+ container: str = "afrilink-yolo",
32
+ **kwargs,
33
+ ) -> AdaptJob:
34
+ """Deprecated — use ``pretrain(kind=..., pretrained=True)``."""
35
+ warnings.warn(
36
+ "adapt() is deprecated in AfriLink SDK 0.9.0. "
37
+ "Use pretrain(kind=..., weights=..., data=..., pretrained=True) instead. "
38
+ "adapt() will be removed in 1.0.0.",
39
+ DeprecationWarning,
40
+ stacklevel=2,
41
+ )
42
+ from .pretrain import pretrain
43
+
44
+ return pretrain(
45
+ kind=recipe,
46
+ data=data,
47
+ weights=weights,
48
+ model=model,
49
+ data_config=data_config,
50
+ params=params,
51
+ pretrained=True,
52
+ gpus=gpus,
53
+ time_limit=time_limit,
54
+ backend=backend,
55
+ output_dir=output_dir,
56
+ script=script,
57
+ script_content=script_content,
58
+ container=container,
59
+ **kwargs,
60
+ )
@@ -0,0 +1,255 @@
1
+ """
2
+ AfriLink API-Key Auth — stateless SDK auth via Cloudflare Worker exchange.
3
+
4
+ Users mint API keys at https://dataspires.com/dashboard/profile and pass them
5
+ to the SDK either as the `api_key=` arg to `client.authenticate()` or via the
6
+ `AFRILINK_API_KEY` env var (Colab/Kaggle "Add a secret" UI).
7
+
8
+ The SDK hits `https://api.dataspires.com/v1/sdk-auth/exchange` with the key
9
+ in `Authorization: Bearer ...`, and receives back everything needed for the
10
+ session: Supabase JWT (for RLS-protected writes against `sessions` and
11
+ `deduct_credits`), Supabase project URL + anon key, and the shared A100 SSH
12
+ key (base64) which the SDK lands in a per-process tempfile at 0600.
13
+
14
+ Nothing persists. Kill the process and the SSH key tempfile + JWT in memory
15
+ are gone with it.
16
+
17
+ NOTE: v1 uses a single shared `dataspires` SSH key for the A100 across all
18
+ users. Per-user keys are planned as a follow-up so the A100's `last login`
19
+ audit reflects the actual DataSpires identity.
20
+ """
21
+
22
+ import base64
23
+ import json
24
+ import os
25
+ import urllib.error
26
+ import urllib.request
27
+ from dataclasses import dataclass
28
+ from typing import Optional
29
+
30
+
31
+ # The afrilink-sdk-auth Cloudflare Worker. Default points at the working
32
+ # .workers.dev URL; override via AFRILINK_AUTH_URL once api.dataspires.com
33
+ # is wired to its Custom Domain in the Cloudflare dashboard.
34
+ DEFAULT_EXCHANGE_URL = (
35
+ "https://afrilink-sdk-auth.sweet-star-2474.workers.dev/v1/sdk-auth/exchange"
36
+ )
37
+
38
+
39
+ @dataclass
40
+ class ApiKeyExchangeResult:
41
+ """Everything the SDK needs to run jobs after an API-key exchange."""
42
+ success: bool
43
+ error: Optional[str] = None
44
+ user_id: Optional[str] = None
45
+ email: Optional[str] = None
46
+ balance_usd: float = 0.0
47
+ supabase_jwt: Optional[str] = None
48
+ supabase_url: Optional[str] = None
49
+ supabase_anon_key: Optional[str] = None
50
+ a100_host: Optional[str] = None
51
+ a100_user: Optional[str] = None
52
+ a100_ssh_key_material: Optional[str] = None # decoded PEM, not base64
53
+ # GHCR creds for pulling private curated containers (org-owned)
54
+ ghcr_token: Optional[str] = None
55
+ ghcr_username: Optional[str] = None
56
+ # GCP build service bundle (Cloud Build + Artifact Registry on anadrome)
57
+ gcp_sa_key_json: Optional[str] = None # single-line JSON of the SA key
58
+ gcp_project: Optional[str] = None
59
+ gcp_region: Optional[str] = None
60
+ gcp_ar_repo: Optional[str] = None
61
+ gcp_build_bucket: Optional[str] = None
62
+ expires_at: Optional[int] = None
63
+
64
+
65
+ def _post_json(url: str, body: dict, headers: dict, timeout: int = 15) -> dict:
66
+ """Minimal HTTP POST with stdlib fallback so the SDK works without `requests`."""
67
+ try:
68
+ import requests
69
+ resp = requests.post(url, json=body, headers=headers, timeout=timeout)
70
+ try:
71
+ return {"status": resp.status_code, "body": resp.json()}
72
+ except Exception:
73
+ return {"status": resp.status_code, "body": {"raw": resp.text[:500]}}
74
+ except ImportError:
75
+ req = urllib.request.Request(
76
+ url,
77
+ data=json.dumps(body).encode("utf-8") if body else b"",
78
+ headers={**headers, "Content-Type": "application/json"},
79
+ method="POST",
80
+ )
81
+ try:
82
+ with urllib.request.urlopen(req, timeout=timeout) as r:
83
+ return {"status": r.status, "body": json.loads(r.read().decode("utf-8"))}
84
+ except urllib.error.HTTPError as e:
85
+ try:
86
+ return {"status": e.code, "body": json.loads(e.read().decode("utf-8"))}
87
+ except Exception:
88
+ return {"status": e.code, "body": {"error": str(e)}}
89
+
90
+
91
+ def resolve_api_key(explicit: Optional[str] = None) -> Optional[str]:
92
+ """
93
+ Resolution order:
94
+ 1. `explicit` argument
95
+ 2. API_KEY env / notebook secret
96
+ 3. AFRILINK_API_KEY env / notebook secret (legacy)
97
+ 4. None — caller must prompt
98
+ """
99
+ return resolve_secret("API_KEY", explicit=explicit) or resolve_secret(
100
+ "AFRILINK_API_KEY", explicit=None
101
+ )
102
+
103
+
104
+ def resolve_secret(name: str, explicit: Optional[str] = None) -> Optional[str]:
105
+ """
106
+ Generic secret resolver. Resolution order:
107
+ 1. `explicit` arg
108
+ 2. `os.environ[name]`
109
+ 3. Colab notebook secret (google.colab.userdata)
110
+ 4. Kaggle notebook secret (kaggle_secrets.UserSecretsClient)
111
+ 5. None
112
+
113
+ Used for AFRILINK_API_KEY, HUGGINGFACE_TOKEN, and other third-party
114
+ tokens users set as notebook secrets without exporting to os.environ.
115
+ """
116
+ if explicit:
117
+ return explicit.strip()
118
+ env = os.environ.get(name)
119
+ if env:
120
+ return env.strip()
121
+
122
+ # Colab
123
+ try:
124
+ from google.colab import userdata # type: ignore
125
+ try:
126
+ v = userdata.get(name)
127
+ if v:
128
+ # Mirror into os.environ so downstream SDK paths (subprocesses,
129
+ # docker -e flags, dataset loaders) see it without re-resolving.
130
+ os.environ[name] = v.strip()
131
+ return v.strip()
132
+ except Exception:
133
+ pass
134
+ except ImportError:
135
+ pass
136
+
137
+ # Kaggle
138
+ try:
139
+ from kaggle_secrets import UserSecretsClient # type: ignore
140
+ try:
141
+ v = UserSecretsClient().get_secret(name)
142
+ if v:
143
+ os.environ[name] = v.strip()
144
+ return v.strip()
145
+ except Exception:
146
+ # Kaggle raises if the secret isn't attached to the notebook;
147
+ # we want to fall through to the explicit-prompt path, not crash.
148
+ pass
149
+ except ImportError:
150
+ pass
151
+
152
+ return None
153
+
154
+
155
+ def exchange_api_key(
156
+ api_key: str,
157
+ exchange_url: str = DEFAULT_EXCHANGE_URL,
158
+ ) -> ApiKeyExchangeResult:
159
+ """
160
+ Exchange a plaintext API key for a session bundle.
161
+
162
+ Network failure → ApiKeyExchangeResult(success=False, error="…")
163
+ 401 → "Invalid or revoked API key"
164
+ Anything else → the Worker's `error` field
165
+ """
166
+ if not api_key or not api_key.startswith(("afk_live_", "afk_test_")):
167
+ return ApiKeyExchangeResult(
168
+ success=False,
169
+ error="Malformed API key (expected to start with afk_live_ or afk_test_)",
170
+ )
171
+ try:
172
+ resp = _post_json(
173
+ exchange_url,
174
+ body={},
175
+ headers={"Authorization": f"Bearer {api_key}"},
176
+ )
177
+ except Exception as e:
178
+ return ApiKeyExchangeResult(success=False, error=f"Network error: {e}")
179
+
180
+ status = resp.get("status", 0)
181
+ data = resp.get("body") or {}
182
+ if status != 200 or not data.get("ok"):
183
+ return ApiKeyExchangeResult(
184
+ success=False,
185
+ error=(data.get("error") or f"HTTP {status}"),
186
+ )
187
+
188
+ # Decode the SSH key from base64 into PEM material
189
+ key_b64 = data.get("a100_ssh_key_b64") or ""
190
+ try:
191
+ key_material = base64.b64decode(key_b64).decode("utf-8")
192
+ except Exception:
193
+ key_material = None
194
+
195
+ return ApiKeyExchangeResult(
196
+ success=True,
197
+ user_id=data.get("user_id"),
198
+ email=data.get("email"),
199
+ balance_usd=float(data.get("balance_usd") or 0),
200
+ supabase_jwt=data.get("supabase_jwt"),
201
+ supabase_url=data.get("supabase_url"),
202
+ supabase_anon_key=data.get("supabase_anon_key"),
203
+ a100_host=data.get("a100_host"),
204
+ a100_user=data.get("a100_user"),
205
+ a100_ssh_key_material=key_material,
206
+ ghcr_token=data.get("ghcr_token"),
207
+ ghcr_username=data.get("ghcr_username"),
208
+ gcp_sa_key_json=data.get("gcp_sa_key_json"),
209
+ gcp_project=data.get("gcp_project"),
210
+ gcp_region=data.get("gcp_region"),
211
+ gcp_ar_repo=data.get("gcp_ar_repo"),
212
+ gcp_build_bucket=data.get("gcp_build_bucket"),
213
+ expires_at=data.get("expires_at"),
214
+ )
215
+
216
+
217
+ # ── Convenience: a minimal AuthClient stand-in that the rest of the SDK
218
+ # (telemetry.SupabaseTelemetryWriter, JobUsageTracker) treats just like
219
+ # AfriLinkAuth. Same attribute surface, populated from the exchange.
220
+
221
+ class ApiKeyAuthClient:
222
+ """
223
+ Drop-in replacement for AfriLinkAuth when authenticating via API key.
224
+
225
+ Exposes the same attributes the rest of the SDK reads:
226
+ user_id, access_token, supabase_url, supabase_key, balance_usd,
227
+ is_authenticated
228
+ """
229
+
230
+ def __init__(self, result: ApiKeyExchangeResult, api_key: str = None, exchange_url: str = None):
231
+ self._result = result
232
+ self._api_key = api_key
233
+ self._exchange_url = exchange_url or DEFAULT_EXCHANGE_URL
234
+ self.user_id = result.user_id
235
+ self.email = result.email
236
+ self.access_token = result.supabase_jwt
237
+ self.supabase_url = result.supabase_url
238
+ self.supabase_key = result.supabase_anon_key
239
+ self.balance_usd = result.balance_usd
240
+
241
+ def refresh_access_token(self) -> bool:
242
+ """Re-exchange API key for a fresh Supabase JWT (billing writes)."""
243
+ if not self._api_key:
244
+ return False
245
+ result = exchange_api_key(self._api_key, exchange_url=self._exchange_url)
246
+ if not result.success or not result.supabase_jwt:
247
+ return False
248
+ self._result = result
249
+ self.access_token = result.supabase_jwt
250
+ self.balance_usd = result.balance_usd
251
+ return True
252
+
253
+ @property
254
+ def is_authenticated(self) -> bool:
255
+ return self._result.success and bool(self._result.supabase_jwt)