graviton-sdk 0.1.0__tar.gz
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,30 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
.pytest_cache/
|
|
4
|
+
*.pyc
|
|
5
|
+
*.pyo
|
|
6
|
+
*.pyd
|
|
7
|
+
.Python
|
|
8
|
+
env/
|
|
9
|
+
.venv/
|
|
10
|
+
|
|
11
|
+
# VS Code
|
|
12
|
+
.vscode/
|
|
13
|
+
|
|
14
|
+
# Build artifacts
|
|
15
|
+
artifacts/
|
|
16
|
+
infra/gcp/.terraform/
|
|
17
|
+
infra/cloudflare/.terraform/
|
|
18
|
+
|
|
19
|
+
# IDE
|
|
20
|
+
.idea/
|
|
21
|
+
|
|
22
|
+
# Mock GCS bucket
|
|
23
|
+
.mock_gcs/
|
|
24
|
+
|
|
25
|
+
# Secrets
|
|
26
|
+
.secrets
|
|
27
|
+
plan.out
|
|
28
|
+
|
|
29
|
+
# infra state
|
|
30
|
+
infra/gcp/current_state/projects/779429570375/SecretManagerSecret
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: graviton-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Graviton Supercompute
|
|
5
|
+
Project-URL: Homepage, https://www.g-qt.com
|
|
6
|
+
Project-URL: Repository, https://github.com/CodeZeroLabs/graviton
|
|
7
|
+
Author-email: CodeZero Labs <aaron@g-qt.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Requires-Dist: ipython
|
|
11
|
+
Requires-Dist: requests
|
|
12
|
+
Requires-Dist: sympy>=1.12
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Graviton Python SDK
|
|
16
|
+
|
|
17
|
+
The Graviton SDK provides a seamless way to connect your local mathematical models (built with SymPy) to the Graviton Supercompute platform.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install graviton-sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start (Jupyter/Colab)
|
|
26
|
+
|
|
27
|
+
The SDK is designed to work beautifully in interactive environments.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import sympy as sp
|
|
31
|
+
import graviton as gv
|
|
32
|
+
|
|
33
|
+
# 1. Authenticate (Use the public evaluation key for prototyping)
|
|
34
|
+
gv.login('a-little-more-art-than-science')
|
|
35
|
+
|
|
36
|
+
# 2. Define a complex model
|
|
37
|
+
x, y = sp.symbols('x y')
|
|
38
|
+
f = sp.sin(x)**2 + sp.exp(-y) * sp.cos(x)
|
|
39
|
+
|
|
40
|
+
# 3. Display with the Graviton "Compute" button
|
|
41
|
+
gv.expr(f)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Programmatic Workflow
|
|
45
|
+
|
|
46
|
+
For advanced integrations, you can use the flattened API to evaluate and compute programmatically.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
# 1. Evaluate to get JSON data
|
|
50
|
+
eval_data = gv.evaluate(f, options={"type": "JSON"})
|
|
51
|
+
|
|
52
|
+
# 2. Use flattened access for cost and ID
|
|
53
|
+
cost = eval_data['cost']
|
|
54
|
+
job_id = eval_data['id']
|
|
55
|
+
|
|
56
|
+
print(f"Estimated Cost: {cost:.2f} credits")
|
|
57
|
+
|
|
58
|
+
# 3. Compute using the ID directly
|
|
59
|
+
job = gv.compute(job_id, options={"auto_charge": True})
|
|
60
|
+
result = job.wait()
|
|
61
|
+
print(f"Result: {result}")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## How it Works
|
|
65
|
+
|
|
66
|
+
When you call `gv(formula)`, the SDK:
|
|
67
|
+
1. Serializes the SymPy object into a structured `srepr` format.
|
|
68
|
+
2. Generates a rich HTML representation with a LaTeX preview.
|
|
69
|
+
3. Adds a secure button that opens the Graviton Dashboard with your formula pre-loaded.
|
|
70
|
+
|
|
71
|
+
This allows you to leverage Graviton's massive cloud compute resources for complexity analysis and evaluation without ever leaving your research environment.
|
|
72
|
+
|
|
73
|
+
## Security
|
|
74
|
+
|
|
75
|
+
Graviton follows a "Data, not Code" security model. The SDK only transmits the mathematical structure of your model, not arbitrary Python code. Your local environment remains isolated, and all authentication is handled securely via the Graviton web platform.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Graviton Python SDK
|
|
2
|
+
|
|
3
|
+
The Graviton SDK provides a seamless way to connect your local mathematical models (built with SymPy) to the Graviton Supercompute platform.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install graviton-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start (Jupyter/Colab)
|
|
12
|
+
|
|
13
|
+
The SDK is designed to work beautifully in interactive environments.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import sympy as sp
|
|
17
|
+
import graviton as gv
|
|
18
|
+
|
|
19
|
+
# 1. Authenticate (Use the public evaluation key for prototyping)
|
|
20
|
+
gv.login('a-little-more-art-than-science')
|
|
21
|
+
|
|
22
|
+
# 2. Define a complex model
|
|
23
|
+
x, y = sp.symbols('x y')
|
|
24
|
+
f = sp.sin(x)**2 + sp.exp(-y) * sp.cos(x)
|
|
25
|
+
|
|
26
|
+
# 3. Display with the Graviton "Compute" button
|
|
27
|
+
gv.expr(f)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Programmatic Workflow
|
|
31
|
+
|
|
32
|
+
For advanced integrations, you can use the flattened API to evaluate and compute programmatically.
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
# 1. Evaluate to get JSON data
|
|
36
|
+
eval_data = gv.evaluate(f, options={"type": "JSON"})
|
|
37
|
+
|
|
38
|
+
# 2. Use flattened access for cost and ID
|
|
39
|
+
cost = eval_data['cost']
|
|
40
|
+
job_id = eval_data['id']
|
|
41
|
+
|
|
42
|
+
print(f"Estimated Cost: {cost:.2f} credits")
|
|
43
|
+
|
|
44
|
+
# 3. Compute using the ID directly
|
|
45
|
+
job = gv.compute(job_id, options={"auto_charge": True})
|
|
46
|
+
result = job.wait()
|
|
47
|
+
print(f"Result: {result}")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## How it Works
|
|
51
|
+
|
|
52
|
+
When you call `gv(formula)`, the SDK:
|
|
53
|
+
1. Serializes the SymPy object into a structured `srepr` format.
|
|
54
|
+
2. Generates a rich HTML representation with a LaTeX preview.
|
|
55
|
+
3. Adds a secure button that opens the Graviton Dashboard with your formula pre-loaded.
|
|
56
|
+
|
|
57
|
+
This allows you to leverage Graviton's massive cloud compute resources for complexity analysis and evaluation without ever leaving your research environment.
|
|
58
|
+
|
|
59
|
+
## Security
|
|
60
|
+
|
|
61
|
+
Graviton follows a "Data, not Code" security model. The SDK only transmits the mathematical structure of your model, not arbitrary Python code. Your local environment remains isolated, and all authentication is handled securely via the Graviton web platform.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "graviton-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for Graviton Supercompute"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "CodeZero Labs", email = "aaron@g-qt.com"},
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"sympy>=1.12",
|
|
17
|
+
"IPython",
|
|
18
|
+
"requests",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://www.g-qt.com"
|
|
23
|
+
Repository = "https://github.com/CodeZeroLabs/graviton"
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["src/graviton"]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import requests
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
import sympy as sp
|
|
6
|
+
|
|
7
|
+
# Internal Constants
|
|
8
|
+
CONFIG_PATH = os.path.expanduser("~/.graviton/config.json")
|
|
9
|
+
|
|
10
|
+
class GravitonSession:
|
|
11
|
+
"""
|
|
12
|
+
Manages the authenticated state, API configuration, and user status
|
|
13
|
+
for the current SDK session.
|
|
14
|
+
"""
|
|
15
|
+
_instance = None
|
|
16
|
+
|
|
17
|
+
def __new__(cls):
|
|
18
|
+
if cls._instance is None:
|
|
19
|
+
cls._instance = super(GravitonSession, cls).__new__(cls)
|
|
20
|
+
cls._instance._initialized = False
|
|
21
|
+
return cls._instance
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
if self._initialized:
|
|
25
|
+
return
|
|
26
|
+
self.api_key: Optional[str] = None
|
|
27
|
+
self.api_url: str = self._detect_api_url()
|
|
28
|
+
self.user_profile: Optional[Dict[str, Any]] = None
|
|
29
|
+
self._load_config()
|
|
30
|
+
self._initialized = True
|
|
31
|
+
|
|
32
|
+
def _detect_api_url(self) -> str:
|
|
33
|
+
if os.environ.get("GRAVITON_API_URL"):
|
|
34
|
+
return os.environ["GRAVITON_API_URL"].rstrip("/")
|
|
35
|
+
|
|
36
|
+
# Default to production-style URL for consistency.
|
|
37
|
+
# Inside the Docker swarm, this resolves to the Nginx gateway.
|
|
38
|
+
# In a host browser, it requires /etc/hosts (optional now with browse_web.py).
|
|
39
|
+
return "https://api.g-qt.com"
|
|
40
|
+
|
|
41
|
+
def _load_config(self):
|
|
42
|
+
if os.path.exists(CONFIG_PATH):
|
|
43
|
+
try:
|
|
44
|
+
with open(CONFIG_PATH, 'r') as f:
|
|
45
|
+
config = json.load(f)
|
|
46
|
+
self.api_key = config.get("api_key")
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def save_config(self, api_key: str):
|
|
51
|
+
self.api_key = api_key
|
|
52
|
+
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
|
53
|
+
with open(CONFIG_PATH, 'w') as f:
|
|
54
|
+
json.dump({"api_key": api_key}, f)
|
|
55
|
+
|
|
56
|
+
def get_dashboard_url(self) -> str:
|
|
57
|
+
"""Returns the correct dashboard URL for the current environment."""
|
|
58
|
+
# Always use the production URL. Routing is handled by the swarm DNS or host setup.
|
|
59
|
+
return "https://www.g-qt.com/dashboard.html"
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def verify(self) -> bool:
|
|
63
|
+
"""Tolerates self-signed certificates in local dev environment."""
|
|
64
|
+
return False if os.environ.get("REGION") == "local-dev" else True
|
|
65
|
+
|
|
66
|
+
def get_status(self) -> Dict[str, Any]:
|
|
67
|
+
"""Fetches the latest user credits and payment info from Shannon."""
|
|
68
|
+
if not self.api_key:
|
|
69
|
+
return {"authenticated": False}
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
resp = requests.get(
|
|
73
|
+
f"{self.api_url}/user",
|
|
74
|
+
headers={"X-API-KEY": self.api_key},
|
|
75
|
+
timeout=5,
|
|
76
|
+
verify=self.verify
|
|
77
|
+
)
|
|
78
|
+
if resp.status_code == 200:
|
|
79
|
+
self.user_profile = resp.json()
|
|
80
|
+
return {
|
|
81
|
+
"authenticated": True,
|
|
82
|
+
"credits": self.user_profile.get("credits", 0),
|
|
83
|
+
"has_payment_method": bool(self.user_profile.get("payment_methods", {}).get("primary")),
|
|
84
|
+
"profile": self.user_profile
|
|
85
|
+
}
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
return {"authenticated": False, "error": "Connection failed"}
|
|
89
|
+
|
|
90
|
+
# Global Session Instance
|
|
91
|
+
session = GravitonSession()
|
|
92
|
+
|
|
93
|
+
def login(api_key: str):
|
|
94
|
+
"""
|
|
95
|
+
Authenticates the SDK with your Graviton API Key.
|
|
96
|
+
"""
|
|
97
|
+
session.save_config(api_key)
|
|
98
|
+
print("✅ Authenticated with Graviton.")
|
|
99
|
+
|
|
100
|
+
def evaluate(expr: sp.Expr, options: Optional[dict] = None):
|
|
101
|
+
"""
|
|
102
|
+
Evaluates a SymPy expression on Graviton.
|
|
103
|
+
"""
|
|
104
|
+
from .main import gv
|
|
105
|
+
return gv(expr).evaluate(options=options)
|
|
106
|
+
|
|
107
|
+
def compute(expr_or_id: Any, options: Optional[Any] = None):
|
|
108
|
+
"""
|
|
109
|
+
Directly executes a compute job.
|
|
110
|
+
Accepts either a SymPy expression or a job_id string.
|
|
111
|
+
Returns a GravitonJob handle.
|
|
112
|
+
"""
|
|
113
|
+
from .main import gv, GravitonExpression
|
|
114
|
+
if isinstance(options, bool):
|
|
115
|
+
options = {"auto_charge": options}
|
|
116
|
+
|
|
117
|
+
if isinstance(expr_or_id, str):
|
|
118
|
+
auto_charge = options.get("auto_charge", False) if isinstance(options, dict) else False
|
|
119
|
+
return GravitonExpression._trigger_compute(expr_or_id, auto_charge=auto_charge)
|
|
120
|
+
|
|
121
|
+
return gv(expr_or_id).compute(options=options)
|
|
122
|
+
|
|
123
|
+
def status(job_id: str):
|
|
124
|
+
"""Checks the status of a specific job."""
|
|
125
|
+
from .main import GravitonJob
|
|
126
|
+
return GravitonJob(job_id).status()
|
|
127
|
+
|
|
128
|
+
def result(job_id: str):
|
|
129
|
+
"""Wait for and return the result of a specific job."""
|
|
130
|
+
from .main import GravitonJob
|
|
131
|
+
return GravitonJob(job_id).result()
|
|
132
|
+
|
|
133
|
+
# Legacy alias for backward compatibility
|
|
134
|
+
from .main import gv, expr
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import sympy as sp
|
|
2
|
+
from IPython.display import display, HTML
|
|
3
|
+
import requests
|
|
4
|
+
import uuid
|
|
5
|
+
import time
|
|
6
|
+
import json
|
|
7
|
+
from typing import Optional, Dict, Any
|
|
8
|
+
|
|
9
|
+
class GravitonJob:
|
|
10
|
+
"""
|
|
11
|
+
A handle for a Graviton compute job.
|
|
12
|
+
Allows for polling status and retrieving results asynchronously.
|
|
13
|
+
"""
|
|
14
|
+
def __init__(self, job_id: str):
|
|
15
|
+
self.job_id = job_id
|
|
16
|
+
self._data = None
|
|
17
|
+
|
|
18
|
+
def status(self) -> str:
|
|
19
|
+
"""Checks the current status of the job."""
|
|
20
|
+
from . import session
|
|
21
|
+
try:
|
|
22
|
+
resp = requests.get(
|
|
23
|
+
f"{session.api_url}/results/{self.job_id}",
|
|
24
|
+
headers={"X-API-KEY": session.api_key},
|
|
25
|
+
timeout=5,
|
|
26
|
+
verify=session.verify
|
|
27
|
+
)
|
|
28
|
+
if resp.status_code == 200:
|
|
29
|
+
self._data = resp.json()
|
|
30
|
+
return self._data.get("status", "UNKNOWN")
|
|
31
|
+
except Exception as e:
|
|
32
|
+
return f"ERROR: {str(e)}"
|
|
33
|
+
return "UNKNOWN"
|
|
34
|
+
|
|
35
|
+
def result(self, block: bool = True, poll_interval: float = 1.0) -> Any:
|
|
36
|
+
"""
|
|
37
|
+
Retrieves the job result. If block=True, polls until completed.
|
|
38
|
+
"""
|
|
39
|
+
from . import session
|
|
40
|
+
|
|
41
|
+
if block:
|
|
42
|
+
while True:
|
|
43
|
+
s = self.status()
|
|
44
|
+
if s == "COMPLETED":
|
|
45
|
+
break
|
|
46
|
+
if s in ["FAILED", "CANCELLED", "ERROR"]:
|
|
47
|
+
raise RuntimeError(f"Job {self.job_id} failed with status: {s}")
|
|
48
|
+
time.sleep(poll_interval)
|
|
49
|
+
|
|
50
|
+
# Download results
|
|
51
|
+
try:
|
|
52
|
+
resp = requests.get(
|
|
53
|
+
f"{session.api_url}/results/{self.job_id}/download",
|
|
54
|
+
headers={"X-API-KEY": session.api_key},
|
|
55
|
+
timeout=10,
|
|
56
|
+
verify=session.verify
|
|
57
|
+
)
|
|
58
|
+
if resp.status_code == 200:
|
|
59
|
+
# Attempt to parse as JSON if it looks like it
|
|
60
|
+
try:
|
|
61
|
+
return resp.json()
|
|
62
|
+
except:
|
|
63
|
+
return resp.text
|
|
64
|
+
else:
|
|
65
|
+
raise RuntimeError(f"Failed to download results: {resp.status_code}")
|
|
66
|
+
except Exception as e:
|
|
67
|
+
raise RuntimeError(f"Result retrieval failed: {e}")
|
|
68
|
+
|
|
69
|
+
def wait(self, poll_interval: float = 1.0) -> Any:
|
|
70
|
+
"""Alias for result(block=True)."""
|
|
71
|
+
return self.result(block=True, poll_interval=poll_interval)
|
|
72
|
+
|
|
73
|
+
class GravitonEvaluation(dict):
|
|
74
|
+
"""
|
|
75
|
+
A dictionary-like object that wraps the Graviton evaluation response
|
|
76
|
+
and provides flattened access to common fields like cost and ID.
|
|
77
|
+
"""
|
|
78
|
+
@property
|
|
79
|
+
def id(self) -> str:
|
|
80
|
+
return super().get('id', '')
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def cost(self) -> float:
|
|
84
|
+
return super().get('cost', {}).get('total_credit_cost', 0.0)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def estimated_time(self) -> float:
|
|
88
|
+
return super().get('estimated_duration_s', 0.1)
|
|
89
|
+
|
|
90
|
+
def __getitem__(self, key):
|
|
91
|
+
# Support flattened access for 'cost' and 'id'
|
|
92
|
+
if key == 'cost':
|
|
93
|
+
return self.cost
|
|
94
|
+
if key == 'id':
|
|
95
|
+
return self.id
|
|
96
|
+
return super().__getitem__(key)
|
|
97
|
+
|
|
98
|
+
def get(self, key, default=None):
|
|
99
|
+
if key == 'cost':
|
|
100
|
+
return self.cost
|
|
101
|
+
if key == 'id':
|
|
102
|
+
return self.id
|
|
103
|
+
return super().get(key, default)
|
|
104
|
+
|
|
105
|
+
class GravitonExpression:
|
|
106
|
+
"""
|
|
107
|
+
A wrapper for SymPy expressions that provides interactive evaluation
|
|
108
|
+
and compute capabilities within notebooks.
|
|
109
|
+
"""
|
|
110
|
+
def __init__(self, expr):
|
|
111
|
+
self.expr = expr
|
|
112
|
+
self.evaluation_result = None
|
|
113
|
+
self.error = None
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def cost(self) -> float:
|
|
117
|
+
"""Returns the credit cost from the last evaluation."""
|
|
118
|
+
if not self.evaluation_result:
|
|
119
|
+
self.evaluate()
|
|
120
|
+
return self.evaluation_result.get('cost', {}).get('total_credit_cost', 0.0)
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def estimated_time(self) -> float:
|
|
124
|
+
"""Returns the estimated duration in seconds from the last evaluation."""
|
|
125
|
+
if not self.evaluation_result:
|
|
126
|
+
self.evaluate()
|
|
127
|
+
return self.evaluation_result.get('estimated_duration_s', 0.1)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def is_affordable(self) -> bool:
|
|
131
|
+
"""Returns True if the user has enough credits for the last evaluation."""
|
|
132
|
+
from . import session
|
|
133
|
+
status = session.get_status()
|
|
134
|
+
return status.get('credits', 0) >= self.cost
|
|
135
|
+
|
|
136
|
+
def evaluate(self, options: Optional[dict] = None):
|
|
137
|
+
"""
|
|
138
|
+
Performs a secure evaluation of the expression on Graviton.
|
|
139
|
+
"""
|
|
140
|
+
from . import session
|
|
141
|
+
|
|
142
|
+
if not session.api_key:
|
|
143
|
+
self.error = "Unauthenticated. Please run 'gv.login(key)' first."
|
|
144
|
+
if options and options.get("type") == "JSON":
|
|
145
|
+
raise RuntimeError(self.error)
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
srepr_str = sp.srepr(self.expr)
|
|
150
|
+
response = requests.post(
|
|
151
|
+
f"{session.api_url}/eval",
|
|
152
|
+
json={"f": srepr_str},
|
|
153
|
+
headers={"X-API-KEY": session.api_key},
|
|
154
|
+
timeout=10,
|
|
155
|
+
verify=session.verify
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if response.status_code in [200, 201]:
|
|
159
|
+
self.evaluation_result = response.json()
|
|
160
|
+
self.error = None
|
|
161
|
+
else:
|
|
162
|
+
try:
|
|
163
|
+
err_data = response.json()
|
|
164
|
+
err_msg = err_data.get('error', 'Unknown error')
|
|
165
|
+
self.error = f"API Error {response.status_code}: {err_msg}"
|
|
166
|
+
except:
|
|
167
|
+
self.error = f"API Error {response.status_code}"
|
|
168
|
+
except Exception as e:
|
|
169
|
+
self.error = f"Connection Error: {str(e)}"
|
|
170
|
+
|
|
171
|
+
if self.error and options and options.get("type") == "JSON":
|
|
172
|
+
raise RuntimeError(self.error)
|
|
173
|
+
|
|
174
|
+
if options and options.get("type") == "JSON":
|
|
175
|
+
return GravitonEvaluation(self.evaluation_result)
|
|
176
|
+
|
|
177
|
+
return self
|
|
178
|
+
|
|
179
|
+
def compute(self, options: Optional[Any] = None) -> GravitonJob:
|
|
180
|
+
"""
|
|
181
|
+
Executes the compute job. If not yet evaluated, evaluates first.
|
|
182
|
+
"""
|
|
183
|
+
from . import session
|
|
184
|
+
|
|
185
|
+
if isinstance(options, bool):
|
|
186
|
+
options = {"auto_charge": options}
|
|
187
|
+
|
|
188
|
+
auto_charge = False
|
|
189
|
+
if options and isinstance(options, dict):
|
|
190
|
+
auto_charge = options.get("auto_charge", False)
|
|
191
|
+
|
|
192
|
+
if not self.evaluation_result:
|
|
193
|
+
self.evaluate()
|
|
194
|
+
if self.error:
|
|
195
|
+
raise RuntimeError(self.error)
|
|
196
|
+
|
|
197
|
+
job_id = self.evaluation_result['id']
|
|
198
|
+
|
|
199
|
+
# Check if we need to auto-charge (client-side check for better DX)
|
|
200
|
+
if auto_charge:
|
|
201
|
+
cost = self.evaluation_result.get('cost', {}).get('total_credit_cost', 0)
|
|
202
|
+
status = session.get_status()
|
|
203
|
+
if status.get('credits', 0) < cost:
|
|
204
|
+
if not status.get('has_payment_method'):
|
|
205
|
+
dashboard_url = session.get_dashboard_url()
|
|
206
|
+
if session.api_key and "localhost" in dashboard_url:
|
|
207
|
+
dashboard_url += f"?dev_key={session.api_key}"
|
|
208
|
+
raise RuntimeError(
|
|
209
|
+
f"Insufficient Credits ({status.get('credits', 0):.2f}/{cost:.2f}). "
|
|
210
|
+
f"Auto-charging is not possible because no primary payment method is configured. "
|
|
211
|
+
f"Please add a payment method in your dashboard: {dashboard_url}"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Call /compute
|
|
215
|
+
return self._trigger_compute(job_id, auto_charge)
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def _trigger_compute(job_id: str, auto_charge: bool = False) -> GravitonJob:
|
|
219
|
+
"""Internal helper to trigger the /compute endpoint."""
|
|
220
|
+
from . import session
|
|
221
|
+
|
|
222
|
+
dashboard_url = session.get_dashboard_url()
|
|
223
|
+
if session.api_key and "localhost" in dashboard_url:
|
|
224
|
+
dashboard_url += f"?dev_key={session.api_key}"
|
|
225
|
+
|
|
226
|
+
resp = requests.post(
|
|
227
|
+
f"{session.api_url}/compute",
|
|
228
|
+
json={"job_id": job_id, "auto_charge": auto_charge},
|
|
229
|
+
headers={"X-API-KEY": session.api_key},
|
|
230
|
+
timeout=15,
|
|
231
|
+
verify=session.verify
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if resp.status_code in [200, 201, 202]:
|
|
235
|
+
return GravitonJob(job_id)
|
|
236
|
+
else:
|
|
237
|
+
msg = resp.text
|
|
238
|
+
try:
|
|
239
|
+
err_data = resp.json()
|
|
240
|
+
if isinstance(err_data, dict):
|
|
241
|
+
msg = err_data.get('error', msg)
|
|
242
|
+
# If there's a more descriptive message, use it
|
|
243
|
+
if err_data.get('message'):
|
|
244
|
+
msg = f"{msg}: {err_data['message']}"
|
|
245
|
+
except:
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
if "PAYMENT_METHOD_REQUIRED" in msg or "INSUFFICIENT_CREDITS" in msg:
|
|
249
|
+
msg = f"{msg}. Please visit your dashboard to manage credits: {dashboard_url}"
|
|
250
|
+
|
|
251
|
+
raise RuntimeError(f"Compute failed: {msg}")
|
|
252
|
+
|
|
253
|
+
def wait(self) -> Any:
|
|
254
|
+
"""Executes the compute job and waits for the result."""
|
|
255
|
+
return self.compute().wait()
|
|
256
|
+
|
|
257
|
+
def _repr_html_(self):
|
|
258
|
+
from . import session
|
|
259
|
+
|
|
260
|
+
# 1. Prepare Base Info
|
|
261
|
+
try:
|
|
262
|
+
latex_str = sp.latex(self.expr)
|
|
263
|
+
srepr_str = sp.srepr(self.expr)
|
|
264
|
+
except Exception as e:
|
|
265
|
+
return f"<div style='color: #ba1a1a;'>Error preparing expression: {e}</div>"
|
|
266
|
+
|
|
267
|
+
# Unique ID for the card instance
|
|
268
|
+
card_id = f"graviton-card-{uuid.uuid4().hex[:8]}"
|
|
269
|
+
srepr_escaped = srepr_str.replace("'", "\\'")
|
|
270
|
+
|
|
271
|
+
# 2. Check Auth Status
|
|
272
|
+
auth_status = session.get_status()
|
|
273
|
+
if not auth_status.get('authenticated'):
|
|
274
|
+
return f"""
|
|
275
|
+
<div style="border: 1px solid #ccc; padding: 25px; border-radius: 16px; background: #fffbfa; font-family: 'Outfit', sans-serif; text-align: center; max-width: 500px;">
|
|
276
|
+
<div style="font-size: 1.4rem; margin-bottom: 10px; color: #1a1a1a;">$${latex_str}$$</div>
|
|
277
|
+
<div style="color: #666; margin-bottom: 20px;">Authenticate to access Graviton Supercomputing.</div>
|
|
278
|
+
<a href="http://www.g-qt.com" target="_blank" style="background: #1a1a1a; color: white; padding: 10px 25px; border-radius: 20px; text-decoration: none; font-weight: 600;">Login to Graviton</a>
|
|
279
|
+
</div>
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
# 3. Build the Information Grid
|
|
283
|
+
if self.evaluation_result:
|
|
284
|
+
res = self.evaluation_result
|
|
285
|
+
# Use estimated duration if available (mocked for now as 0.02s per op)
|
|
286
|
+
est_time = res.get('estimated_duration_s', 0.1)
|
|
287
|
+
cost = res.get('cost', {}).get('total_credit_cost', 0.0)
|
|
288
|
+
|
|
289
|
+
# Determine button state
|
|
290
|
+
has_credits = auth_status.get('credits', 0) >= cost
|
|
291
|
+
has_payment = auth_status.get('has_payment_method')
|
|
292
|
+
|
|
293
|
+
if has_credits:
|
|
294
|
+
btn_text = "Supercompute on Graviton"
|
|
295
|
+
btn_action = "runCompute()"
|
|
296
|
+
elif has_payment:
|
|
297
|
+
btn_text = "Purchase & Execute"
|
|
298
|
+
btn_action = "runPurchaseAndCompute()"
|
|
299
|
+
else:
|
|
300
|
+
btn_text = "Insufficient Credits"
|
|
301
|
+
dashboard_url = session.get_dashboard_url()
|
|
302
|
+
if session.api_key:
|
|
303
|
+
dashboard_url += f"?dev_key={session.api_key}"
|
|
304
|
+
btn_action = f"window.open('{dashboard_url}', '_blank')"
|
|
305
|
+
|
|
306
|
+
eval_html = f"""
|
|
307
|
+
<div id="{card_id}-metrics" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; width: 100%; margin-bottom: 15px; padding: 12px; background: rgba(0,0,0,0.03); border-radius: 8px; font-size: 0.9rem;">
|
|
308
|
+
<div><strong>Est. Time:</strong> {est_time:.1f}s</div>
|
|
309
|
+
<div><strong>Variables:</strong> {len(res.get('variables', []))}</div>
|
|
310
|
+
<div style="grid-column: span 2; text-align: center; border-top: 1px solid rgba(0,0,0,0.05); padding-top: 8px; margin-top: 4px; color: #2e7d32;">
|
|
311
|
+
<strong>Cost:</strong> {cost:.2f} Credits
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
"""
|
|
315
|
+
else:
|
|
316
|
+
btn_text = "Evaluate on Graviton"
|
|
317
|
+
btn_action = "runEvaluate()"
|
|
318
|
+
eval_html = f"""
|
|
319
|
+
<div id="{card_id}-metrics" style="color: #666; font-size: 0.85rem; margin-bottom: 15px; text-align: center;">
|
|
320
|
+
Evaluate to see estimated time and costs.
|
|
321
|
+
</div>
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
# 4. Final HTML with JS Logic
|
|
325
|
+
return f"""
|
|
326
|
+
<div id="{card_id}" class="graviton-card" style="border: 1px solid #ccc; padding: 25px; border-radius: 16px; background: #fdfdfd; font-family: 'Outfit', sans-serif; display: flex; flex-direction: column; align-items: center; box-shadow: 0 4px 12px rgba(0,0,0,0.05); max-width: 500px;">
|
|
327
|
+
<div style="font-size: 1.4rem; margin-bottom: 15px; color: #1a1a1a;">$${latex_str}$$</div>
|
|
328
|
+
|
|
329
|
+
{eval_html}
|
|
330
|
+
|
|
331
|
+
<button id="{card_id}-btn" onclick="{btn_action}" style="background: #1a1a1a; color: #ffffff; border: 1px solid rgba(255,255,255,0.2); padding: 12px 32px; border-radius: 30px; cursor: pointer; font-weight: 600; font-size: 1rem; width: 100%; transition: all 0.2s;">
|
|
332
|
+
{btn_text}
|
|
333
|
+
</button>
|
|
334
|
+
|
|
335
|
+
<div id="{card_id}-status" style="font-size: 0.75rem; color: #777; margin-top: 12px; font-style: italic;">
|
|
336
|
+
Verified SymPy model ready for Graviton Acceleration
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<script>
|
|
340
|
+
(function() {{
|
|
341
|
+
const card = document.getElementById('{card_id}');
|
|
342
|
+
const btn = document.getElementById('{card_id}-btn');
|
|
343
|
+
const status = document.getElementById('{card_id}-status');
|
|
344
|
+
const metrics = document.getElementById('{card_id}-metrics');
|
|
345
|
+
const api_url = '{session.api_url}';
|
|
346
|
+
const api_key = '{session.api_key}';
|
|
347
|
+
|
|
348
|
+
let currentJobId = '';
|
|
349
|
+
window.runEvaluate = async () => {{
|
|
350
|
+
btn.disabled = true;
|
|
351
|
+
btn.innerText = "Evaluating...";
|
|
352
|
+
try {{
|
|
353
|
+
const resp = await fetch(api_url + '/eval', {{
|
|
354
|
+
method: 'POST',
|
|
355
|
+
headers: {{ 'Content-Type': 'application/json', 'X-API-KEY': api_key }},
|
|
356
|
+
body: JSON.stringify({{ f: '{srepr_escaped}' }})
|
|
357
|
+
}});
|
|
358
|
+
if (resp.ok) {{
|
|
359
|
+
const data = await resp.json();
|
|
360
|
+
currentJobId = data.id;
|
|
361
|
+
status.innerText = "Evaluation complete. Ready to provision.";
|
|
362
|
+
|
|
363
|
+
if (data.result !== undefined) {{
|
|
364
|
+
metrics.innerHTML = '<div style="grid-column: span 2; text-align: center; font-size: 1.2rem; color: #1a1a1a;"><strong>Result:</strong> ' + data.result + '</div>';
|
|
365
|
+
btn.innerText = "Compute Finished";
|
|
366
|
+
}} else {{
|
|
367
|
+
let costStr = (data.cost && data.cost.total_credit_cost !== undefined) ? data.cost.total_credit_cost.toFixed(2) : "0.00";
|
|
368
|
+
let timeStr = data.estimated_duration_s !== undefined ? data.estimated_duration_s : "?";
|
|
369
|
+
metrics.innerHTML = 'Estimated Cost: ' + costStr + ' credits | Estimated Time: ' + timeStr + 's';
|
|
370
|
+
btn.innerText = "Compute on Swarm";
|
|
371
|
+
btn.onclick = window.runCompute;
|
|
372
|
+
btn.disabled = false;
|
|
373
|
+
}}
|
|
374
|
+
}} else {{
|
|
375
|
+
status.innerText = "Evaluation failed: " + resp.status;
|
|
376
|
+
btn.disabled = false;
|
|
377
|
+
}}
|
|
378
|
+
}} catch (e) {{
|
|
379
|
+
status.innerText = "Connection failed.";
|
|
380
|
+
btn.disabled = false;
|
|
381
|
+
}}
|
|
382
|
+
}};
|
|
383
|
+
|
|
384
|
+
window.runCompute = async () => {{
|
|
385
|
+
if (!currentJobId) return;
|
|
386
|
+
btn.disabled = true;
|
|
387
|
+
btn.innerText = "Provisioning...";
|
|
388
|
+
try {{
|
|
389
|
+
const resp = await fetch(api_url + '/compute', {{
|
|
390
|
+
method: 'POST',
|
|
391
|
+
headers: {{ 'Content-Type': 'application/json', 'X-API-KEY': api_key }},
|
|
392
|
+
body: JSON.stringify({{ job_id: currentJobId }})
|
|
393
|
+
}});
|
|
394
|
+
if (resp.ok) {{
|
|
395
|
+
pollResult(currentJobId);
|
|
396
|
+
}} else {{
|
|
397
|
+
status.innerText = "Compute failed.";
|
|
398
|
+
btn.disabled = false;
|
|
399
|
+
}}
|
|
400
|
+
}} catch (e) {{
|
|
401
|
+
status.innerText = "Network error.";
|
|
402
|
+
btn.disabled = false;
|
|
403
|
+
}}
|
|
404
|
+
}};
|
|
405
|
+
|
|
406
|
+
const pollResult = async (jobId) => {{
|
|
407
|
+
status.innerText = "Executing on Graviton Swarm...";
|
|
408
|
+
const timer = setInterval(async () => {{
|
|
409
|
+
const resp = await fetch(api_url + '/results/' + jobId, {{
|
|
410
|
+
headers: {{ 'X-API-KEY': api_key }}
|
|
411
|
+
}});
|
|
412
|
+
const data = await resp.json();
|
|
413
|
+
if (data.status === 'COMPLETED') {{
|
|
414
|
+
clearInterval(timer);
|
|
415
|
+
const resResp = await fetch(api_url + '/results/' + jobId + '/download', {{
|
|
416
|
+
headers: {{ 'X-API-KEY': api_key }}
|
|
417
|
+
}});
|
|
418
|
+
const result = await resResp.text();
|
|
419
|
+
metrics.innerHTML = '<div style="grid-column: span 2; text-align: center; font-size: 1.2rem; color: #1a1a1a;"><strong>Result:</strong> ' + result + '</div>';
|
|
420
|
+
btn.style.display = 'none';
|
|
421
|
+
status.innerText = "Job COMPLETED";
|
|
422
|
+
}} else if (data.status === 'FAILED') {{
|
|
423
|
+
clearInterval(timer);
|
|
424
|
+
status.innerText = "Job FAILED";
|
|
425
|
+
}}
|
|
426
|
+
}}, 1000);
|
|
427
|
+
}};
|
|
428
|
+
}})();
|
|
429
|
+
</script>
|
|
430
|
+
</div>
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
def gv(expr):
|
|
434
|
+
"""
|
|
435
|
+
Wraps a SymPy expression for Graviton integration.
|
|
436
|
+
"""
|
|
437
|
+
return GravitonExpression(expr)
|
|
438
|
+
|
|
439
|
+
# Alias for better readability
|
|
440
|
+
expr = gv
|