commutator-sdk 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.
- commutator/__init__.py +19 -0
- commutator/client.py +155 -0
- commutator/exceptions.py +106 -0
- commutator/fleet.py +72 -0
- commutator/identity.py +91 -0
- commutator/jobs.py +159 -0
- commutator/optimizer.py +66 -0
- commutator_sdk-0.1.0.dist-info/METADATA +16 -0
- commutator_sdk-0.1.0.dist-info/RECORD +11 -0
- commutator_sdk-0.1.0.dist-info/WHEEL +5 -0
- commutator_sdk-0.1.0.dist-info/top_level.txt +1 -0
commutator/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Copyright (c) 2026. Commutator Studios GmbH.
|
|
2
|
+
|
|
3
|
+
from commutator.client import CommutatorClient
|
|
4
|
+
from commutator.exceptions import (
|
|
5
|
+
APIError,
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
CommutatorError,
|
|
8
|
+
NotFoundError,
|
|
9
|
+
ValidationError,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"CommutatorClient",
|
|
14
|
+
"CommutatorError",
|
|
15
|
+
"APIError",
|
|
16
|
+
"AuthenticationError",
|
|
17
|
+
"ValidationError",
|
|
18
|
+
"NotFoundError",
|
|
19
|
+
]
|
commutator/client.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Copyright (c) 2026. Commutator Studios GmbH.
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import typing
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from commutator.exceptions import raise_for_status
|
|
9
|
+
from commutator.fleet import FleetModule
|
|
10
|
+
from commutator.identity import IdentityModule
|
|
11
|
+
from commutator.jobs import JobsModule
|
|
12
|
+
from commutator.optimizer import OptimizerModule
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Environment(str, Enum):
|
|
16
|
+
"""The target environment for the Commutator SDK."""
|
|
17
|
+
|
|
18
|
+
PROD = "prod"
|
|
19
|
+
STAGE = "stage"
|
|
20
|
+
DEMO = "demo"
|
|
21
|
+
LOCAL = "local"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CommutatorClient:
|
|
25
|
+
"""The central client for interacting with Commutator Studios Platform."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
*,
|
|
30
|
+
api_key: typing.Optional[str] = None,
|
|
31
|
+
token: typing.Optional[str] = None,
|
|
32
|
+
organization_id: typing.Optional[str] = None,
|
|
33
|
+
_internal_base_url: typing.Optional[str] = None,
|
|
34
|
+
):
|
|
35
|
+
"""Initializes the Commutator SDK Client.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
api_key: The user's API Key. If not provided, will look for the
|
|
39
|
+
'COMMUTATOR_API_KEY' environment variable.
|
|
40
|
+
token: A JWT bearer token retrieved via login (if not using API Key).
|
|
41
|
+
organization_id: The organization ID. If not provided, will look for the
|
|
42
|
+
'COMMUTATOR_ORGANIZATION_ID' environment variable.
|
|
43
|
+
"""
|
|
44
|
+
self._api_key = api_key or os.environ.get("COMMUTATOR_API_KEY")
|
|
45
|
+
self._token = token
|
|
46
|
+
self._organization_id = organization_id or os.environ.get("COMMUTATOR_ORGANIZATION_ID")
|
|
47
|
+
|
|
48
|
+
if not self._api_key and not self._token:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
"Either api_key or token must be provided, or "
|
|
51
|
+
"COMMUTATOR_API_KEY environment variable must be set."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if _internal_base_url:
|
|
55
|
+
self.base_url = _internal_base_url
|
|
56
|
+
elif self._api_key:
|
|
57
|
+
if self._api_key.startswith("cs_loc_"):
|
|
58
|
+
self.base_url = "http://localhost:3000"
|
|
59
|
+
elif self._api_key.startswith("cs_stage_"):
|
|
60
|
+
self.base_url = "https://commutatorstaging.com"
|
|
61
|
+
elif self._api_key.startswith("cs_demo_"):
|
|
62
|
+
self.base_url = "https://commutator.tech"
|
|
63
|
+
else:
|
|
64
|
+
self.base_url = "https://commutator.app"
|
|
65
|
+
else:
|
|
66
|
+
self.base_url = "https://commutator.app"
|
|
67
|
+
|
|
68
|
+
# Initialize submodules
|
|
69
|
+
self.identity = IdentityModule(client=self)
|
|
70
|
+
self.admin = self.identity # Alias for documentation consistency
|
|
71
|
+
self.jobs = JobsModule(client=self)
|
|
72
|
+
self.fleet = FleetModule(client=self)
|
|
73
|
+
self.optimizer = OptimizerModule(client=self)
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def login_with_api_key(cls, *, api_key: str) -> "CommutatorClient":
|
|
77
|
+
"""Initialize the client using an explicit API key.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
api_key: The user's API Key.
|
|
81
|
+
"""
|
|
82
|
+
return cls(api_key=api_key)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def login_with_email(
|
|
86
|
+
cls,
|
|
87
|
+
*,
|
|
88
|
+
email: str,
|
|
89
|
+
password: str,
|
|
90
|
+
environment: Environment = Environment.PROD,
|
|
91
|
+
) -> "CommutatorClient":
|
|
92
|
+
"""Authenticate using email & password to retrieve a JWT token.
|
|
93
|
+
|
|
94
|
+
POST /api/v1/identity/login
|
|
95
|
+
"""
|
|
96
|
+
env_map = {
|
|
97
|
+
Environment.PROD: "https://commutator.app",
|
|
98
|
+
Environment.STAGE: "https://commutatorstaging.com",
|
|
99
|
+
Environment.DEMO: "https://commutator.tech",
|
|
100
|
+
Environment.LOCAL: "http://localhost:3000",
|
|
101
|
+
}
|
|
102
|
+
target_url = env_map.get(environment, "https://commutator.app")
|
|
103
|
+
|
|
104
|
+
response = requests.post(
|
|
105
|
+
f"{target_url}/api/v1/identity/login",
|
|
106
|
+
json={"email": email, "password": password},
|
|
107
|
+
timeout=60,
|
|
108
|
+
)
|
|
109
|
+
raise_for_status(response)
|
|
110
|
+
|
|
111
|
+
auth_response = response.json()
|
|
112
|
+
data_block = auth_response.get("data", {})
|
|
113
|
+
token = data_block.get("token")
|
|
114
|
+
|
|
115
|
+
if not token:
|
|
116
|
+
raise ValueError("Authentication did not return a valid token.")
|
|
117
|
+
|
|
118
|
+
user_info = data_block.get("user", {})
|
|
119
|
+
org_id = user_info.get("organizationId") or user_info.get("organization_id")
|
|
120
|
+
|
|
121
|
+
return cls(token=token, organization_id=org_id, _internal_base_url=target_url)
|
|
122
|
+
|
|
123
|
+
def _request(
|
|
124
|
+
self,
|
|
125
|
+
*,
|
|
126
|
+
method: str,
|
|
127
|
+
path: str,
|
|
128
|
+
params: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
129
|
+
json: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
130
|
+
data: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
131
|
+
files: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
132
|
+
headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
133
|
+
) -> requests.Response:
|
|
134
|
+
"""Sends an HTTP request with authenticating headers."""
|
|
135
|
+
url = f"{self.base_url}{path}"
|
|
136
|
+
|
|
137
|
+
request_headers = headers or {}
|
|
138
|
+
if self._token:
|
|
139
|
+
request_headers["Authorization"] = f"Bearer {self._token}"
|
|
140
|
+
elif self._api_key:
|
|
141
|
+
request_headers["X-API-Key"] = self._api_key
|
|
142
|
+
|
|
143
|
+
if self._organization_id:
|
|
144
|
+
request_headers["X-Organization-Id"] = self._organization_id
|
|
145
|
+
|
|
146
|
+
return requests.request(
|
|
147
|
+
method=method,
|
|
148
|
+
url=url,
|
|
149
|
+
params=params,
|
|
150
|
+
json=json,
|
|
151
|
+
data=data,
|
|
152
|
+
files=files,
|
|
153
|
+
headers=request_headers,
|
|
154
|
+
timeout=60,
|
|
155
|
+
)
|
commutator/exceptions.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Copyright (c) 2026. Commutator Studios GmbH.
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CommutatorError(Exception):
|
|
7
|
+
"""Base exception for all Commutator SDK errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, *, message: str):
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.message = message
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class APIError(CommutatorError):
|
|
15
|
+
"""Exception raised when the API returns an error status code."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
status_code: int,
|
|
21
|
+
code: str,
|
|
22
|
+
message: str,
|
|
23
|
+
details: typing.Optional[typing.List[typing.Dict[str, typing.Any]]] = None,
|
|
24
|
+
request_id: typing.Optional[str] = None,
|
|
25
|
+
documentation_url: typing.Optional[str] = None,
|
|
26
|
+
):
|
|
27
|
+
super().__init__(message=message)
|
|
28
|
+
self.status_code = status_code
|
|
29
|
+
self.code = code
|
|
30
|
+
self.details = details or []
|
|
31
|
+
self.request_id = request_id
|
|
32
|
+
self.documentation_url = documentation_url
|
|
33
|
+
|
|
34
|
+
def __str__(self) -> str:
|
|
35
|
+
err_str = f"[{self.status_code} {self.code}] {self.message}"
|
|
36
|
+
if self.request_id:
|
|
37
|
+
err_str += f" (Request ID: {self.request_id})"
|
|
38
|
+
return err_str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AuthenticationError(APIError):
|
|
42
|
+
"""Raised on authentication failure (e.g. invalid API key or login)."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ValidationError(APIError):
|
|
46
|
+
"""Raised when the API returns validation issues."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class NotFoundError(APIError):
|
|
50
|
+
"""Raised when a resource (e.g., job) does not exist."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def raise_for_status(response: typing.Any) -> None:
|
|
54
|
+
"""Helper to parse error JSON and raise appropriate APIError."""
|
|
55
|
+
if response.status_code >= 400:
|
|
56
|
+
try:
|
|
57
|
+
err_data = response.json()
|
|
58
|
+
status_code = err_data.get("status", response.status_code)
|
|
59
|
+
code = err_data.get("code", "API_ERROR")
|
|
60
|
+
message = err_data.get("message", response.text)
|
|
61
|
+
details = err_data.get("details", [])
|
|
62
|
+
request_id = err_data.get("requestId")
|
|
63
|
+
documentation_url = err_data.get("documentationUrl")
|
|
64
|
+
except ValueError:
|
|
65
|
+
status_code = response.status_code
|
|
66
|
+
code = "API_ERROR"
|
|
67
|
+
message = response.text
|
|
68
|
+
details = []
|
|
69
|
+
request_id = None
|
|
70
|
+
documentation_url = None
|
|
71
|
+
|
|
72
|
+
if status_code == 401:
|
|
73
|
+
raise AuthenticationError(
|
|
74
|
+
status_code=status_code,
|
|
75
|
+
code=code,
|
|
76
|
+
message=message,
|
|
77
|
+
details=details,
|
|
78
|
+
request_id=request_id,
|
|
79
|
+
documentation_url=documentation_url,
|
|
80
|
+
)
|
|
81
|
+
if status_code == 400:
|
|
82
|
+
raise ValidationError(
|
|
83
|
+
status_code=status_code,
|
|
84
|
+
code=code,
|
|
85
|
+
message=message,
|
|
86
|
+
details=details,
|
|
87
|
+
request_id=request_id,
|
|
88
|
+
documentation_url=documentation_url,
|
|
89
|
+
)
|
|
90
|
+
if status_code == 404:
|
|
91
|
+
raise NotFoundError(
|
|
92
|
+
status_code=status_code,
|
|
93
|
+
code=code,
|
|
94
|
+
message=message,
|
|
95
|
+
details=details,
|
|
96
|
+
request_id=request_id,
|
|
97
|
+
documentation_url=documentation_url,
|
|
98
|
+
)
|
|
99
|
+
raise APIError(
|
|
100
|
+
status_code=status_code,
|
|
101
|
+
code=code,
|
|
102
|
+
message=message,
|
|
103
|
+
details=details,
|
|
104
|
+
request_id=request_id,
|
|
105
|
+
documentation_url=documentation_url,
|
|
106
|
+
)
|
commutator/fleet.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Copyright (c) 2026. Commutator Studios GmbH.
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from commutator.exceptions import raise_for_status
|
|
6
|
+
|
|
7
|
+
if typing.TYPE_CHECKING:
|
|
8
|
+
from commutator.client import CommutatorClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FleetModule:
|
|
12
|
+
"""Module for querying active quantum hardware backends."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, *, client: "CommutatorClient"):
|
|
15
|
+
self._client = client
|
|
16
|
+
|
|
17
|
+
def list_backends(self) -> typing.List[typing.Dict[str, typing.Any]]:
|
|
18
|
+
"""Get available quantum backends and fleet status.
|
|
19
|
+
|
|
20
|
+
GET /api/v1/fleet
|
|
21
|
+
"""
|
|
22
|
+
response = self._client._request(method="GET", path="/api/v1/backends")
|
|
23
|
+
raise_for_status(response)
|
|
24
|
+
return typing.cast(
|
|
25
|
+
typing.List[typing.Dict[str, typing.Any]], response.json().get("data", [])
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def update_backend(self, *, backend_id: str, enabled: bool) -> typing.Dict[str, typing.Any]:
|
|
29
|
+
"""Update fleet configurations or backend preferences.
|
|
30
|
+
|
|
31
|
+
PATCH /api/v1/fleet
|
|
32
|
+
"""
|
|
33
|
+
payload = {"id": backend_id, "enabled": enabled}
|
|
34
|
+
response = self._client._request(method="PATCH", path="/api/v1/backends", json=payload)
|
|
35
|
+
raise_for_status(response)
|
|
36
|
+
return typing.cast(typing.Dict[str, typing.Any], response.json().get("data", {}))
|
|
37
|
+
|
|
38
|
+
def discover(self) -> typing.List[typing.Dict[str, typing.Any]]:
|
|
39
|
+
"""Discover detailed topology and calibration data.
|
|
40
|
+
|
|
41
|
+
POST /api/v1/backends/discovery
|
|
42
|
+
"""
|
|
43
|
+
response = self._client._request(method="POST", path="/api/v1/backends/discovery")
|
|
44
|
+
raise_for_status(response)
|
|
45
|
+
# Fetch and return the newly synchronized detailed list
|
|
46
|
+
return self.list_backends()
|
|
47
|
+
|
|
48
|
+
def activate(self, *, backend_id: str) -> bool:
|
|
49
|
+
"""Request exclusive access or activation of a specific reserved backend.
|
|
50
|
+
|
|
51
|
+
PATCH /api/v1/backend/[id]/activate
|
|
52
|
+
"""
|
|
53
|
+
response = self._client._request(
|
|
54
|
+
method="PATCH", path=f"/api/v1/backend/{backend_id}/activate", json={"isActive": True}
|
|
55
|
+
)
|
|
56
|
+
if response.status_code in (200, 204):
|
|
57
|
+
return True
|
|
58
|
+
raise_for_status(response)
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
def deactivate(self, *, backend_id: str) -> bool:
|
|
62
|
+
"""Deactivate a specific reserved backend.
|
|
63
|
+
|
|
64
|
+
PATCH /api/v1/backend/[id]/activate
|
|
65
|
+
"""
|
|
66
|
+
response = self._client._request(
|
|
67
|
+
method="PATCH", path=f"/api/v1/backend/{backend_id}/activate", json={"isActive": False}
|
|
68
|
+
)
|
|
69
|
+
if response.status_code in (200, 204):
|
|
70
|
+
return True
|
|
71
|
+
raise_for_status(response)
|
|
72
|
+
return False
|
commutator/identity.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Copyright (c) 2026. Commutator Studios GmbH.
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from commutator.exceptions import raise_for_status
|
|
6
|
+
|
|
7
|
+
if typing.TYPE_CHECKING:
|
|
8
|
+
from commutator.client import CommutatorClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class IdentityModule:
|
|
12
|
+
"""Module for identity and authentication-related APIs."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, *, client: "CommutatorClient"):
|
|
15
|
+
self._client = client
|
|
16
|
+
|
|
17
|
+
def list_api_keys(self) -> typing.List[typing.Dict[str, typing.Any]]:
|
|
18
|
+
"""Retrieve a list of the user's API keys.
|
|
19
|
+
|
|
20
|
+
GET /api/v1/identity/keys
|
|
21
|
+
"""
|
|
22
|
+
response = self._client._request(method="GET", path="/api/v1/identity/keys")
|
|
23
|
+
raise_for_status(response)
|
|
24
|
+
return typing.cast(
|
|
25
|
+
typing.List[typing.Dict[str, typing.Any]], response.json().get("data", [])
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def create_api_key(
|
|
29
|
+
self, *, name: str, expires_in_days: int = 30
|
|
30
|
+
) -> typing.Dict[str, typing.Any]:
|
|
31
|
+
"""Generate a new API key.
|
|
32
|
+
|
|
33
|
+
POST /api/v1/identity/keys
|
|
34
|
+
"""
|
|
35
|
+
payload = {"name": name, "expiresInDays": expires_in_days}
|
|
36
|
+
response = self._client._request(method="POST", path="/api/v1/identity/keys", json=payload)
|
|
37
|
+
raise_for_status(response)
|
|
38
|
+
return typing.cast(typing.Dict[str, typing.Any], response.json().get("data", {}))
|
|
39
|
+
|
|
40
|
+
def revoke_api_key(self, *, key_id: str) -> bool:
|
|
41
|
+
"""Revoke an active API key immediately.
|
|
42
|
+
|
|
43
|
+
DELETE /api/v1/identity/keys/[id]
|
|
44
|
+
"""
|
|
45
|
+
response = self._client._request(method="DELETE", path=f"/api/v1/identity/keys/{key_id}")
|
|
46
|
+
if response.status_code in (200, 204):
|
|
47
|
+
return True
|
|
48
|
+
raise_for_status(response)
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
def get_team(self) -> typing.List[typing.Dict[str, typing.Any]]:
|
|
52
|
+
"""Get details about the user's team.
|
|
53
|
+
|
|
54
|
+
GET /api/v1/identity/team
|
|
55
|
+
"""
|
|
56
|
+
response = self._client._request(method="GET", path="/api/v1/identity/team")
|
|
57
|
+
raise_for_status(response)
|
|
58
|
+
return typing.cast(
|
|
59
|
+
typing.List[typing.Dict[str, typing.Any]], response.json().get("data", [])
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def get_audit_logs(self, *, limit: int = 50) -> typing.List[typing.Dict[str, typing.Any]]:
|
|
63
|
+
"""Retrieve recent security and access audit logs for the team.
|
|
64
|
+
|
|
65
|
+
GET /api/v1/audit/logs
|
|
66
|
+
"""
|
|
67
|
+
response = self._client._request(
|
|
68
|
+
method="GET", path="/api/v1/audit/logs", params={"limit": limit}
|
|
69
|
+
)
|
|
70
|
+
raise_for_status(response)
|
|
71
|
+
return typing.cast(
|
|
72
|
+
typing.List[typing.Dict[str, typing.Any]], response.json().get("data", [])
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def add_ibm_key(
|
|
76
|
+
self, *, api_key: str, name: str = "IBM API Key"
|
|
77
|
+
) -> typing.Union[typing.Dict[str, typing.Any], str]:
|
|
78
|
+
"""Set your IBM API key for quantum simulations.
|
|
79
|
+
|
|
80
|
+
POST /api/v1/identity/credentials
|
|
81
|
+
"""
|
|
82
|
+
payload = {"provider": "IBM", "name": name, "config": {"apiToken": api_key, "crns": []}}
|
|
83
|
+
response = self._client._request(
|
|
84
|
+
method="POST", path="/api/v1/identity/credentials", json=payload
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if response.status_code == 409:
|
|
88
|
+
return "this key is already set"
|
|
89
|
+
|
|
90
|
+
raise_for_status(response)
|
|
91
|
+
return typing.cast(typing.Dict[str, typing.Any], response.json().get("data", {}))
|
commutator/jobs.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Copyright (c) 2026. Commutator Studios GmbH.
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from commutator.exceptions import raise_for_status
|
|
6
|
+
|
|
7
|
+
if typing.TYPE_CHECKING:
|
|
8
|
+
from commutator.client import CommutatorClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class JobsModule:
|
|
12
|
+
"""Module for job-related APIs."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, *, client: "CommutatorClient"):
|
|
15
|
+
self._client = client
|
|
16
|
+
|
|
17
|
+
def list(self) -> typing.List[typing.Dict[str, typing.Any]]:
|
|
18
|
+
"""List job history.
|
|
19
|
+
|
|
20
|
+
GET /api/v1/jobs
|
|
21
|
+
"""
|
|
22
|
+
response = self._client._request(method="GET", path="/api/v1/jobs")
|
|
23
|
+
raise_for_status(response)
|
|
24
|
+
return typing.cast(
|
|
25
|
+
typing.List[typing.Dict[str, typing.Any]], response.json().get("data", [])
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def create(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
name: str,
|
|
32
|
+
algorithm: str,
|
|
33
|
+
file_path: str,
|
|
34
|
+
organization_id: typing.Optional[str] = None,
|
|
35
|
+
) -> typing.Dict[str, typing.Any]:
|
|
36
|
+
"""Submit a new quantum job via file configuration.
|
|
37
|
+
|
|
38
|
+
POST /api/v1/job
|
|
39
|
+
"""
|
|
40
|
+
data = {
|
|
41
|
+
"name": name,
|
|
42
|
+
"algorithm": algorithm,
|
|
43
|
+
}
|
|
44
|
+
if organization_id:
|
|
45
|
+
data["organization_id"] = organization_id
|
|
46
|
+
with open(file_path, "rb") as f:
|
|
47
|
+
files = {"file": (file_path, f, "application/json")}
|
|
48
|
+
response = self._client._request(
|
|
49
|
+
method="POST", path="/api/v1/job", data=data, files=files
|
|
50
|
+
)
|
|
51
|
+
raise_for_status(response)
|
|
52
|
+
return typing.cast(typing.Dict[str, typing.Any], response.json().get("data", {}))
|
|
53
|
+
|
|
54
|
+
def get(self, *, job_id: str) -> typing.Dict[str, typing.Any]:
|
|
55
|
+
"""Get details for a specific job by its ID.
|
|
56
|
+
|
|
57
|
+
GET /api/v1/job/[id]
|
|
58
|
+
"""
|
|
59
|
+
response = self._client._request(method="GET", path=f"/api/v1/job/{job_id}")
|
|
60
|
+
raise_for_status(response)
|
|
61
|
+
return typing.cast(typing.Dict[str, typing.Any], response.json().get("data", {}))
|
|
62
|
+
|
|
63
|
+
def update(self, *, job_id: str, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]:
|
|
64
|
+
"""Updates the job details.
|
|
65
|
+
|
|
66
|
+
PATCH /api/v1/jobs/[id]
|
|
67
|
+
"""
|
|
68
|
+
payload = {}
|
|
69
|
+
for k, v in kwargs.items():
|
|
70
|
+
camel_k = "".join(x.capitalize() or "_" for x in k.split("_"))
|
|
71
|
+
camel_k = camel_k[0].lower() + camel_k[1:]
|
|
72
|
+
payload[camel_k] = v
|
|
73
|
+
|
|
74
|
+
response = self._client._request(method="PATCH", path=f"/api/v1/job/{job_id}", json=payload)
|
|
75
|
+
raise_for_status(response)
|
|
76
|
+
return typing.cast(typing.Dict[str, typing.Any], response.json().get("data", {}))
|
|
77
|
+
|
|
78
|
+
def list_algorithms(self) -> typing.List[typing.Dict[str, typing.Any]]:
|
|
79
|
+
"""Retrieve the list of supported quantum algorithms available for execution.
|
|
80
|
+
|
|
81
|
+
GET /api/v1/job/algorithms
|
|
82
|
+
"""
|
|
83
|
+
response = self._client._request(method="GET", path="/api/v1/job/algorithms")
|
|
84
|
+
raise_for_status(response)
|
|
85
|
+
return typing.cast(
|
|
86
|
+
typing.List[typing.Dict[str, typing.Any]], response.json().get("data", [])
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def execute(
|
|
90
|
+
self,
|
|
91
|
+
*,
|
|
92
|
+
job_id: str,
|
|
93
|
+
backend: str,
|
|
94
|
+
optimization_level: int = 3,
|
|
95
|
+
dynamic_decoupling: bool = False,
|
|
96
|
+
max_iterations: typing.Optional[int] = None,
|
|
97
|
+
convergence: typing.Optional[float] = None,
|
|
98
|
+
predicted_shots: typing.Optional[int] = None,
|
|
99
|
+
**kwargs: typing.Any,
|
|
100
|
+
) -> typing.Dict[str, typing.Any]:
|
|
101
|
+
"""Executes a previously created quantum job on target hardware.
|
|
102
|
+
|
|
103
|
+
PATCH /api/v1/job/[id]
|
|
104
|
+
"""
|
|
105
|
+
payload: typing.Dict[str, typing.Any] = {
|
|
106
|
+
"status": "Pending",
|
|
107
|
+
"hardware": backend,
|
|
108
|
+
"optimizationLevel": optimization_level,
|
|
109
|
+
"dynamicDecoupling": dynamic_decoupling,
|
|
110
|
+
}
|
|
111
|
+
if max_iterations is not None:
|
|
112
|
+
payload["maxIterations"] = max_iterations
|
|
113
|
+
if convergence is not None:
|
|
114
|
+
payload["convergence"] = convergence
|
|
115
|
+
if predicted_shots is not None:
|
|
116
|
+
payload["predictedShots"] = predicted_shots
|
|
117
|
+
|
|
118
|
+
for k, v in kwargs.items():
|
|
119
|
+
camel_k = "".join(x.capitalize() or "_" for x in k.split("_"))
|
|
120
|
+
camel_k = camel_k[0].lower() + camel_k[1:]
|
|
121
|
+
if camel_k not in payload:
|
|
122
|
+
payload[camel_k] = v
|
|
123
|
+
|
|
124
|
+
response = self._client._request(method="PATCH", path=f"/api/v1/job/{job_id}", json=payload)
|
|
125
|
+
raise_for_status(response)
|
|
126
|
+
return typing.cast(typing.Dict[str, typing.Any], response.json().get("data", {}))
|
|
127
|
+
|
|
128
|
+
def cancel(self, *, job_id: str) -> bool:
|
|
129
|
+
"""Attempt to cancel a pending or queued job.
|
|
130
|
+
|
|
131
|
+
DELETE /api/v1/job/[id]
|
|
132
|
+
"""
|
|
133
|
+
response = self._client._request(method="DELETE", path=f"/api/v1/job/{job_id}")
|
|
134
|
+
if response.status_code in (200, 204):
|
|
135
|
+
return True
|
|
136
|
+
raise_for_status(response)
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
def get_download_url(self, *, job_id: str) -> str:
|
|
140
|
+
"""Retrieves a presigned URL to download the job results.
|
|
141
|
+
|
|
142
|
+
GET /api/v1/jobs/download-url?jobId=[id]
|
|
143
|
+
"""
|
|
144
|
+
response = self._client._request(
|
|
145
|
+
method="GET", path="/api/v1/jobs/download-url", params={"jobId": job_id}
|
|
146
|
+
)
|
|
147
|
+
raise_for_status(response)
|
|
148
|
+
return typing.cast(str, response.json().get("url", ""))
|
|
149
|
+
|
|
150
|
+
def download_results(self, *, job_id: str) -> typing.Dict[str, typing.Any]:
|
|
151
|
+
"""Downloads the job results data.
|
|
152
|
+
|
|
153
|
+
GET /api/v1/jobs/download-data?jobId=[id]
|
|
154
|
+
"""
|
|
155
|
+
response = self._client._request(
|
|
156
|
+
method="GET", path="/api/v1/jobs/download-data", params={"jobId": job_id}
|
|
157
|
+
)
|
|
158
|
+
raise_for_status(response)
|
|
159
|
+
return typing.cast(typing.Dict[str, typing.Any], response.json())
|
commutator/optimizer.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Copyright (c) 2026. Commutator Studios GmbH.
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from commutator.exceptions import raise_for_status
|
|
6
|
+
|
|
7
|
+
if typing.TYPE_CHECKING:
|
|
8
|
+
from commutator.client import CommutatorClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OptimizerModule:
|
|
12
|
+
"""Module for mathematical optimization and hardware recommendation engines."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, *, client: "CommutatorClient"):
|
|
15
|
+
self._client = client
|
|
16
|
+
|
|
17
|
+
def get_metadata(self) -> typing.List[typing.Dict[str, typing.Any]]:
|
|
18
|
+
"""Get current optimizer algorithm metadata.
|
|
19
|
+
|
|
20
|
+
GET /api/v1/optimizer
|
|
21
|
+
"""
|
|
22
|
+
response = self._client._request(method="GET", path="/api/v1/optimizer")
|
|
23
|
+
raise_for_status(response)
|
|
24
|
+
return typing.cast(
|
|
25
|
+
typing.List[typing.Dict[str, typing.Any]], response.json().get("data", [])
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def run(
|
|
29
|
+
self, *, algorithm: str, qubits: int, circuit_depth: int, **kwargs: typing.Any
|
|
30
|
+
) -> typing.Dict[str, typing.Any]:
|
|
31
|
+
"""Configure or create a new optimizer run.
|
|
32
|
+
|
|
33
|
+
POST /api/v1/optimizer
|
|
34
|
+
"""
|
|
35
|
+
payload = {"algorithm": algorithm, "qubits": qubits, "circuit_depth": circuit_depth}
|
|
36
|
+
payload.update(kwargs)
|
|
37
|
+
response = self._client._request(method="POST", path="/api/v1/optimizer", json=payload)
|
|
38
|
+
raise_for_status(response)
|
|
39
|
+
return typing.cast(typing.Dict[str, typing.Any], response.json().get("data", {}))
|
|
40
|
+
|
|
41
|
+
def recommend(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
execution_id: str,
|
|
45
|
+
target_backend: typing.Optional[str] = None,
|
|
46
|
+
target_dd: bool = True,
|
|
47
|
+
plan_only: bool = True,
|
|
48
|
+
) -> typing.List[typing.Dict[str, typing.Any]]:
|
|
49
|
+
"""Request optimization and hardware recommendations.
|
|
50
|
+
|
|
51
|
+
POST /api/v1/optimizer/recommendations
|
|
52
|
+
"""
|
|
53
|
+
payload = {
|
|
54
|
+
"executionId": execution_id,
|
|
55
|
+
"targetBackend": target_backend,
|
|
56
|
+
"targetDD": target_dd,
|
|
57
|
+
"planOnly": plan_only,
|
|
58
|
+
}
|
|
59
|
+
response = self._client._request(
|
|
60
|
+
method="POST", path="/api/v1/optimizer/recommendations", json=payload
|
|
61
|
+
)
|
|
62
|
+
raise_for_status(response)
|
|
63
|
+
return typing.cast(
|
|
64
|
+
typing.List[typing.Dict[str, typing.Any]],
|
|
65
|
+
response.json().get("data", {}).get("recommendations", []),
|
|
66
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: commutator-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Commutator Studios Python SDK
|
|
5
|
+
Author-email: Hadi <hadi@commutatorstudios.com>
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: requests>=2.32.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
11
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
12
|
+
Requires-Dist: responses>=0.23.0; extra == "dev"
|
|
13
|
+
Requires-Dist: black>=24.0.0; extra == "dev"
|
|
14
|
+
Requires-Dist: mypy>=1.9.0; extra == "dev"
|
|
15
|
+
Requires-Dist: rich>=13.0.0; extra == "dev"
|
|
16
|
+
Requires-Dist: pre-commit>=3.5.0; extra == "dev"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
commutator/__init__.py,sha256=qtRzSu37k2hSszzPaGRpSh4Byw8wksldIn1XPJYkSlQ,383
|
|
2
|
+
commutator/client.py,sha256=Sx2erc6ZfpT4EofYHKgU44OLAnSBmEElk8FqV9XhftQ,5355
|
|
3
|
+
commutator/exceptions.py,sha256=wsbhNebkHNJNhIM4JeStPJWu5u1jdre0eTwgcOOOSDg,3368
|
|
4
|
+
commutator/fleet.py,sha256=ssLdElZlK-AJ54Aze0PuD71vSl44o7d1Npu6QFpKOvE,2578
|
|
5
|
+
commutator/identity.py,sha256=aP0JrYKow3IGuOsZv7BZrF72Xmk9ammvdxcDXhLPRNI,3256
|
|
6
|
+
commutator/jobs.py,sha256=DvhybYwId-4jATj1QK9vdstV7rTNpbitPfun0tLyVcI,5694
|
|
7
|
+
commutator/optimizer.py,sha256=YqE7r_FUMQRrzTlxeFQxs6-JeTbhGHQTRJzHtnZEvjI,2262
|
|
8
|
+
commutator_sdk-0.1.0.dist-info/METADATA,sha256=7UV3F829pYEO-Y053QXbswtkz1FHeAeVZCvewpYb3qU,585
|
|
9
|
+
commutator_sdk-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
10
|
+
commutator_sdk-0.1.0.dist-info/top_level.txt,sha256=5AcEIflVnu7tXN66zH2MRcDEF1CgoBxwG9sNQjobuDA,11
|
|
11
|
+
commutator_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
commutator
|