getabrain 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.
- getabrain/__init__.py +57 -0
- getabrain/_http.py +85 -0
- getabrain/client.py +58 -0
- getabrain/errors.py +103 -0
- getabrain/py.typed +0 -0
- getabrain/resources/__init__.py +0 -0
- getabrain/resources/account.py +16 -0
- getabrain/resources/queries.py +68 -0
- getabrain/resources/responses.py +36 -0
- getabrain/types.py +225 -0
- getabrain-0.1.0.dist-info/METADATA +77 -0
- getabrain-0.1.0.dist-info/RECORD +13 -0
- getabrain-0.1.0.dist-info/WHEEL +4 -0
getabrain/__init__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""getabrain — official Python SDK for GetABrain.ai."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .client import GetABrain
|
|
5
|
+
from .errors import (
|
|
6
|
+
GetABrainError,
|
|
7
|
+
ValidationError,
|
|
8
|
+
AuthError,
|
|
9
|
+
InsufficientBalanceError,
|
|
10
|
+
ForbiddenError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
ConflictError,
|
|
13
|
+
ServerError,
|
|
14
|
+
RateLimitError,
|
|
15
|
+
TimeoutError,
|
|
16
|
+
NetworkError,
|
|
17
|
+
error_from_response,
|
|
18
|
+
)
|
|
19
|
+
from .types import (
|
|
20
|
+
QUERY_TYPES,
|
|
21
|
+
QueryType,
|
|
22
|
+
Query,
|
|
23
|
+
QueryResponse,
|
|
24
|
+
QueryList,
|
|
25
|
+
RatingResult,
|
|
26
|
+
Rating,
|
|
27
|
+
AccountStats,
|
|
28
|
+
BillingInfo,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__version__ = "0.1.0"
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"GetABrain",
|
|
35
|
+
"GetABrainError",
|
|
36
|
+
"ValidationError",
|
|
37
|
+
"AuthError",
|
|
38
|
+
"InsufficientBalanceError",
|
|
39
|
+
"ForbiddenError",
|
|
40
|
+
"NotFoundError",
|
|
41
|
+
"ConflictError",
|
|
42
|
+
"ServerError",
|
|
43
|
+
"RateLimitError",
|
|
44
|
+
"TimeoutError",
|
|
45
|
+
"NetworkError",
|
|
46
|
+
"error_from_response",
|
|
47
|
+
"QUERY_TYPES",
|
|
48
|
+
"QueryType",
|
|
49
|
+
"Query",
|
|
50
|
+
"QueryResponse",
|
|
51
|
+
"QueryList",
|
|
52
|
+
"RatingResult",
|
|
53
|
+
"Rating",
|
|
54
|
+
"AccountStats",
|
|
55
|
+
"BillingInfo",
|
|
56
|
+
"__version__",
|
|
57
|
+
]
|
getabrain/_http.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""HTTP transport: builds an httpx.Client and a `_request` callable with auth,
|
|
2
|
+
JSON-envelope error mapping, and GET-only retries."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Callable, Dict, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .errors import NetworkError, TimeoutError, error_from_response
|
|
11
|
+
|
|
12
|
+
DEFAULT_BASE_URL = "https://www.getabrain.ai/api/v1"
|
|
13
|
+
|
|
14
|
+
RequestFn = Callable[..., Any]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _backoff(attempt: int) -> float:
|
|
18
|
+
return min(2.0 ** (attempt - 1), 8.0)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_retry_after(value: Optional[str]) -> Optional[int]:
|
|
22
|
+
if not value:
|
|
23
|
+
return None
|
|
24
|
+
try:
|
|
25
|
+
return int(float(value) * 1000)
|
|
26
|
+
except ValueError:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_transport(
|
|
31
|
+
*,
|
|
32
|
+
api_key: str,
|
|
33
|
+
api_secret: str,
|
|
34
|
+
base_url: Optional[str] = None,
|
|
35
|
+
timeout_s: float = 30.0,
|
|
36
|
+
max_retries: int = 2,
|
|
37
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
38
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
39
|
+
) -> Tuple[httpx.Client, RequestFn]:
|
|
40
|
+
base = (base_url or DEFAULT_BASE_URL).rstrip("/")
|
|
41
|
+
client = httpx.Client(
|
|
42
|
+
timeout=timeout_s,
|
|
43
|
+
transport=transport,
|
|
44
|
+
headers={
|
|
45
|
+
"X-API-Key": api_key,
|
|
46
|
+
"X-API-Secret": api_secret,
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def request(method: str, path: str, *, body: Any = None, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
52
|
+
url = base + path
|
|
53
|
+
clean = {k: v for k, v in (params or {}).items() if v is not None}
|
|
54
|
+
is_read = method.upper() == "GET"
|
|
55
|
+
attempt = 0
|
|
56
|
+
while True:
|
|
57
|
+
try:
|
|
58
|
+
resp = client.request(method, url, json=body, params=clean or None)
|
|
59
|
+
except httpx.TimeoutException:
|
|
60
|
+
raise TimeoutError()
|
|
61
|
+
except httpx.TransportError as exc:
|
|
62
|
+
if is_read and attempt < max_retries:
|
|
63
|
+
attempt += 1
|
|
64
|
+
sleep(_backoff(attempt))
|
|
65
|
+
continue
|
|
66
|
+
raise NetworkError(str(exc))
|
|
67
|
+
|
|
68
|
+
if resp.is_success:
|
|
69
|
+
if resp.status_code == 204:
|
|
70
|
+
return None
|
|
71
|
+
return resp.json()
|
|
72
|
+
|
|
73
|
+
retry_after_ms = _parse_retry_after(resp.headers.get("retry-after"))
|
|
74
|
+
if is_read and (resp.status_code == 429 or resp.status_code >= 500) and attempt < max_retries:
|
|
75
|
+
attempt += 1
|
|
76
|
+
sleep(retry_after_ms / 1000 if retry_after_ms is not None else _backoff(attempt))
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
err_body = resp.json()
|
|
81
|
+
except Exception:
|
|
82
|
+
err_body = None
|
|
83
|
+
raise error_from_response(resp.status_code, err_body, retry_after_ms)
|
|
84
|
+
|
|
85
|
+
return client, request
|
getabrain/client.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""The GetABrain client — wires config -> transport -> resources."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
from typing import Callable, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._http import build_transport
|
|
10
|
+
from .resources.account import AccountResource
|
|
11
|
+
from .resources.queries import QueriesResource
|
|
12
|
+
from .resources.responses import ResponsesResource
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GetABrain:
|
|
16
|
+
"""Client for the GetABrain requestor API.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
gab = GetABrain(api_key="gab_k_...", api_secret="gab_s_...")
|
|
20
|
+
q = gab.queries.create(type="yes_no", title="Is it live?",
|
|
21
|
+
content_data={"question": "Is it live?"},
|
|
22
|
+
required_responses=2, bid_amount_cents=50)
|
|
23
|
+
responses = gab.queries.wait_for_responses(q["id"], min_responses=2)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
api_key: str,
|
|
29
|
+
api_secret: str,
|
|
30
|
+
*,
|
|
31
|
+
base_url: Optional[str] = None,
|
|
32
|
+
timeout_s: float = 30.0,
|
|
33
|
+
max_retries: int = 2,
|
|
34
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
35
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
36
|
+
) -> None:
|
|
37
|
+
self._client, request = build_transport(
|
|
38
|
+
api_key=api_key,
|
|
39
|
+
api_secret=api_secret,
|
|
40
|
+
base_url=base_url,
|
|
41
|
+
timeout_s=timeout_s,
|
|
42
|
+
max_retries=max_retries,
|
|
43
|
+
transport=transport,
|
|
44
|
+
sleep=sleep,
|
|
45
|
+
)
|
|
46
|
+
self.queries = QueriesResource(request)
|
|
47
|
+
self.responses = ResponsesResource(request)
|
|
48
|
+
self.account = AccountResource(request)
|
|
49
|
+
|
|
50
|
+
def close(self) -> None:
|
|
51
|
+
"""Close the underlying HTTP client."""
|
|
52
|
+
self._client.close()
|
|
53
|
+
|
|
54
|
+
def __enter__(self) -> "GetABrain":
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
def __exit__(self, *exc: object) -> None:
|
|
58
|
+
self.close()
|
getabrain/errors.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Typed error hierarchy mirroring the GetABrain HTTP envelope."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GetABrainError(Exception):
|
|
8
|
+
"""Base error. `status` is the HTTP status (0 for client-side errors)."""
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
message: str,
|
|
13
|
+
status: int = 0,
|
|
14
|
+
code: str = "error",
|
|
15
|
+
request_id: Optional[str] = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.message = message
|
|
19
|
+
self.status = status
|
|
20
|
+
self.code = code
|
|
21
|
+
self.request_id = request_id
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ValidationError(GetABrainError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AuthError(GetABrainError):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class InsufficientBalanceError(GetABrainError):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ForbiddenError(GetABrainError):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class NotFoundError(GetABrainError):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ConflictError(GetABrainError):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ServerError(GetABrainError):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RateLimitError(GetABrainError):
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
message: str,
|
|
56
|
+
status: int = 429,
|
|
57
|
+
code: str = "error",
|
|
58
|
+
retry_after_ms: Optional[int] = None,
|
|
59
|
+
request_id: Optional[str] = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
super().__init__(message, status, code, request_id)
|
|
62
|
+
self.retry_after_ms = retry_after_ms
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TimeoutError(GetABrainError):
|
|
66
|
+
"""Request timed out, or wait_for_responses exceeded its deadline.
|
|
67
|
+
|
|
68
|
+
Subclasses GetABrainError (NOT the builtin TimeoutError) so callers can
|
|
69
|
+
`except GetABrainError`.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, message: str = "Request timed out") -> None:
|
|
73
|
+
super().__init__(message, 0, "timeout")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class NetworkError(GetABrainError):
|
|
77
|
+
def __init__(self, message: str = "Network request failed") -> None:
|
|
78
|
+
super().__init__(message, 0, "network")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def error_from_response(
|
|
82
|
+
status: int, body: Any, retry_after_ms: Optional[int] = None
|
|
83
|
+
) -> GetABrainError:
|
|
84
|
+
b = body if isinstance(body, dict) else {}
|
|
85
|
+
message = b.get("message") or b.get("error") or f"HTTP {status}"
|
|
86
|
+
code = b.get("error") or "error"
|
|
87
|
+
if status == 400:
|
|
88
|
+
return ValidationError(message, status, code)
|
|
89
|
+
if status == 401:
|
|
90
|
+
return AuthError(message, status, code)
|
|
91
|
+
if status == 402:
|
|
92
|
+
return InsufficientBalanceError(message, status, code)
|
|
93
|
+
if status == 403:
|
|
94
|
+
return ForbiddenError(message, status, code)
|
|
95
|
+
if status == 404:
|
|
96
|
+
return NotFoundError(message, status, code)
|
|
97
|
+
if status == 409:
|
|
98
|
+
return ConflictError(message, status, code)
|
|
99
|
+
if status == 429:
|
|
100
|
+
return RateLimitError(message, status, code, retry_after_ms)
|
|
101
|
+
if status >= 500:
|
|
102
|
+
return ServerError(message, status, code)
|
|
103
|
+
return GetABrainError(message, status, code)
|
getabrain/py.typed
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Account resource (stats + billing)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .._http import RequestFn
|
|
5
|
+
from ..types import AccountStats, BillingInfo
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AccountResource:
|
|
9
|
+
def __init__(self, request: RequestFn) -> None:
|
|
10
|
+
self._request = request
|
|
11
|
+
|
|
12
|
+
def stats(self) -> AccountStats:
|
|
13
|
+
return self._request("GET", "/requestor/stats")
|
|
14
|
+
|
|
15
|
+
def balance(self) -> BillingInfo:
|
|
16
|
+
return self._request("GET", "/requestor/billing")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Queries resource."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from .._http import RequestFn
|
|
9
|
+
from ..errors import TimeoutError
|
|
10
|
+
from ..types import Query, QueryList, QueryResponse
|
|
11
|
+
|
|
12
|
+
_TERMINAL = {"completed", "cancelled", "failed", "expired"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class QueriesResource:
|
|
16
|
+
def __init__(self, request: RequestFn) -> None:
|
|
17
|
+
self._request = request
|
|
18
|
+
|
|
19
|
+
def create(self, **input: Any) -> Query:
|
|
20
|
+
return self._request("POST", "/requestor/queries", body=input)
|
|
21
|
+
|
|
22
|
+
def get(self, query_id: str) -> Query:
|
|
23
|
+
return self._request("GET", f"/requestor/queries/{quote(query_id, safe='')}")
|
|
24
|
+
|
|
25
|
+
def list(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
status: Optional[str] = None,
|
|
29
|
+
limit: Optional[int] = None,
|
|
30
|
+
offset: Optional[int] = None,
|
|
31
|
+
) -> QueryList:
|
|
32
|
+
return self._request(
|
|
33
|
+
"GET", "/requestor/queries",
|
|
34
|
+
params={"status": status, "limit": limit, "offset": offset},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def cancel(self, query_id: str) -> Dict[str, Any]:
|
|
38
|
+
return self._request(
|
|
39
|
+
"PUT", f"/requestor/queries/{quote(query_id, safe='')}",
|
|
40
|
+
body={"action": "cancel"},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def wait_for_responses(
|
|
44
|
+
self,
|
|
45
|
+
query_id: str,
|
|
46
|
+
*,
|
|
47
|
+
min_responses: int,
|
|
48
|
+
timeout_s: float = 600.0,
|
|
49
|
+
poll_interval_s: float = 5.0,
|
|
50
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
51
|
+
now: Callable[[], float] = time.monotonic,
|
|
52
|
+
) -> List[QueryResponse]:
|
|
53
|
+
deadline = now() + timeout_s
|
|
54
|
+
while True:
|
|
55
|
+
query = self.get(query_id)
|
|
56
|
+
responses = query.get("responses") or []
|
|
57
|
+
completed = query.get("completed_responses")
|
|
58
|
+
if completed is None:
|
|
59
|
+
completed = len(responses)
|
|
60
|
+
if completed >= min_responses:
|
|
61
|
+
return responses
|
|
62
|
+
if query.get("status") in _TERMINAL:
|
|
63
|
+
return responses
|
|
64
|
+
if now() >= deadline:
|
|
65
|
+
raise TimeoutError(
|
|
66
|
+
f"wait_for_responses timed out after {timeout_s}s for query {query_id}"
|
|
67
|
+
)
|
|
68
|
+
sleep(poll_interval_s)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Responses resource (ratings)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .._http import RequestFn
|
|
8
|
+
from ..types import Rating, RatingResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ResponsesResource:
|
|
12
|
+
def __init__(self, request: RequestFn) -> None:
|
|
13
|
+
self._request = request
|
|
14
|
+
|
|
15
|
+
def rate(
|
|
16
|
+
self,
|
|
17
|
+
query_id: str,
|
|
18
|
+
response_id: str,
|
|
19
|
+
*,
|
|
20
|
+
score: int,
|
|
21
|
+
feedback_text: Optional[str] = None,
|
|
22
|
+
) -> RatingResult:
|
|
23
|
+
q = quote(query_id, safe="")
|
|
24
|
+
r = quote(response_id, safe="")
|
|
25
|
+
body: dict = {"score": score}
|
|
26
|
+
if feedback_text is not None:
|
|
27
|
+
body["feedback_text"] = feedback_text
|
|
28
|
+
return self._request(
|
|
29
|
+
"POST", f"/requestor/queries/{q}/responses/{r}/rate",
|
|
30
|
+
body=body,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def get_rating(self, query_id: str, response_id: str) -> Rating:
|
|
34
|
+
q = quote(query_id, safe="")
|
|
35
|
+
r = quote(response_id, safe="")
|
|
36
|
+
return self._request("GET", f"/requestor/queries/{q}/responses/{r}/rate")
|
getabrain/types.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Query-type Literal + content/response TypedDicts (mirror of the OpenAPI contract).
|
|
2
|
+
|
|
3
|
+
All TypedDicts are `total=False`: the SDK is a thin typed client and the server is
|
|
4
|
+
the validation authority, so every field is optional at the type level. They give
|
|
5
|
+
IDE help; they do not enforce required keys at runtime.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, List, Literal, Optional, TypedDict
|
|
10
|
+
|
|
11
|
+
QUERY_TYPES = (
|
|
12
|
+
"text",
|
|
13
|
+
"multiple_choice",
|
|
14
|
+
"rating_scale",
|
|
15
|
+
"image_comparison",
|
|
16
|
+
"ranking",
|
|
17
|
+
"yes_no",
|
|
18
|
+
"sentiment",
|
|
19
|
+
"image_selection",
|
|
20
|
+
"free_form_text",
|
|
21
|
+
"video_review",
|
|
22
|
+
"audio_review",
|
|
23
|
+
"image_analysis",
|
|
24
|
+
"ab_test",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
QueryType = Literal[
|
|
28
|
+
"text",
|
|
29
|
+
"multiple_choice",
|
|
30
|
+
"rating_scale",
|
|
31
|
+
"image_comparison",
|
|
32
|
+
"ranking",
|
|
33
|
+
"yes_no",
|
|
34
|
+
"sentiment",
|
|
35
|
+
"image_selection",
|
|
36
|
+
"free_form_text",
|
|
37
|
+
"video_review",
|
|
38
|
+
"audio_review",
|
|
39
|
+
"image_analysis",
|
|
40
|
+
"ab_test",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TextContent(TypedDict, total=False):
|
|
45
|
+
question: str
|
|
46
|
+
instructions: str
|
|
47
|
+
context: str
|
|
48
|
+
min_words: int
|
|
49
|
+
max_words: int
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class MultipleChoiceContent(TypedDict, total=False):
|
|
53
|
+
question: str
|
|
54
|
+
instructions: str
|
|
55
|
+
context: str
|
|
56
|
+
options: List[Dict[str, Any]]
|
|
57
|
+
allow_multiple: bool
|
|
58
|
+
min_selections: int
|
|
59
|
+
max_selections: int
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class RatingScaleContent(TypedDict, total=False):
|
|
63
|
+
question: str
|
|
64
|
+
instructions: str
|
|
65
|
+
context: str
|
|
66
|
+
scale_type: Literal["1-5", "1-10", "0-100", "stars"]
|
|
67
|
+
scale_min: int
|
|
68
|
+
scale_max: int
|
|
69
|
+
scale_labels: Dict[str, str]
|
|
70
|
+
criteria: List[str]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ImageComparisonContent(TypedDict, total=False):
|
|
74
|
+
question: str
|
|
75
|
+
instructions: str
|
|
76
|
+
context: str
|
|
77
|
+
images: List[Dict[str, Any]]
|
|
78
|
+
comparison_type: Literal["preference", "quality", "relevance", "custom"]
|
|
79
|
+
require_reasoning: bool
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class RankingContent(TypedDict, total=False):
|
|
83
|
+
question: str
|
|
84
|
+
instructions: str
|
|
85
|
+
context: str
|
|
86
|
+
items: List[Dict[str, Any]]
|
|
87
|
+
max_items: int
|
|
88
|
+
require_all: bool
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class YesNoContent(TypedDict, total=False):
|
|
92
|
+
question: str
|
|
93
|
+
instructions: str
|
|
94
|
+
context: str
|
|
95
|
+
require_explanation: bool
|
|
96
|
+
min_explanation_words: int
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class SentimentContent(TypedDict, total=False):
|
|
100
|
+
question: str
|
|
101
|
+
instructions: str
|
|
102
|
+
context: str
|
|
103
|
+
subject: str
|
|
104
|
+
sentiment_options: List[str]
|
|
105
|
+
require_reasoning: bool
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ImageSelectionContent(TypedDict, total=False):
|
|
109
|
+
question: str
|
|
110
|
+
instructions: str
|
|
111
|
+
context: str
|
|
112
|
+
images: List[Dict[str, Any]]
|
|
113
|
+
min_selections: int
|
|
114
|
+
max_selections: int
|
|
115
|
+
selection_criteria: str
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class FreeFormTextContent(TypedDict, total=False):
|
|
119
|
+
question: str
|
|
120
|
+
instructions: str
|
|
121
|
+
context: str
|
|
122
|
+
prompt: str
|
|
123
|
+
min_characters: int
|
|
124
|
+
max_characters: int
|
|
125
|
+
format_guidelines: str
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class VideoReviewContent(TypedDict, total=False):
|
|
129
|
+
question: str
|
|
130
|
+
instructions: str
|
|
131
|
+
context: str
|
|
132
|
+
video_url: str
|
|
133
|
+
video_duration_seconds: int
|
|
134
|
+
review_aspects: List[str]
|
|
135
|
+
min_watch_time_seconds: int
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class AudioReviewContent(TypedDict, total=False):
|
|
139
|
+
question: str
|
|
140
|
+
instructions: str
|
|
141
|
+
context: str
|
|
142
|
+
audio_url: str
|
|
143
|
+
audio_duration_seconds: int
|
|
144
|
+
review_aspects: List[str]
|
|
145
|
+
transcription_required: bool
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ImageAnalysisContent(TypedDict, total=False):
|
|
149
|
+
question: str
|
|
150
|
+
image_url: str
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class AbTestContent(TypedDict, total=False):
|
|
154
|
+
question: str
|
|
155
|
+
variant_a: Dict[str, Any]
|
|
156
|
+
variant_b: Dict[str, Any]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class QueryResponse(TypedDict, total=False):
|
|
160
|
+
id: str
|
|
161
|
+
response_data: Dict[str, Any]
|
|
162
|
+
status: str
|
|
163
|
+
submitted_at: str
|
|
164
|
+
worker: Dict[str, Any]
|
|
165
|
+
rating: Optional[Dict[str, Any]]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class Query(TypedDict, total=False):
|
|
169
|
+
id: str
|
|
170
|
+
type: str
|
|
171
|
+
title: str
|
|
172
|
+
description: str
|
|
173
|
+
status: str
|
|
174
|
+
required_responses: int
|
|
175
|
+
completed_responses: int
|
|
176
|
+
bid_amount_cents: int
|
|
177
|
+
bonus_amount_cents: int
|
|
178
|
+
total_cost_cents: int
|
|
179
|
+
created_at: str
|
|
180
|
+
expires_at: Optional[str]
|
|
181
|
+
content_data: Dict[str, Any]
|
|
182
|
+
responses: List[QueryResponse]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class QueryList(TypedDict, total=False):
|
|
186
|
+
queries: List[Query]
|
|
187
|
+
total: int
|
|
188
|
+
limit: int
|
|
189
|
+
offset: int
|
|
190
|
+
has_more: bool
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class RatingResult(TypedDict, total=False):
|
|
194
|
+
response_id: str
|
|
195
|
+
score: int
|
|
196
|
+
worker_new_quality_score: float
|
|
197
|
+
worker_new_rating_count: int
|
|
198
|
+
worker_suspended: bool
|
|
199
|
+
feedback_text: Optional[str]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class Rating(TypedDict, total=False):
|
|
203
|
+
id: str
|
|
204
|
+
response_id: str
|
|
205
|
+
score: int
|
|
206
|
+
feedback_text: Optional[str]
|
|
207
|
+
created_at: str
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class AccountStats(TypedDict, total=False):
|
|
211
|
+
balance_cents: int
|
|
212
|
+
company_name: str
|
|
213
|
+
total_queries: int
|
|
214
|
+
active_queries: int
|
|
215
|
+
completed_queries: int
|
|
216
|
+
total_spent_cents: int
|
|
217
|
+
recent_queries: List[Query]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class BillingInfo(TypedDict, total=False):
|
|
221
|
+
balance_cents: int
|
|
222
|
+
company_name: str
|
|
223
|
+
transactions: List[Dict[str, Any]]
|
|
224
|
+
total: int
|
|
225
|
+
has_more: bool
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: getabrain
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for GetABrain.ai — real human judgment as an API
|
|
5
|
+
Project-URL: Homepage, https://getabrain.ai
|
|
6
|
+
Project-URL: Documentation, https://getabrain.ai/docs/api
|
|
7
|
+
Project-URL: Source, https://github.com/Guitarmaniac24/Getabrain.ai
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/Guitarmaniac24/Getabrain.ai/issues
|
|
9
|
+
Author-email: GetABrain <hello@getabrain.ai>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: ai,api,data-labeling,getabrain,human-in-the-loop,rlhf,sdk
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: httpx>=0.27
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# getabrain
|
|
25
|
+
|
|
26
|
+
Official Python SDK for [GetABrain.ai](https://getabrain.ai) — real human judgment as an API.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install getabrain
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quickstart
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from getabrain import GetABrain, InsufficientBalanceError
|
|
38
|
+
|
|
39
|
+
gab = GetABrain(api_key="gab_k_...", api_secret="gab_s_...")
|
|
40
|
+
|
|
41
|
+
# Submit a query (returns immediately)
|
|
42
|
+
query = gab.queries.create(
|
|
43
|
+
type="ab_test",
|
|
44
|
+
title="Which headline converts better?",
|
|
45
|
+
content_data={
|
|
46
|
+
"question": "Which is more compelling?",
|
|
47
|
+
"variant_a": {"description": "Save 20% today"},
|
|
48
|
+
"variant_b": {"description": "Your future self will thank you"},
|
|
49
|
+
},
|
|
50
|
+
required_responses=5,
|
|
51
|
+
bid_amount_cents=25,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Wait for humans to answer (polls for you)
|
|
55
|
+
try:
|
|
56
|
+
responses = gab.queries.wait_for_responses(query["id"], min_responses=5, timeout_s=600)
|
|
57
|
+
for r in responses:
|
|
58
|
+
print(r["response_data"])
|
|
59
|
+
except InsufficientBalanceError:
|
|
60
|
+
print("Top up your balance at https://getabrain.ai")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## API
|
|
64
|
+
|
|
65
|
+
- `gab.queries.create(type=..., title=..., content_data={...}, required_responses=..., bid_amount_cents=...)`
|
|
66
|
+
- `gab.queries.get(query_id)` · `gab.queries.list(status=..., limit=...)` · `gab.queries.cancel(query_id)`
|
|
67
|
+
- `gab.queries.wait_for_responses(query_id, min_responses=..., timeout_s=600, poll_interval_s=5)`
|
|
68
|
+
- `gab.responses.rate(query_id, response_id, score=5, feedback_text="...")` · `gab.responses.get_rating(query_id, response_id)`
|
|
69
|
+
- `gab.account.stats()` · `gab.account.balance()`
|
|
70
|
+
|
|
71
|
+
Query types: `text`, `multiple_choice`, `rating_scale`, `image_comparison`, `ranking`, `yes_no`, `sentiment`, `image_selection`, `free_form_text`, `video_review`, `audio_review`, `image_analysis`, `ab_test`.
|
|
72
|
+
|
|
73
|
+
## Errors
|
|
74
|
+
|
|
75
|
+
All errors subclass `GetABrainError`: `ValidationError`, `AuthError`, `InsufficientBalanceError`, `ForbiddenError`, `NotFoundError`, `ConflictError`, `RateLimitError` (`.retry_after_ms`), `ServerError`, `TimeoutError`, `NetworkError`.
|
|
76
|
+
|
|
77
|
+
Full API docs: https://getabrain.ai/docs/api
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
getabrain/__init__.py,sha256=CouWJ8dcQX3uZRt6OcE3ylNyGurNPp4NV0pb7F5rUOg,1055
|
|
2
|
+
getabrain/_http.py,sha256=7KkwopWIviMflGfRqUSRlS5y9Vw2K7yRWlylzssUts4,2718
|
|
3
|
+
getabrain/client.py,sha256=jTcl8sxFXqtT_le8sziYE2n_WKqrv5MINu_wrrLepgA,1785
|
|
4
|
+
getabrain/errors.py,sha256=2y_3WYNTbxTlYHkGI58KuA7oRbqVHvKCEAtGAN9hr9M,2704
|
|
5
|
+
getabrain/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
getabrain/types.py,sha256=QVUrs69Chb6zAkFZyE2RpnE2B_Lj-THn2Prh3jRfefw,4870
|
|
7
|
+
getabrain/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
getabrain/resources/account.py,sha256=T1-DygqbeinPFKckBc9WUr5UO9d9CL6vyI5orKbbNgc,454
|
|
9
|
+
getabrain/resources/queries.py,sha256=8b-65yCbYfpumiyU5L6OnMjzyR8zBaoQyCVJ9zJVub4,2190
|
|
10
|
+
getabrain/resources/responses.py,sha256=UdrmIoTrufd-_PNS4OUCGSDm7s70lTdy8UhUWdN5LEk,1052
|
|
11
|
+
getabrain-0.1.0.dist-info/METADATA,sha256=xgb03TdEA3nWo90xS7wI_Nu_hqiJCQ1O_dS_C4A-78k,3024
|
|
12
|
+
getabrain-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
13
|
+
getabrain-0.1.0.dist-info/RECORD,,
|