ascent-science 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.
- ascent_science-0.1.0/PKG-INFO +35 -0
- ascent_science-0.1.0/README.md +23 -0
- ascent_science-0.1.0/pyproject.toml +23 -0
- ascent_science-0.1.0/setup.cfg +4 -0
- ascent_science-0.1.0/src/ascent/__init__.py +38 -0
- ascent_science-0.1.0/src/ascent/_transport.py +86 -0
- ascent_science-0.1.0/src/ascent/async_client.py +106 -0
- ascent_science-0.1.0/src/ascent/client.py +167 -0
- ascent_science-0.1.0/src/ascent/errors.py +48 -0
- ascent_science-0.1.0/src/ascent/models.py +56 -0
- ascent_science-0.1.0/src/ascent/py.typed +1 -0
- ascent_science-0.1.0/src/ascent_science.egg-info/PKG-INFO +35 -0
- ascent_science-0.1.0/src/ascent_science.egg-info/SOURCES.txt +13 -0
- ascent_science-0.1.0/src/ascent_science.egg-info/dependency_links.txt +1 -0
- ascent_science-0.1.0/src/ascent_science.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ascent-science
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Ascent Science algorithm and evidence platform
|
|
5
|
+
Author: Ascent Science
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
9
|
+
Classifier: Typing :: Typed
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Ascent Science Python SDK
|
|
14
|
+
|
|
15
|
+
The SDK is the supported Python client for an Ascent Science server. It does
|
|
16
|
+
not install solver runtimes locally.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from ascent import Client
|
|
20
|
+
|
|
21
|
+
client = Client(endpoint="http://127.0.0.1:8000/v1", api_key="asc_live_...")
|
|
22
|
+
job = client.algorithms.submit(
|
|
23
|
+
"GraphColoring",
|
|
24
|
+
inputs={"data_size": 32},
|
|
25
|
+
parameters={"nodes": 12, "edge_density": 0.2},
|
|
26
|
+
idempotency_key="graph-demo-1",
|
|
27
|
+
)
|
|
28
|
+
result = job.wait(timeout=60)
|
|
29
|
+
print(result.result)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`ASCENT_ENDPOINT` and `ASCENT_API_KEY` can be used instead of constructor
|
|
33
|
+
arguments. API keys are created in an authenticated browser session and shown
|
|
34
|
+
only once.
|
|
35
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Ascent Science Python SDK
|
|
2
|
+
|
|
3
|
+
The SDK is the supported Python client for an Ascent Science server. It does
|
|
4
|
+
not install solver runtimes locally.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from ascent import Client
|
|
8
|
+
|
|
9
|
+
client = Client(endpoint="http://127.0.0.1:8000/v1", api_key="asc_live_...")
|
|
10
|
+
job = client.algorithms.submit(
|
|
11
|
+
"GraphColoring",
|
|
12
|
+
inputs={"data_size": 32},
|
|
13
|
+
parameters={"nodes": 12, "edge_density": 0.2},
|
|
14
|
+
idempotency_key="graph-demo-1",
|
|
15
|
+
)
|
|
16
|
+
result = job.wait(timeout=60)
|
|
17
|
+
print(result.result)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`ASCENT_ENDPOINT` and `ASCENT_API_KEY` can be used instead of constructor
|
|
21
|
+
arguments. API keys are created in an authenticated browser session and shown
|
|
22
|
+
only once.
|
|
23
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ascent-science"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for the Ascent Science algorithm and evidence platform"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "LicenseRef-Proprietary"
|
|
12
|
+
authors = [{name = "Ascent Science"}]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
16
|
+
"Typing :: Typed",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
where = ["src"]
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.package-data]
|
|
23
|
+
ascent = ["py.typed"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Public interface for the Ascent Science Python SDK."""
|
|
2
|
+
|
|
3
|
+
from .async_client import AsyncClient, AsyncJob
|
|
4
|
+
from .client import Client, Job
|
|
5
|
+
from .errors import (
|
|
6
|
+
AscentError,
|
|
7
|
+
AuthenticationError,
|
|
8
|
+
JobFailedError,
|
|
9
|
+
JobTimeoutError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
PermissionDeniedError,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
ServerError,
|
|
14
|
+
TransportError,
|
|
15
|
+
ValidationError,
|
|
16
|
+
)
|
|
17
|
+
from .models import JobResult
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"AscentError",
|
|
23
|
+
"AsyncClient",
|
|
24
|
+
"AsyncJob",
|
|
25
|
+
"AuthenticationError",
|
|
26
|
+
"Client",
|
|
27
|
+
"Job",
|
|
28
|
+
"JobFailedError",
|
|
29
|
+
"JobResult",
|
|
30
|
+
"JobTimeoutError",
|
|
31
|
+
"NotFoundError",
|
|
32
|
+
"PermissionDeniedError",
|
|
33
|
+
"RateLimitError",
|
|
34
|
+
"ServerError",
|
|
35
|
+
"TransportError",
|
|
36
|
+
"ValidationError",
|
|
37
|
+
"__version__",
|
|
38
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Small JSON HTTP transport built on the Python standard library."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import socket
|
|
7
|
+
from urllib.error import HTTPError, URLError
|
|
8
|
+
from urllib.parse import urlencode
|
|
9
|
+
from urllib.request import Request, urlopen
|
|
10
|
+
|
|
11
|
+
from .errors import (
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
NotFoundError,
|
|
14
|
+
PermissionDeniedError,
|
|
15
|
+
RateLimitError,
|
|
16
|
+
ServerError,
|
|
17
|
+
TransportError,
|
|
18
|
+
ValidationError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
ERROR_TYPES = {
|
|
23
|
+
400: ValidationError,
|
|
24
|
+
401: AuthenticationError,
|
|
25
|
+
403: PermissionDeniedError,
|
|
26
|
+
404: NotFoundError,
|
|
27
|
+
409: ValidationError,
|
|
28
|
+
422: ValidationError,
|
|
29
|
+
429: RateLimitError,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class JsonTransport:
|
|
34
|
+
def __init__(self, endpoint, api_key, timeout=30.0, user_agent="ascent-science-python/0.1.0"):
|
|
35
|
+
endpoint = str(endpoint).strip().rstrip("/")
|
|
36
|
+
if not endpoint.startswith(("http://", "https://")):
|
|
37
|
+
raise ValueError("endpoint must start with http:// or https://")
|
|
38
|
+
if not api_key:
|
|
39
|
+
raise ValueError("api_key is required")
|
|
40
|
+
self.endpoint = endpoint
|
|
41
|
+
self.api_key = str(api_key)
|
|
42
|
+
self.timeout = float(timeout)
|
|
43
|
+
self.user_agent = user_agent
|
|
44
|
+
|
|
45
|
+
def request(self, method, path, *, payload=None, query=None, headers=None, timeout=None):
|
|
46
|
+
url = self.endpoint + "/" + path.lstrip("/")
|
|
47
|
+
if query:
|
|
48
|
+
url += "?" + urlencode(query)
|
|
49
|
+
body = None
|
|
50
|
+
request_headers = {
|
|
51
|
+
"Accept": "application/json",
|
|
52
|
+
"Authorization": "Bearer " + self.api_key,
|
|
53
|
+
"User-Agent": self.user_agent,
|
|
54
|
+
}
|
|
55
|
+
if payload is not None:
|
|
56
|
+
body = json.dumps(payload, ensure_ascii=False, allow_nan=False).encode("utf-8")
|
|
57
|
+
request_headers["Content-Type"] = "application/json"
|
|
58
|
+
request_headers.update(headers or {})
|
|
59
|
+
request = Request(url, data=body, method=method, headers=request_headers)
|
|
60
|
+
try:
|
|
61
|
+
with urlopen(request, timeout=self.timeout if timeout is None else timeout) as response:
|
|
62
|
+
raw = response.read()
|
|
63
|
+
return json.loads(raw.decode("utf-8")) if raw else None
|
|
64
|
+
except HTTPError as error:
|
|
65
|
+
try:
|
|
66
|
+
raw = error.read()
|
|
67
|
+
finally:
|
|
68
|
+
error.close()
|
|
69
|
+
try:
|
|
70
|
+
response = json.loads(raw.decode("utf-8")) if raw else {}
|
|
71
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
72
|
+
response = {}
|
|
73
|
+
detail = response.get("error", response)
|
|
74
|
+
if isinstance(detail, dict):
|
|
75
|
+
message = detail.get("message") or detail.get("code") or str(detail)
|
|
76
|
+
details = detail.get("details")
|
|
77
|
+
request_id = detail.get("request_id") or error.headers.get("X-Request-Id")
|
|
78
|
+
else:
|
|
79
|
+
message, details = str(detail or error.reason), None
|
|
80
|
+
request_id = error.headers.get("X-Request-Id")
|
|
81
|
+
exception_type = ERROR_TYPES.get(error.code, ServerError if error.code >= 500 else TransportError)
|
|
82
|
+
raise exception_type(
|
|
83
|
+
message, status_code=error.code, request_id=request_id, details=details
|
|
84
|
+
) from None
|
|
85
|
+
except (URLError, socket.timeout, TimeoutError, OSError) as error:
|
|
86
|
+
raise TransportError("Unable to reach Ascent endpoint: %s" % error) from error
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Async facade for applications using asyncio."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
from .client import Client
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AsyncJob:
|
|
11
|
+
def __init__(self, job):
|
|
12
|
+
self._job = job
|
|
13
|
+
|
|
14
|
+
async def refresh(self):
|
|
15
|
+
await asyncio.to_thread(self._job.refresh)
|
|
16
|
+
return self
|
|
17
|
+
|
|
18
|
+
async def cancel(self):
|
|
19
|
+
await asyncio.to_thread(self._job.cancel)
|
|
20
|
+
return self
|
|
21
|
+
|
|
22
|
+
async def logs(self, *, after=0, limit=200):
|
|
23
|
+
return await asyncio.to_thread(self._job.logs, after=after, limit=limit)
|
|
24
|
+
|
|
25
|
+
async def wait(self, *, timeout=None, poll_interval=0.25, max_interval=3.0):
|
|
26
|
+
return await asyncio.to_thread(
|
|
27
|
+
self._job.wait, timeout=timeout, poll_interval=poll_interval, max_interval=max_interval
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def __getattr__(self, name):
|
|
31
|
+
return getattr(self._job, name)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AsyncAlgorithmsResource:
|
|
35
|
+
def __init__(self, resource):
|
|
36
|
+
self._resource = resource
|
|
37
|
+
|
|
38
|
+
async def list(self):
|
|
39
|
+
return await asyncio.to_thread(self._resource.list)
|
|
40
|
+
|
|
41
|
+
async def retrieve(self, name):
|
|
42
|
+
return await asyncio.to_thread(self._resource.retrieve, name)
|
|
43
|
+
|
|
44
|
+
async def submit(self, algorithm, **kwargs):
|
|
45
|
+
return AsyncJob(await asyncio.to_thread(self._resource.submit, algorithm, **kwargs))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AsyncJobsResource:
|
|
49
|
+
def __init__(self, resource):
|
|
50
|
+
self._resource = resource
|
|
51
|
+
|
|
52
|
+
async def list(self, *, limit=30):
|
|
53
|
+
jobs = await asyncio.to_thread(self._resource.list, limit=limit)
|
|
54
|
+
return [AsyncJob(job) for job in jobs]
|
|
55
|
+
|
|
56
|
+
async def retrieve(self, job_id):
|
|
57
|
+
return AsyncJob(await asyncio.to_thread(self._resource.retrieve, job_id))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AsyncPipelinesResource:
|
|
61
|
+
def __init__(self, resource):
|
|
62
|
+
self._resource = resource
|
|
63
|
+
|
|
64
|
+
async def submit(self, algorithms, data, **kwargs):
|
|
65
|
+
job = await asyncio.to_thread(self._resource.submit, algorithms, data, **kwargs)
|
|
66
|
+
return AsyncJob(job)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class AsyncExperimentsResource:
|
|
70
|
+
def __init__(self, resource):
|
|
71
|
+
self._resource = resource
|
|
72
|
+
|
|
73
|
+
async def list(self, *, limit=30):
|
|
74
|
+
return await asyncio.to_thread(self._resource.list, limit=limit)
|
|
75
|
+
|
|
76
|
+
async def retrieve(self, experiment_id):
|
|
77
|
+
return await asyncio.to_thread(self._resource.retrieve, experiment_id)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AsyncEvidenceResource:
|
|
81
|
+
def __init__(self, resource):
|
|
82
|
+
self._resource = resource
|
|
83
|
+
|
|
84
|
+
async def bundle(self):
|
|
85
|
+
return await asyncio.to_thread(self._resource.bundle)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class AsyncClient:
|
|
89
|
+
"""Async facade with the same configuration as :class:`Client`."""
|
|
90
|
+
|
|
91
|
+
def __init__(self, **kwargs):
|
|
92
|
+
self._client = Client(**kwargs)
|
|
93
|
+
self.algorithms = AsyncAlgorithmsResource(self._client.algorithms)
|
|
94
|
+
self.jobs = AsyncJobsResource(self._client.jobs)
|
|
95
|
+
self.pipelines = AsyncPipelinesResource(self._client.pipelines)
|
|
96
|
+
self.experiments = AsyncExperimentsResource(self._client.experiments)
|
|
97
|
+
self.evidence = AsyncEvidenceResource(self._client.evidence)
|
|
98
|
+
|
|
99
|
+
async def close(self):
|
|
100
|
+
await asyncio.to_thread(self._client.close)
|
|
101
|
+
|
|
102
|
+
async def __aenter__(self):
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
async def __aexit__(self, *args):
|
|
106
|
+
await self.close()
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Synchronous Ascent Science client and resource objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import random
|
|
7
|
+
import time
|
|
8
|
+
from urllib.parse import quote
|
|
9
|
+
|
|
10
|
+
from ._transport import JsonTransport
|
|
11
|
+
from .errors import JobFailedError, JobTimeoutError
|
|
12
|
+
from .models import JobResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Job:
|
|
16
|
+
def __init__(self, client, value):
|
|
17
|
+
self._client = client
|
|
18
|
+
self._value = JobResult.from_dict(value)
|
|
19
|
+
|
|
20
|
+
def _replace(self, value):
|
|
21
|
+
self._value = JobResult.from_dict(value)
|
|
22
|
+
return self
|
|
23
|
+
|
|
24
|
+
def refresh(self):
|
|
25
|
+
return self._replace(self._client._request("GET", "jobs/" + self.id)["job"])
|
|
26
|
+
|
|
27
|
+
def cancel(self):
|
|
28
|
+
return self._replace(self._client._request("POST", "jobs/" + self.id + "/cancel", payload={})["job"])
|
|
29
|
+
|
|
30
|
+
def logs(self, *, after=0, limit=200):
|
|
31
|
+
return self._client._request("GET", "jobs/" + self.id + "/logs", query={"after": after, "limit": limit})["logs"]
|
|
32
|
+
|
|
33
|
+
def wait(self, *, timeout=None, poll_interval=0.25, max_interval=3.0):
|
|
34
|
+
deadline = None if timeout is None else time.monotonic() + float(timeout)
|
|
35
|
+
delay = max(0.05, float(poll_interval))
|
|
36
|
+
while True:
|
|
37
|
+
self.refresh()
|
|
38
|
+
if self.done:
|
|
39
|
+
if self.status == "succeeded":
|
|
40
|
+
return self._value
|
|
41
|
+
if self.status == "timed_out":
|
|
42
|
+
raise JobTimeoutError(self.error or "The server-side job timed out.")
|
|
43
|
+
raise JobFailedError(self.error or "Job ended with status %s." % self.status)
|
|
44
|
+
if deadline is not None and time.monotonic() >= deadline:
|
|
45
|
+
raise JobTimeoutError("Timed out while waiting for job %s." % self.id)
|
|
46
|
+
remaining = None if deadline is None else max(0.0, deadline - time.monotonic())
|
|
47
|
+
sleep_for = delay + random.uniform(0.0, min(0.1, delay / 4))
|
|
48
|
+
time.sleep(sleep_for if remaining is None else min(sleep_for, remaining))
|
|
49
|
+
delay = min(float(max_interval), delay * 1.5)
|
|
50
|
+
|
|
51
|
+
def __getattr__(self, name):
|
|
52
|
+
try:
|
|
53
|
+
return getattr(self._value, name)
|
|
54
|
+
except AttributeError:
|
|
55
|
+
raise AttributeError(name) from None
|
|
56
|
+
|
|
57
|
+
def __repr__(self):
|
|
58
|
+
return "Job(id=%r, status=%r)" % (self.id, self.status)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AlgorithmsResource:
|
|
62
|
+
def __init__(self, client):
|
|
63
|
+
self._client = client
|
|
64
|
+
|
|
65
|
+
def list(self):
|
|
66
|
+
return self._client._request("GET", "algorithms")["categories"]
|
|
67
|
+
|
|
68
|
+
def retrieve(self, name):
|
|
69
|
+
return self._client._request("GET", "algorithms/" + quote(str(name), safe=""))
|
|
70
|
+
|
|
71
|
+
def submit(
|
|
72
|
+
self,
|
|
73
|
+
algorithm,
|
|
74
|
+
*,
|
|
75
|
+
inputs=None,
|
|
76
|
+
parameters=None,
|
|
77
|
+
execution=None,
|
|
78
|
+
dataset_version_id=None,
|
|
79
|
+
idempotency_key=None
|
|
80
|
+
):
|
|
81
|
+
payload = {
|
|
82
|
+
"kind": "algorithm",
|
|
83
|
+
"algorithm": algorithm,
|
|
84
|
+
"input": inputs or {},
|
|
85
|
+
"parameters": parameters or {},
|
|
86
|
+
"execution": execution or {},
|
|
87
|
+
}
|
|
88
|
+
if dataset_version_id:
|
|
89
|
+
payload["dataset_version_id"] = dataset_version_id
|
|
90
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
91
|
+
response = self._client._request("POST", "jobs", payload=payload, headers=headers)
|
|
92
|
+
return Job(self._client, response["job"])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class JobsResource:
|
|
96
|
+
def __init__(self, client):
|
|
97
|
+
self._client = client
|
|
98
|
+
|
|
99
|
+
def list(self, *, limit=30):
|
|
100
|
+
return [Job(self._client, item) for item in self._client._request("GET", "jobs", query={"limit": limit})["jobs"]]
|
|
101
|
+
|
|
102
|
+
def retrieve(self, job_id):
|
|
103
|
+
return Job(self._client, self._client._request("GET", "jobs/" + str(job_id))["job"])
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class PipelinesResource:
|
|
107
|
+
def __init__(self, client):
|
|
108
|
+
self._client = client
|
|
109
|
+
|
|
110
|
+
def submit(self, algorithms, data, *, iterations=50, execution=None, idempotency_key=None):
|
|
111
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
112
|
+
response = self._client._request(
|
|
113
|
+
"POST",
|
|
114
|
+
"jobs",
|
|
115
|
+
payload={
|
|
116
|
+
"kind": "pipeline",
|
|
117
|
+
"input": {"algorithms": list(algorithms), "data": list(data), "iterations": iterations},
|
|
118
|
+
"execution": execution or {},
|
|
119
|
+
},
|
|
120
|
+
headers=headers,
|
|
121
|
+
)
|
|
122
|
+
return Job(self._client, response["job"])
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ExperimentsResource:
|
|
126
|
+
def __init__(self, client):
|
|
127
|
+
self._client = client
|
|
128
|
+
|
|
129
|
+
def list(self, *, limit=30):
|
|
130
|
+
return self._client._request("GET", "experiments", query={"limit": limit})["experiments"]
|
|
131
|
+
|
|
132
|
+
def retrieve(self, experiment_id):
|
|
133
|
+
return self._client._request("GET", "experiments/" + str(experiment_id))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class EvidenceResource:
|
|
137
|
+
def __init__(self, client):
|
|
138
|
+
self._client = client
|
|
139
|
+
|
|
140
|
+
def bundle(self):
|
|
141
|
+
return self._client._request("GET", "evidence")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Client:
|
|
145
|
+
"""Client for the versioned Ascent HTTP API."""
|
|
146
|
+
|
|
147
|
+
def __init__(self, *, endpoint=None, api_key=None, timeout=30.0):
|
|
148
|
+
endpoint = endpoint or os.environ.get("ASCENT_ENDPOINT", "http://127.0.0.1:8000/v1")
|
|
149
|
+
api_key = api_key or os.environ.get("ASCENT_API_KEY") or os.environ.get("ASCENT_TOKEN")
|
|
150
|
+
self._transport = JsonTransport(endpoint, api_key, timeout=timeout)
|
|
151
|
+
self.algorithms = AlgorithmsResource(self)
|
|
152
|
+
self.jobs = JobsResource(self)
|
|
153
|
+
self.pipelines = PipelinesResource(self)
|
|
154
|
+
self.experiments = ExperimentsResource(self)
|
|
155
|
+
self.evidence = EvidenceResource(self)
|
|
156
|
+
|
|
157
|
+
def _request(self, method, path, **kwargs):
|
|
158
|
+
return self._transport.request(method, path, **kwargs)
|
|
159
|
+
|
|
160
|
+
def close(self):
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
def __enter__(self):
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
def __exit__(self, *args):
|
|
167
|
+
self.close()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Public SDK exception hierarchy."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AscentError(Exception):
|
|
5
|
+
"""Base class for all SDK errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message, *, status_code=None, request_id=None, details=None):
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.status_code = status_code
|
|
10
|
+
self.request_id = request_id
|
|
11
|
+
self.details = details
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuthenticationError(AscentError):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PermissionDeniedError(AscentError):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NotFoundError(AscentError):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ValidationError(AscentError):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RateLimitError(AscentError):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ServerError(AscentError):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TransportError(AscentError):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class JobFailedError(AscentError):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class JobTimeoutError(AscentError):
|
|
47
|
+
pass
|
|
48
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Typed response objects exposed by the SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
TERMINAL_JOB_STATUSES = frozenset({"succeeded", "failed", "cancelled", "timed_out"})
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class JobResult:
|
|
14
|
+
id: str
|
|
15
|
+
kind: str
|
|
16
|
+
status: str
|
|
17
|
+
payload: Dict[str, Any]
|
|
18
|
+
result: Optional[Dict[str, Any]]
|
|
19
|
+
error: Optional[str]
|
|
20
|
+
created_at: str
|
|
21
|
+
started_at: Optional[str]
|
|
22
|
+
completed_at: Optional[str]
|
|
23
|
+
raw: Dict[str, Any]
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_dict(cls, value):
|
|
27
|
+
return cls(
|
|
28
|
+
id=value["id"],
|
|
29
|
+
kind=value["kind"],
|
|
30
|
+
status=value["status"],
|
|
31
|
+
payload=value.get("payload") or {},
|
|
32
|
+
result=value.get("result"),
|
|
33
|
+
error=value.get("error"),
|
|
34
|
+
created_at=value["created_at"],
|
|
35
|
+
started_at=value.get("started_at"),
|
|
36
|
+
completed_at=value.get("completed_at"),
|
|
37
|
+
raw=dict(value),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def done(self):
|
|
42
|
+
return self.status in TERMINAL_JOB_STATUSES
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def experiment(self):
|
|
46
|
+
return (self.result or {}).get("experiment")
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def output(self):
|
|
50
|
+
return (self.result or {}).get("output")
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def fingerprint(self):
|
|
54
|
+
experiment = self.experiment or {}
|
|
55
|
+
return experiment.get("fingerprint") or experiment.get("request_fingerprint")
|
|
56
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ascent-science
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Ascent Science algorithm and evidence platform
|
|
5
|
+
Author: Ascent Science
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
9
|
+
Classifier: Typing :: Typed
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Ascent Science Python SDK
|
|
14
|
+
|
|
15
|
+
The SDK is the supported Python client for an Ascent Science server. It does
|
|
16
|
+
not install solver runtimes locally.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from ascent import Client
|
|
20
|
+
|
|
21
|
+
client = Client(endpoint="http://127.0.0.1:8000/v1", api_key="asc_live_...")
|
|
22
|
+
job = client.algorithms.submit(
|
|
23
|
+
"GraphColoring",
|
|
24
|
+
inputs={"data_size": 32},
|
|
25
|
+
parameters={"nodes": 12, "edge_density": 0.2},
|
|
26
|
+
idempotency_key="graph-demo-1",
|
|
27
|
+
)
|
|
28
|
+
result = job.wait(timeout=60)
|
|
29
|
+
print(result.result)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`ASCENT_ENDPOINT` and `ASCENT_API_KEY` can be used instead of constructor
|
|
33
|
+
arguments. API keys are created in an authenticated browser session and shown
|
|
34
|
+
only once.
|
|
35
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/ascent/__init__.py
|
|
4
|
+
src/ascent/_transport.py
|
|
5
|
+
src/ascent/async_client.py
|
|
6
|
+
src/ascent/client.py
|
|
7
|
+
src/ascent/errors.py
|
|
8
|
+
src/ascent/models.py
|
|
9
|
+
src/ascent/py.typed
|
|
10
|
+
src/ascent_science.egg-info/PKG-INFO
|
|
11
|
+
src/ascent_science.egg-info/SOURCES.txt
|
|
12
|
+
src/ascent_science.egg-info/dependency_links.txt
|
|
13
|
+
src/ascent_science.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ascent
|