haaslab 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.
- haaslab/__init__.py +19 -0
- haaslab/client.py +56 -0
- haaslab/exceptions.py +20 -0
- haaslab/jobs.py +117 -0
- haaslab/models.py +145 -0
- haaslab/tokens.py +26 -0
- haaslab-0.1.0.dist-info/METADATA +110 -0
- haaslab-0.1.0.dist-info/RECORD +9 -0
- haaslab-0.1.0.dist-info/WHEEL +4 -0
haaslab/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .client import Client
|
|
2
|
+
from .models import Job, Token, CreatedToken, JobList, Pagination
|
|
3
|
+
from .exceptions import HaasLabError, AuthError, NotFoundError, RateLimitError, ServerError
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"Client",
|
|
7
|
+
"Job",
|
|
8
|
+
"Token",
|
|
9
|
+
"CreatedToken",
|
|
10
|
+
"JobList",
|
|
11
|
+
"Pagination",
|
|
12
|
+
"HaasLabError",
|
|
13
|
+
"AuthError",
|
|
14
|
+
"NotFoundError",
|
|
15
|
+
"RateLimitError",
|
|
16
|
+
"ServerError",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
haaslab/client.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from .jobs import JobsNamespace
|
|
8
|
+
from .tokens import TokensNamespace
|
|
9
|
+
|
|
10
|
+
DEFAULT_BASE_URL = "https://haas-lab.com"
|
|
11
|
+
_SDK_VERSION = "0.1.0"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Client:
|
|
15
|
+
"""haas-lab API client.
|
|
16
|
+
|
|
17
|
+
Usage::
|
|
18
|
+
|
|
19
|
+
from haaslab import Client
|
|
20
|
+
c = Client(token="pat_xxx")
|
|
21
|
+
job = c.jobs.create("blink.bin")
|
|
22
|
+
job = c.jobs.wait(job.id)
|
|
23
|
+
print(job.serial) # not on Job; call c.jobs.serial(job.id)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
token: str | None = None,
|
|
29
|
+
base_url: str | None = None,
|
|
30
|
+
timeout: float = 30.0,
|
|
31
|
+
) -> None:
|
|
32
|
+
# The docs page and the README promise these two variables (HAAS-389).
|
|
33
|
+
token = token or os.environ.get("HAASLAB_TOKEN")
|
|
34
|
+
if not token:
|
|
35
|
+
raise ValueError("no token: pass Client(token=...) or set HAASLAB_TOKEN")
|
|
36
|
+
base_url = base_url or os.environ.get("HAASLAB_BASE_URL") or DEFAULT_BASE_URL
|
|
37
|
+
api_base = base_url.rstrip("/") + "/api/v1"
|
|
38
|
+
self._http = httpx.Client(
|
|
39
|
+
base_url=api_base,
|
|
40
|
+
headers={
|
|
41
|
+
"Authorization": f"Bearer {token}",
|
|
42
|
+
"User-Agent": f"haaslab-python/{_SDK_VERSION}",
|
|
43
|
+
},
|
|
44
|
+
timeout=timeout,
|
|
45
|
+
)
|
|
46
|
+
self.jobs = JobsNamespace(self._http)
|
|
47
|
+
self.tokens = TokensNamespace(self._http)
|
|
48
|
+
|
|
49
|
+
def close(self) -> None:
|
|
50
|
+
self._http.close()
|
|
51
|
+
|
|
52
|
+
def __enter__(self) -> "Client":
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def __exit__(self, *_) -> None:
|
|
56
|
+
self.close()
|
haaslab/exceptions.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class HaasLabError(Exception):
|
|
2
|
+
def __init__(self, message: str, status_code: int | None = None):
|
|
3
|
+
super().__init__(message)
|
|
4
|
+
self.status_code = status_code
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AuthError(HaasLabError):
|
|
8
|
+
"""401 Unauthorized — invalid or missing PAT."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NotFoundError(HaasLabError):
|
|
12
|
+
"""404 Not Found."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RateLimitError(HaasLabError):
|
|
16
|
+
"""429 Too Many Requests."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ServerError(HaasLabError):
|
|
20
|
+
"""5xx Server Error."""
|
haaslab/jobs.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Iterator, Union
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .models import Job, JobList, Pagination, TERMINAL_STATUSES
|
|
10
|
+
from .exceptions import HaasLabError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class JobsNamespace:
|
|
14
|
+
def __init__(self, http: httpx.Client) -> None:
|
|
15
|
+
self._http = http
|
|
16
|
+
|
|
17
|
+
def create(
|
|
18
|
+
self,
|
|
19
|
+
firmware: Union[str, Path, bytes],
|
|
20
|
+
name: str | None = None,
|
|
21
|
+
*,
|
|
22
|
+
profile_key: str | None = None,
|
|
23
|
+
device_id: str | None = None,
|
|
24
|
+
expected_runtime_s: int | None = None,
|
|
25
|
+
) -> Job:
|
|
26
|
+
"""Submit a firmware and book its window.
|
|
27
|
+
|
|
28
|
+
``expected_runtime_s`` is the run time you book; the job's booked window
|
|
29
|
+
is that plus the profile's flash allowance, and it is what the escrow
|
|
30
|
+
is reserved against. ``profile_key`` asks for any board of that
|
|
31
|
+
profile, ``device_id`` for one board in particular; neither means any
|
|
32
|
+
board that is online. Returns the Job in QUEUED status.
|
|
33
|
+
"""
|
|
34
|
+
if isinstance(firmware, (str, Path)):
|
|
35
|
+
path = Path(firmware)
|
|
36
|
+
data = path.read_bytes()
|
|
37
|
+
filename = path.name
|
|
38
|
+
else:
|
|
39
|
+
data = firmware
|
|
40
|
+
filename = name or "firmware.bin"
|
|
41
|
+
|
|
42
|
+
files = {"firmware": (filename, data, "application/octet-stream")}
|
|
43
|
+
fields: dict[str, str] = {}
|
|
44
|
+
if profile_key is not None:
|
|
45
|
+
fields["profile_key"] = profile_key
|
|
46
|
+
if device_id is not None:
|
|
47
|
+
fields["device_id"] = device_id
|
|
48
|
+
if expected_runtime_s is not None:
|
|
49
|
+
fields["expected_runtime_s"] = str(expected_runtime_s)
|
|
50
|
+
resp = self._http.post("/jobs", files=files, data=fields)
|
|
51
|
+
_raise_for_status(resp)
|
|
52
|
+
return Job.from_dict(resp.json())
|
|
53
|
+
|
|
54
|
+
def get(self, job_id: str) -> Job:
|
|
55
|
+
resp = self._http.get(f"/jobs/{job_id}")
|
|
56
|
+
_raise_for_status(resp)
|
|
57
|
+
return Job.from_dict(resp.json())
|
|
58
|
+
|
|
59
|
+
def list(self, limit: int = 20, page: int = 1) -> list[Job]:
|
|
60
|
+
resp = self._http.get("/jobs", params={"page": page})
|
|
61
|
+
_raise_for_status(resp)
|
|
62
|
+
body = resp.json()
|
|
63
|
+
jobs = [Job.from_dict(j) for j in body["data"]]
|
|
64
|
+
return jobs[:limit]
|
|
65
|
+
|
|
66
|
+
def serial(self, job_id: str) -> str:
|
|
67
|
+
"""Return the full archived serial log as a string."""
|
|
68
|
+
resp = self._http.get(f"/jobs/{job_id}/serial")
|
|
69
|
+
_raise_for_status(resp)
|
|
70
|
+
return resp.text
|
|
71
|
+
|
|
72
|
+
def stream_serial(self, job_id: str) -> Iterator[str]:
|
|
73
|
+
"""Yield lines from the live SSE serial stream."""
|
|
74
|
+
from httpx_sse import connect_sse
|
|
75
|
+
|
|
76
|
+
with connect_sse(self._http, "GET", f"/jobs/{job_id}/stream") as event_source:
|
|
77
|
+
for event in event_source.iter_sse():
|
|
78
|
+
if event.event == "done":
|
|
79
|
+
break
|
|
80
|
+
if event.data:
|
|
81
|
+
yield event.data
|
|
82
|
+
|
|
83
|
+
def wait(self, job_id: str, timeout: float = 120, poll_interval: float = 2.0) -> Job:
|
|
84
|
+
"""Poll until the job reaches a terminal status or timeout expires."""
|
|
85
|
+
deadline = time.monotonic() + timeout
|
|
86
|
+
while True:
|
|
87
|
+
job = self.get(job_id)
|
|
88
|
+
if job.is_terminal:
|
|
89
|
+
return job
|
|
90
|
+
remaining = deadline - time.monotonic()
|
|
91
|
+
if remaining <= 0:
|
|
92
|
+
raise HaasLabError(
|
|
93
|
+
f"Job {job_id} did not reach terminal status within {timeout}s "
|
|
94
|
+
f"(current status: {job.status})"
|
|
95
|
+
)
|
|
96
|
+
time.sleep(min(poll_interval, remaining))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _raise_for_status(resp: httpx.Response) -> None:
|
|
100
|
+
from .exceptions import AuthError, NotFoundError, RateLimitError, ServerError
|
|
101
|
+
|
|
102
|
+
if resp.status_code < 400:
|
|
103
|
+
return
|
|
104
|
+
try:
|
|
105
|
+
msg = resp.json().get("error", resp.text)
|
|
106
|
+
except Exception:
|
|
107
|
+
msg = resp.text
|
|
108
|
+
|
|
109
|
+
if resp.status_code == 401:
|
|
110
|
+
raise AuthError(msg, resp.status_code)
|
|
111
|
+
if resp.status_code == 404:
|
|
112
|
+
raise NotFoundError(msg, resp.status_code)
|
|
113
|
+
if resp.status_code == 429:
|
|
114
|
+
raise RateLimitError(msg, resp.status_code)
|
|
115
|
+
if resp.status_code >= 500:
|
|
116
|
+
raise ServerError(msg, resp.status_code)
|
|
117
|
+
raise HaasLabError(msg, resp.status_code)
|
haaslab/models.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# synced from /api/v1/openapi.json
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Literal, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Every status the server can answer with, from the schema's JobStatus enum;
|
|
8
|
+
# the six the API document used to list were a subset, and SETTLED — the
|
|
9
|
+
# state a successful job actually ends in, written by settlement in the same
|
|
10
|
+
# request as completion — was not among them, so `wait` never returned on
|
|
11
|
+
# success (review, 2026-09-05).
|
|
12
|
+
JobStatus = Literal[
|
|
13
|
+
"DRAFT",
|
|
14
|
+
"STATIC_CHECK",
|
|
15
|
+
"STATIC_CHECK_FAILED",
|
|
16
|
+
"ESTIMATED",
|
|
17
|
+
"ESCROW_HELD",
|
|
18
|
+
"QUEUED",
|
|
19
|
+
"CLAIMED",
|
|
20
|
+
"FLASHING",
|
|
21
|
+
"RUNNING",
|
|
22
|
+
"COMPLETED",
|
|
23
|
+
"FAILED",
|
|
24
|
+
"CANCELLED",
|
|
25
|
+
"DAMAGE_REVIEW",
|
|
26
|
+
"SETTLED",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# Nothing moves a job out of these. COMPLETED is included because a job that
|
|
30
|
+
# has no escrow to settle stays there; SETTLED is where a settled one ends.
|
|
31
|
+
TERMINAL_STATUSES = {"COMPLETED", "FAILED", "CANCELLED", "SETTLED", "STATIC_CHECK_FAILED"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Job:
|
|
36
|
+
id: str
|
|
37
|
+
userId: str
|
|
38
|
+
status: JobStatus
|
|
39
|
+
firmwareUrl: str
|
|
40
|
+
firmwareSize: int
|
|
41
|
+
serialLogUrl: Optional[str]
|
|
42
|
+
createdAt: str
|
|
43
|
+
startedAt: Optional[str]
|
|
44
|
+
completedAt: Optional[str]
|
|
45
|
+
# One-based place among the jobs waiting for the same boards while the job is
|
|
46
|
+
# QUEUED; None in every other status, and None from a server older than the
|
|
47
|
+
# field. Jobs are taken oldest first, so it only falls, except when a job
|
|
48
|
+
# created earlier is confirmed late or requeued after a board failed it.
|
|
49
|
+
queuePosition: Optional[int] = None
|
|
50
|
+
# The booked window (HAAS-20): the board seconds chosen at submit, the
|
|
51
|
+
# moment the window closes once the job is on a board, and why it ended.
|
|
52
|
+
# None from a server older than the fields, and for jobs from before them.
|
|
53
|
+
bookedSeconds: Optional[int] = None
|
|
54
|
+
expiresAt: Optional[str] = None
|
|
55
|
+
endedReason: Optional[str] = None
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_dict(cls, d: dict) -> "Job":
|
|
59
|
+
return cls(
|
|
60
|
+
id=d["id"],
|
|
61
|
+
userId=d["userId"],
|
|
62
|
+
status=d["status"],
|
|
63
|
+
firmwareUrl=d["firmwareUrl"],
|
|
64
|
+
firmwareSize=d["firmwareSize"],
|
|
65
|
+
serialLogUrl=d.get("serialLogUrl"),
|
|
66
|
+
createdAt=d["createdAt"],
|
|
67
|
+
startedAt=d.get("startedAt"),
|
|
68
|
+
completedAt=d.get("completedAt"),
|
|
69
|
+
queuePosition=d.get("queuePosition"),
|
|
70
|
+
bookedSeconds=d.get("bookedSeconds"),
|
|
71
|
+
expiresAt=d.get("expiresAt"),
|
|
72
|
+
endedReason=d.get("endedReason"),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def is_terminal(self) -> bool:
|
|
77
|
+
return self.status in TERMINAL_STATUSES
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def is_queued(self) -> bool:
|
|
81
|
+
return self.status == "QUEUED"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class Token:
|
|
86
|
+
id: str
|
|
87
|
+
name: str
|
|
88
|
+
createdAt: str
|
|
89
|
+
lastUsedAt: Optional[str]
|
|
90
|
+
revokedAt: Optional[str]
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def from_dict(cls, d: dict) -> "Token":
|
|
94
|
+
return cls(
|
|
95
|
+
id=d["id"],
|
|
96
|
+
name=d["name"],
|
|
97
|
+
createdAt=d["createdAt"],
|
|
98
|
+
lastUsedAt=d.get("lastUsedAt"),
|
|
99
|
+
revokedAt=d.get("revokedAt"),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class CreatedToken:
|
|
105
|
+
"""Returned once on token creation — includes the plaintext PAT."""
|
|
106
|
+
id: str
|
|
107
|
+
name: str
|
|
108
|
+
createdAt: str
|
|
109
|
+
lastUsedAt: Optional[str]
|
|
110
|
+
revokedAt: Optional[str]
|
|
111
|
+
token: str
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def from_dict(cls, d: dict) -> "CreatedToken":
|
|
115
|
+
return cls(
|
|
116
|
+
id=d["id"],
|
|
117
|
+
name=d["name"],
|
|
118
|
+
createdAt=d["createdAt"],
|
|
119
|
+
lastUsedAt=d.get("lastUsedAt"),
|
|
120
|
+
revokedAt=d.get("revokedAt"),
|
|
121
|
+
token=d["token"],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class Pagination:
|
|
127
|
+
page: int
|
|
128
|
+
pageSize: int
|
|
129
|
+
total: int
|
|
130
|
+
totalPages: int
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def from_dict(cls, d: dict) -> "Pagination":
|
|
134
|
+
return cls(
|
|
135
|
+
page=d["page"],
|
|
136
|
+
pageSize=d["pageSize"],
|
|
137
|
+
total=d["total"],
|
|
138
|
+
totalPages=d["totalPages"],
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class JobList:
|
|
144
|
+
data: list[Job]
|
|
145
|
+
meta: Pagination
|
haaslab/tokens.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from .models import Token, CreatedToken
|
|
6
|
+
from .jobs import _raise_for_status
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TokensNamespace:
|
|
10
|
+
def __init__(self, http: httpx.Client) -> None:
|
|
11
|
+
self._http = http
|
|
12
|
+
|
|
13
|
+
def list(self) -> list[Token]:
|
|
14
|
+
resp = self._http.get("/tokens")
|
|
15
|
+
_raise_for_status(resp)
|
|
16
|
+
return [Token.from_dict(t) for t in resp.json()]
|
|
17
|
+
|
|
18
|
+
def create(self, name: str) -> CreatedToken:
|
|
19
|
+
"""Create a new PAT. The plaintext token is returned once in CreatedToken.token."""
|
|
20
|
+
resp = self._http.post("/tokens", json={"name": name})
|
|
21
|
+
_raise_for_status(resp)
|
|
22
|
+
return CreatedToken.from_dict(resp.json())
|
|
23
|
+
|
|
24
|
+
def revoke(self, token_id: str) -> None:
|
|
25
|
+
resp = self._http.delete(f"/tokens/{token_id}")
|
|
26
|
+
_raise_for_status(resp)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: haaslab
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the haas-lab hardware-as-a-service API
|
|
5
|
+
Project-URL: Homepage, https://haas-lab.com
|
|
6
|
+
Project-URL: Documentation, https://haas-lab.com/docs
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: httpx-sse>=0.4.0
|
|
10
|
+
Requires-Dist: httpx>=0.27.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest-httpx>=0.30.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# haaslab Python SDK
|
|
17
|
+
|
|
18
|
+
Python client for the [haas-lab](https://haas-lab.com) hardware-as-a-service API.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install haaslab
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Requires Python 3.9+. You need a Personal Access Token: create one at
|
|
27
|
+
https://haas-lab.com/dashboard/tokens after signing in.
|
|
28
|
+
|
|
29
|
+
## Quickstart
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from haaslab import Client
|
|
33
|
+
|
|
34
|
+
c = Client(token="pat_xxx") # or set HAASLAB_TOKEN and call Client()
|
|
35
|
+
job = c.jobs.create("blink.bin") # multipart upload
|
|
36
|
+
job = c.jobs.wait(job.id, timeout=120) # poll until terminal
|
|
37
|
+
print(c.jobs.serial(job.id)) # full archived log
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Live streaming
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
for line in c.jobs.stream_serial(job.id):
|
|
44
|
+
print(line)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Token management
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
tok = c.tokens.create("ci-bot")
|
|
51
|
+
print(tok.token) # plaintext PAT — shown once
|
|
52
|
+
|
|
53
|
+
c.tokens.list() # list all tokens
|
|
54
|
+
c.tokens.revoke(tok.id) # revoke a token
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API Reference
|
|
58
|
+
|
|
59
|
+
### `Client(token, base_url, timeout)`
|
|
60
|
+
|
|
61
|
+
| Param | Default | Description |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `token` | `$HAASLAB_TOKEN` | Personal Access Token (`pat_xxx`); `ValueError` if neither is set |
|
|
64
|
+
| `base_url` | `$HAASLAB_BASE_URL`, else `https://haas-lab.com` | Base URL of the server |
|
|
65
|
+
| `timeout` | `30.0` | HTTP request timeout in seconds |
|
|
66
|
+
|
|
67
|
+
### `client.jobs`
|
|
68
|
+
|
|
69
|
+
| Method | Returns | Description |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `create(firmware, name=None, *, profile_key=None, device_id=None, expected_runtime_s=None)` | `Job` | Upload a firmware `.bin` and queue it; `profile_key` asks for any board of that profile, `device_id` for one board, `expected_runtime_s` is the run time you book |
|
|
72
|
+
| `get(job_id)` | `Job` | Fetch job by ID |
|
|
73
|
+
| `list(limit=20, page=1)` | `list[Job]` | List jobs (paginated) |
|
|
74
|
+
| `serial(job_id)` | `str` | Full archived serial log |
|
|
75
|
+
| `stream_serial(job_id)` | `Iterator[str]` | Live SSE serial lines |
|
|
76
|
+
| `wait(job_id, timeout=120)` | `Job` | Poll until COMPLETED/FAILED/CANCELLED |
|
|
77
|
+
|
|
78
|
+
### `client.tokens`
|
|
79
|
+
|
|
80
|
+
| Method | Returns | Description |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `list()` | `list[Token]` | List PATs (no plaintext) |
|
|
83
|
+
| `create(name)` | `CreatedToken` | Create PAT (plaintext in `.token`, shown once) |
|
|
84
|
+
| `revoke(id)` | `None` | Revoke a PAT |
|
|
85
|
+
|
|
86
|
+
### Exceptions
|
|
87
|
+
|
|
88
|
+
| Exception | HTTP status |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `AuthError` | 401 |
|
|
91
|
+
| `NotFoundError` | 404 |
|
|
92
|
+
| `RateLimitError` | 429 |
|
|
93
|
+
| `ServerError` | 5xx |
|
|
94
|
+
| `HaasLabError` | base class / other 4xx |
|
|
95
|
+
|
|
96
|
+
## Environment variables
|
|
97
|
+
|
|
98
|
+
`Client()` reads these when `token` or `base_url` is not passed:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
export HAASLAB_TOKEN=pat_xxx # your Personal Access Token
|
|
102
|
+
export HAASLAB_BASE_URL=http://localhost:3000 # optional; defaults to https://haas-lab.com
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Development
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
pip install -e ".[dev]"
|
|
109
|
+
pytest -v
|
|
110
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
haaslab/__init__.py,sha256=vTQwD0MGrIofmvkMg5HsiCNUO8IDjfRTAoGHd-qCwB0,432
|
|
2
|
+
haaslab/client.py,sha256=yRp_vix1T2Sl38aM7p5S01w1jGONl7bSXezeXLM1V_s,1590
|
|
3
|
+
haaslab/exceptions.py,sha256=jLGBMAcgWEes-ykXGcQFJCY8zfXdhnAy6lAXrfV2mJw,478
|
|
4
|
+
haaslab/jobs.py,sha256=HNufo6tk754KO3NNf5v2npT8AzfahCwQk0FD5MC3XjI,4308
|
|
5
|
+
haaslab/models.py,sha256=EgX_8xK1pgOd0ArrQjyJo-VoGzfttnv0OGkfYPup544,4201
|
|
6
|
+
haaslab/tokens.py,sha256=9F5NArBmu4hT2dSAfCMJbfVncKgLXbz5Uu5OcT_DnrE,843
|
|
7
|
+
haaslab-0.1.0.dist-info/METADATA,sha256=d0ghaQYagKsQxuNGhoLgQ8kKJG24iK63Ojk1wgAEwso,3170
|
|
8
|
+
haaslab-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
9
|
+
haaslab-0.1.0.dist-info/RECORD,,
|