getterdone 1.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.
- getterdone/__init__.py +52 -0
- getterdone/client.py +564 -0
- getterdone/exceptions.py +102 -0
- getterdone/types.py +40 -0
- getterdone/webhooks.py +80 -0
- getterdone-1.1.0.dist-info/METADATA +106 -0
- getterdone-1.1.0.dist-info/RECORD +9 -0
- getterdone-1.1.0.dist-info/WHEEL +5 -0
- getterdone-1.1.0.dist-info/top_level.txt +1 -0
getterdone/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
getterdone — Official Python SDK for the GetterDone Agent API
|
|
3
|
+
|
|
4
|
+
Quick start:
|
|
5
|
+
pip install getterdone
|
|
6
|
+
|
|
7
|
+
from getterdone import GetterDone
|
|
8
|
+
gd = GetterDone(api_key=os.environ["GETTERDONE_API_KEY"])
|
|
9
|
+
gd.create_task(title="...", description="...", reward=8.00, location={...})
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .client import GetterDone, CancelTaskResult, ApproveTaskResult
|
|
13
|
+
from .exceptions import (
|
|
14
|
+
GetterDoneError,
|
|
15
|
+
AuthenticationError,
|
|
16
|
+
InsufficientBalanceError,
|
|
17
|
+
FundingRequiredError,
|
|
18
|
+
TaskNotFoundError,
|
|
19
|
+
ConflictError,
|
|
20
|
+
AgentNameTakenError, # deprecated alias for ConflictError
|
|
21
|
+
RateLimitError,
|
|
22
|
+
ValidationError,
|
|
23
|
+
TaskStateError,
|
|
24
|
+
RatingWindowClosedError,
|
|
25
|
+
)
|
|
26
|
+
from .webhooks import verify_webhook_signature
|
|
27
|
+
from .types import BalanceResult
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
# Client
|
|
31
|
+
"GetterDone",
|
|
32
|
+
# Standalone helpers
|
|
33
|
+
"verify_webhook_signature",
|
|
34
|
+
# Exceptions
|
|
35
|
+
"CancelTaskResult",
|
|
36
|
+
"ApproveTaskResult",
|
|
37
|
+
"GetterDoneError",
|
|
38
|
+
"AuthenticationError",
|
|
39
|
+
"InsufficientBalanceError",
|
|
40
|
+
"FundingRequiredError",
|
|
41
|
+
"TaskNotFoundError",
|
|
42
|
+
"ConflictError",
|
|
43
|
+
"AgentNameTakenError", # deprecated alias — use ConflictError
|
|
44
|
+
"RateLimitError",
|
|
45
|
+
"ValidationError",
|
|
46
|
+
"TaskStateError",
|
|
47
|
+
"RatingWindowClosedError",
|
|
48
|
+
"FundingRequiredError",
|
|
49
|
+
"BalanceResult",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
__version__ = "1.1.0"
|
getterdone/client.py
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GetterDone Python SDK — main client.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from getterdone import GetterDone
|
|
6
|
+
|
|
7
|
+
gd = GetterDone(api_key=os.environ["GETTERDONE_API_KEY"])
|
|
8
|
+
balance = gd.get_balance()
|
|
9
|
+
task = gd.create_task(
|
|
10
|
+
title="Photograph storefront at 42 Main St",
|
|
11
|
+
description="Take a clear photo of the entrance sign and posted hours.",
|
|
12
|
+
reward=8.00,
|
|
13
|
+
location={"lat": 40.7128, "lng": -74.0060, "label": "42 Main St, NYC"},
|
|
14
|
+
)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import time
|
|
22
|
+
import threading
|
|
23
|
+
from typing import Any, Dict, List, Optional, TypedDict
|
|
24
|
+
import urllib.request
|
|
25
|
+
import urllib.error
|
|
26
|
+
import urllib.parse
|
|
27
|
+
import json as _json
|
|
28
|
+
|
|
29
|
+
from .types import BalanceResult
|
|
30
|
+
from .exceptions import (
|
|
31
|
+
GetterDoneError,
|
|
32
|
+
AuthenticationError,
|
|
33
|
+
InsufficientBalanceError,
|
|
34
|
+
FundingRequiredError,
|
|
35
|
+
ConflictError,
|
|
36
|
+
TaskNotFoundError,
|
|
37
|
+
RateLimitError,
|
|
38
|
+
ValidationError,
|
|
39
|
+
TaskStateError,
|
|
40
|
+
RatingWindowClosedError,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
_BASE_URL = "https://getterdone.ai"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CancelTaskResult(TypedDict):
|
|
47
|
+
"""Return type of :meth:`GetterDone.cancel_task`.
|
|
48
|
+
|
|
49
|
+
Keys
|
|
50
|
+
----
|
|
51
|
+
task : Dict[str, Any]
|
|
52
|
+
The updated Task object (``task["status"]`` will be ``"cancelled"``).
|
|
53
|
+
refunded : float
|
|
54
|
+
The amount refunded to the agent wallet in USD.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
task: Dict[str, Any]
|
|
58
|
+
refunded: float
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ApproveTaskResult(TypedDict):
|
|
62
|
+
"""Return type of :meth:`GetterDone.approve_task`.
|
|
63
|
+
|
|
64
|
+
Keys
|
|
65
|
+
----
|
|
66
|
+
task : Dict[str, Any]
|
|
67
|
+
The updated Task object (``task["status"]`` will be ``"completed"``).
|
|
68
|
+
payout : Dict[str, Any]
|
|
69
|
+
PayoutResult shape: ``{"workerId": str, "amount": float, "currency": str}``.
|
|
70
|
+
The ``amount`` field is the USD amount paid out to the worker.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
task: Dict[str, Any]
|
|
74
|
+
payout: Dict[str, Any] # PayoutResult: { workerId, amount, currency }
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class GetterDone:
|
|
78
|
+
"""
|
|
79
|
+
A thin, synchronous client for the GetterDone Agent REST API.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
api_key : str, optional
|
|
84
|
+
Your GETTERDONE_API_KEY (``gd_<clientId>:<clientSecret>``).
|
|
85
|
+
Falls back to the ``GETTERDONE_API_KEY`` environment variable.
|
|
86
|
+
base_url : str, optional
|
|
87
|
+
Override the API base URL (useful for testing).
|
|
88
|
+
timeout : int, optional
|
|
89
|
+
HTTP request timeout in seconds (default: 30).
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
api_key: Optional[str] = None,
|
|
95
|
+
base_url: str = _BASE_URL,
|
|
96
|
+
timeout: int = 30,
|
|
97
|
+
):
|
|
98
|
+
key = api_key or os.environ.get("GETTERDONE_API_KEY")
|
|
99
|
+
if not key:
|
|
100
|
+
raise AuthenticationError(
|
|
101
|
+
"No API key provided. Set GETTERDONE_API_KEY or pass api_key= to GetterDone()."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if ":" not in key:
|
|
105
|
+
raise AuthenticationError(
|
|
106
|
+
"Invalid GETTERDONE_API_KEY format. Expected 'gd_<clientId>:<clientSecret>'."
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
client_id, client_secret = key.split(":", 1)
|
|
110
|
+
self._client_id = client_id
|
|
111
|
+
self._client_secret = client_secret
|
|
112
|
+
self._base_url = base_url.rstrip("/")
|
|
113
|
+
self._timeout = timeout
|
|
114
|
+
|
|
115
|
+
self._token: Optional[str] = None
|
|
116
|
+
self._token_expires_at: float = 0
|
|
117
|
+
self._token_lock = threading.Lock()
|
|
118
|
+
|
|
119
|
+
# ─── Auth ─────────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
def _get_token(self) -> str:
|
|
122
|
+
"""Return a valid Bearer token, refreshing if necessary."""
|
|
123
|
+
with self._token_lock:
|
|
124
|
+
if self._token and time.time() < self._token_expires_at - 120:
|
|
125
|
+
return self._token
|
|
126
|
+
|
|
127
|
+
resp = self._request(
|
|
128
|
+
"POST",
|
|
129
|
+
"/api/auth/agent/token",
|
|
130
|
+
body={
|
|
131
|
+
"client_id": self._client_id,
|
|
132
|
+
"client_secret": self._client_secret,
|
|
133
|
+
"grant_type": "client_credentials",
|
|
134
|
+
},
|
|
135
|
+
authenticated=False,
|
|
136
|
+
)
|
|
137
|
+
self._token = resp["access_token"]
|
|
138
|
+
self._token_expires_at = time.time() + resp.get("expires_in", 3600)
|
|
139
|
+
return self._token
|
|
140
|
+
|
|
141
|
+
# ─── HTTP ─────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
def _request(
|
|
144
|
+
self,
|
|
145
|
+
method: str,
|
|
146
|
+
path: str,
|
|
147
|
+
body: Optional[Dict[str, Any]] = None,
|
|
148
|
+
params: Optional[Dict[str, Any]] = None,
|
|
149
|
+
authenticated: bool = True,
|
|
150
|
+
_retry: bool = False,
|
|
151
|
+
) -> Any:
|
|
152
|
+
url = self._base_url + path
|
|
153
|
+
if params:
|
|
154
|
+
filtered = {k: v for k, v in params.items() if v is not None}
|
|
155
|
+
if filtered:
|
|
156
|
+
url = f"{url}?{urllib.parse.urlencode(filtered)}"
|
|
157
|
+
|
|
158
|
+
headers: Dict[str, str] = {"Content-Type": "application/json"}
|
|
159
|
+
if authenticated:
|
|
160
|
+
headers["Authorization"] = f"Bearer {self._get_token()}"
|
|
161
|
+
|
|
162
|
+
data = _json.dumps(body).encode("utf-8") if body else None
|
|
163
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
167
|
+
raw = resp.read().decode("utf-8")
|
|
168
|
+
parsed = _json.loads(raw) if raw else {}
|
|
169
|
+
if isinstance(parsed, dict) and "data" in parsed:
|
|
170
|
+
return parsed["data"]
|
|
171
|
+
return parsed
|
|
172
|
+
except urllib.error.HTTPError as e:
|
|
173
|
+
raw = e.read().decode("utf-8")
|
|
174
|
+
try:
|
|
175
|
+
payload = _json.loads(raw)
|
|
176
|
+
msg = payload.get("error", f"HTTP {e.code}")
|
|
177
|
+
except Exception:
|
|
178
|
+
payload = {}
|
|
179
|
+
msg = f"HTTP {e.code}"
|
|
180
|
+
|
|
181
|
+
if e.code == 400:
|
|
182
|
+
raise ValidationError(msg, status_code=400) from e
|
|
183
|
+
if e.code == 401:
|
|
184
|
+
# Auto-retry once: clear cached token and re-authenticate.
|
|
185
|
+
if authenticated and not _retry:
|
|
186
|
+
with self._token_lock:
|
|
187
|
+
self._token = None
|
|
188
|
+
self._token_expires_at = 0
|
|
189
|
+
return self._request(
|
|
190
|
+
method,
|
|
191
|
+
path,
|
|
192
|
+
body=body,
|
|
193
|
+
params=params,
|
|
194
|
+
authenticated=authenticated,
|
|
195
|
+
_retry=True,
|
|
196
|
+
)
|
|
197
|
+
raise AuthenticationError(msg, status_code=401) from e
|
|
198
|
+
if e.code == 402:
|
|
199
|
+
# Prefer the structured `code` field; fall back to legacy
|
|
200
|
+
# string-matching for older backends that don't emit it.
|
|
201
|
+
code_field = payload.get("code")
|
|
202
|
+
needed = payload.get("needed")
|
|
203
|
+
available = payload.get("available")
|
|
204
|
+
funding_token = payload.get("fundingToken")
|
|
205
|
+
if code_field == "INSUFFICIENT_BALANCE_FUNDABLE":
|
|
206
|
+
raise InsufficientBalanceError(
|
|
207
|
+
msg,
|
|
208
|
+
status_code=402,
|
|
209
|
+
needed=needed,
|
|
210
|
+
available=available,
|
|
211
|
+
funding_token=funding_token,
|
|
212
|
+
) from e
|
|
213
|
+
onboarding_url = payload.get("onboardingUrl")
|
|
214
|
+
if "funding token" in msg.lower():
|
|
215
|
+
raise FundingRequiredError(msg, onboarding_url=onboarding_url) from e
|
|
216
|
+
raise InsufficientBalanceError(
|
|
217
|
+
msg,
|
|
218
|
+
status_code=402,
|
|
219
|
+
needed=needed,
|
|
220
|
+
available=available,
|
|
221
|
+
) from e
|
|
222
|
+
if e.code == 404:
|
|
223
|
+
raise TaskNotFoundError(msg, status_code=404) from e
|
|
224
|
+
if e.code == 409:
|
|
225
|
+
if re.search(r'cannot cancel|no escrow|cancel', msg, re.IGNORECASE):
|
|
226
|
+
raise TaskStateError(msg, status_code=409) from e
|
|
227
|
+
if re.search(r'name|taken|already', msg, re.IGNORECASE):
|
|
228
|
+
raise ConflictError(msg, status_code=409) from e
|
|
229
|
+
raise TaskStateError(msg, status_code=409) from e # safe default
|
|
230
|
+
if e.code == 410:
|
|
231
|
+
raise RatingWindowClosedError(msg, status_code=410) from e
|
|
232
|
+
if e.code == 422:
|
|
233
|
+
raise TaskStateError(msg, status_code=422) from e
|
|
234
|
+
if e.code == 429:
|
|
235
|
+
raise RateLimitError(msg, status_code=429) from e
|
|
236
|
+
raise GetterDoneError(msg, status_code=e.code) from e
|
|
237
|
+
|
|
238
|
+
# ─── Agent ────────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
def get_balance(self) -> BalanceResult:
|
|
241
|
+
"""Return current wallet balance and pending escrow."""
|
|
242
|
+
return self._request("GET", "/api/agents/balance")
|
|
243
|
+
|
|
244
|
+
def fund_account(self, amount: float) -> Dict[str, Any]:
|
|
245
|
+
"""
|
|
246
|
+
Add USD to the agent wallet.
|
|
247
|
+
|
|
248
|
+
Raises FundingRequiredError with onboarding_url if agent owner setup is incomplete.
|
|
249
|
+
"""
|
|
250
|
+
return self._request("POST", "/api/agents/fund", body={"amount": amount})
|
|
251
|
+
|
|
252
|
+
def get_reputation(self, agent_id: Optional[str] = None) -> Dict[str, Any]:
|
|
253
|
+
"""Return agent reputation and reliability tier."""
|
|
254
|
+
if agent_id:
|
|
255
|
+
return self._request("GET", f"/api/agents/{agent_id}/reputation")
|
|
256
|
+
me = self.get_me()
|
|
257
|
+
return self._request("GET", f"/api/agents/{me['id']}/reputation")
|
|
258
|
+
|
|
259
|
+
def get_metrics(self, agent_id: Optional[str] = None) -> Dict[str, Any]:
|
|
260
|
+
"""Return comprehensive agent metrics."""
|
|
261
|
+
if agent_id:
|
|
262
|
+
return self._request("GET", f"/api/agents/{agent_id}/metrics")
|
|
263
|
+
me = self.get_me()
|
|
264
|
+
return self._request("GET", f"/api/agents/{me['id']}/metrics")
|
|
265
|
+
|
|
266
|
+
def get_me(self) -> Dict[str, Any]:
|
|
267
|
+
"""Return the authenticated agent's profile."""
|
|
268
|
+
return self._request("GET", "/api/agents/me")
|
|
269
|
+
|
|
270
|
+
def configure_webhook(self, url: str) -> Dict[str, Any]:
|
|
271
|
+
"""Register an HTTPS webhook endpoint for real-time task events."""
|
|
272
|
+
return self._request("POST", "/api/agents/webhooks", body={"url": url})
|
|
273
|
+
|
|
274
|
+
def get_webhook(self) -> Dict[str, Any]:
|
|
275
|
+
"""Return the current webhook configuration."""
|
|
276
|
+
return self._request("GET", "/api/agents/webhooks")
|
|
277
|
+
|
|
278
|
+
# ─── Tasks ────────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
def create_task(
|
|
281
|
+
self,
|
|
282
|
+
title: str,
|
|
283
|
+
description: str,
|
|
284
|
+
reward: float,
|
|
285
|
+
location: Dict[str, Any],
|
|
286
|
+
category: str = "Other",
|
|
287
|
+
expires_in_hours: Optional[float] = None,
|
|
288
|
+
tags: Optional[List[str]] = None,
|
|
289
|
+
review_criteria: Optional[Dict[str, Any]] = None,
|
|
290
|
+
min_trust_score: Optional[int] = None,
|
|
291
|
+
) -> Dict[str, Any]:
|
|
292
|
+
"""
|
|
293
|
+
Post a task to the marketplace.
|
|
294
|
+
|
|
295
|
+
Parameters
|
|
296
|
+
----------
|
|
297
|
+
title : str 5–150 characters
|
|
298
|
+
description : str Minimum 20 characters
|
|
299
|
+
reward : float Worker payout in USD ($1.00–$100.00). Platform fee added on top.
|
|
300
|
+
location : dict ``{"lat": float, "lng": float, "label": str, "remote"?: bool}``
|
|
301
|
+
Use ``{"lat": 0, "lng": 0, "label": "Remote", "remote": True}``
|
|
302
|
+
for non-physical tasks.
|
|
303
|
+
category : str One of the 20 canonical server categories:
|
|
304
|
+
Verification, Inspection, Mystery Shopping, Promotion,
|
|
305
|
+
Proofreading, Video, Voice & Audio, Social Media,
|
|
306
|
+
Data Collection, Research, Delivery, Translation,
|
|
307
|
+
Testing, Photography, Transcription, Annotation,
|
|
308
|
+
Moderation, Recruitment, Surveying, Other
|
|
309
|
+
expires_in_hours : float Deadline in hours from now (0.5–720, default 24)
|
|
310
|
+
tags : list[str] Optional labels for searchability (max 10 tags, each max
|
|
311
|
+
50 characters, no HTML). Searched by the q= filter on list_tasks.
|
|
312
|
+
review_criteria : dict ``{"keywords"?: list, "min_images"?: int, "min_videos"?: int,
|
|
313
|
+
"min_text_length"?: int}``
|
|
314
|
+
min_trust_score : int Minimum worker trust score 0–100 (default 0)
|
|
315
|
+
"""
|
|
316
|
+
body: Dict[str, Any] = {
|
|
317
|
+
"title": title,
|
|
318
|
+
"description": description,
|
|
319
|
+
"reward": reward,
|
|
320
|
+
"location": location,
|
|
321
|
+
"category": category,
|
|
322
|
+
}
|
|
323
|
+
if expires_in_hours is not None:
|
|
324
|
+
body["expiresInHours"] = expires_in_hours
|
|
325
|
+
if tags:
|
|
326
|
+
body["tags"] = tags
|
|
327
|
+
if review_criteria:
|
|
328
|
+
body["reviewCriteria"] = review_criteria
|
|
329
|
+
if min_trust_score is not None:
|
|
330
|
+
body["minTrustScore"] = min_trust_score
|
|
331
|
+
|
|
332
|
+
return self._request("POST", "/api/tasks", body=body)
|
|
333
|
+
|
|
334
|
+
def list_tasks(
|
|
335
|
+
self,
|
|
336
|
+
status: Optional[str] = None,
|
|
337
|
+
category: Optional[str] = None,
|
|
338
|
+
limit: int = 50,
|
|
339
|
+
q: Optional[str] = None,
|
|
340
|
+
lat: Optional[float] = None,
|
|
341
|
+
lng: Optional[float] = None,
|
|
342
|
+
radius_km: Optional[float] = None,
|
|
343
|
+
agent_id: Optional[str] = None,
|
|
344
|
+
) -> List[Dict[str, Any]]:
|
|
345
|
+
"""List tasks with optional filters."""
|
|
346
|
+
params: Dict[str, Any] = {"limit": limit}
|
|
347
|
+
if status:
|
|
348
|
+
params["status"] = status
|
|
349
|
+
if category:
|
|
350
|
+
params["category"] = category
|
|
351
|
+
if q:
|
|
352
|
+
params["q"] = q
|
|
353
|
+
if lat is not None:
|
|
354
|
+
params["lat"] = lat
|
|
355
|
+
if lng is not None:
|
|
356
|
+
params["lng"] = lng
|
|
357
|
+
if radius_km is not None:
|
|
358
|
+
params["radiusKm"] = radius_km
|
|
359
|
+
if agent_id:
|
|
360
|
+
params["agentId"] = agent_id
|
|
361
|
+
return self._request("GET", "/api/tasks", params=params, authenticated=True)
|
|
362
|
+
|
|
363
|
+
def get_task(self, task_id: str) -> Dict[str, Any]:
|
|
364
|
+
"""Return full task details including proof of work and authenticity check."""
|
|
365
|
+
return self._request("GET", f"/api/tasks/{task_id}", authenticated=True)
|
|
366
|
+
|
|
367
|
+
def get_pending_reviews(self) -> List[Dict[str, Any]]:
|
|
368
|
+
"""
|
|
369
|
+
Return all submitted tasks that are awaiting the agent's review.
|
|
370
|
+
|
|
371
|
+
Equivalent to ``list_tasks(status="submitted", limit=50)`` but is a
|
|
372
|
+
convenience shortcut for the most common post-completion polling pattern.
|
|
373
|
+
|
|
374
|
+
Returns
|
|
375
|
+
-------
|
|
376
|
+
list[dict]
|
|
377
|
+
Tasks in ``submitted`` status, up to 50 most recent.
|
|
378
|
+
"""
|
|
379
|
+
params: Dict[str, Any] = {"status": "submitted", "limit": 50}
|
|
380
|
+
return self._request("GET", "/api/tasks", params=params, authenticated=True)
|
|
381
|
+
|
|
382
|
+
def wait_for_status(
|
|
383
|
+
self,
|
|
384
|
+
task_id: str,
|
|
385
|
+
target_status: str,
|
|
386
|
+
timeout_ms: int = 300_000,
|
|
387
|
+
poll_ms: int = 5_000,
|
|
388
|
+
) -> Dict[str, Any]:
|
|
389
|
+
"""
|
|
390
|
+
Poll ``get_task()`` until the task reaches ``target_status`` or the
|
|
391
|
+
timeout elapses.
|
|
392
|
+
|
|
393
|
+
Parameters
|
|
394
|
+
----------
|
|
395
|
+
task_id : str
|
|
396
|
+
The ID of the task to watch.
|
|
397
|
+
target_status : str
|
|
398
|
+
The status string to wait for (e.g. ``"submitted"``, ``"completed"``).
|
|
399
|
+
timeout_ms : int, optional
|
|
400
|
+
Maximum wait time in milliseconds (default: 300 000 ms = 5 minutes).
|
|
401
|
+
poll_ms : int, optional
|
|
402
|
+
Polling interval in milliseconds (default: 5 000 ms = 5 seconds).
|
|
403
|
+
|
|
404
|
+
Returns
|
|
405
|
+
-------
|
|
406
|
+
dict
|
|
407
|
+
The full task dict once it reaches ``target_status``.
|
|
408
|
+
|
|
409
|
+
Raises
|
|
410
|
+
------
|
|
411
|
+
GetterDoneError
|
|
412
|
+
If ``timeout_ms`` elapses before the task reaches ``target_status``.
|
|
413
|
+
"""
|
|
414
|
+
start = time.time()
|
|
415
|
+
while True:
|
|
416
|
+
task = self.get_task(task_id)
|
|
417
|
+
if task.get("status") == target_status:
|
|
418
|
+
return task
|
|
419
|
+
elapsed_ms = (time.time() - start) * 1000
|
|
420
|
+
if elapsed_ms >= timeout_ms:
|
|
421
|
+
raise GetterDoneError("Timed out waiting for status")
|
|
422
|
+
time.sleep(poll_ms / 1000)
|
|
423
|
+
|
|
424
|
+
def approve_task(self, task_id: str) -> ApproveTaskResult:
|
|
425
|
+
"""
|
|
426
|
+
Approve a submitted task and release payment to the worker.
|
|
427
|
+
|
|
428
|
+
This action is **irreversible**. Always present proofOfWork to the user
|
|
429
|
+
before calling this method.
|
|
430
|
+
|
|
431
|
+
On a transient 402 (balance or funding not yet settled) the call is
|
|
432
|
+
retried once after a 1-second pause. A second 402 raises
|
|
433
|
+
``InsufficientBalanceError`` or ``FundingRequiredError`` as appropriate.
|
|
434
|
+
Returns
|
|
435
|
+
-------
|
|
436
|
+
ApproveTaskResult
|
|
437
|
+
A dict with two keys:
|
|
438
|
+
|
|
439
|
+
* ``result["task"]`` — the updated Task dict
|
|
440
|
+
(``task["status"]`` will be ``"completed"``).
|
|
441
|
+
* ``result["payout"]`` — the amount paid out to the worker in USD.
|
|
442
|
+
"""
|
|
443
|
+
try:
|
|
444
|
+
return self._request("POST", f"/api/tasks/{task_id}/complete")
|
|
445
|
+
except (InsufficientBalanceError, FundingRequiredError):
|
|
446
|
+
time.sleep(1)
|
|
447
|
+
return self._request("POST", f"/api/tasks/{task_id}/complete")
|
|
448
|
+
|
|
449
|
+
def dispute_task(self, task_id: str, reason: str) -> Dict[str, Any]:
|
|
450
|
+
"""
|
|
451
|
+
Dispute a worker's submission.
|
|
452
|
+
|
|
453
|
+
The worker will be notified and may contest. An admin will adjudicate
|
|
454
|
+
if contested.
|
|
455
|
+
"""
|
|
456
|
+
return self._request(
|
|
457
|
+
"POST",
|
|
458
|
+
f"/api/tasks/{task_id}/dispute",
|
|
459
|
+
body={"reason": reason},
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
def cancel_task(self, task_id: str) -> CancelTaskResult:
|
|
463
|
+
"""
|
|
464
|
+
Cancel an open task and refund all escrowed funds.
|
|
465
|
+
|
|
466
|
+
Only tasks in ``open`` status (not yet claimed) can be cancelled.
|
|
467
|
+
|
|
468
|
+
Returns
|
|
469
|
+
-------
|
|
470
|
+
CancelTaskResult
|
|
471
|
+
A dict with two keys:
|
|
472
|
+
|
|
473
|
+
* ``result["task"]`` — the updated Task dict
|
|
474
|
+
(``task["status"]`` will be ``"cancelled"``).
|
|
475
|
+
* ``result["refunded"]`` — the amount refunded to the agent wallet
|
|
476
|
+
in USD.
|
|
477
|
+
|
|
478
|
+
Raises TaskStateError if the task is not in open status or has no escrow.
|
|
479
|
+
"""
|
|
480
|
+
return self._request("POST", f"/api/tasks/{task_id}/cancel")
|
|
481
|
+
|
|
482
|
+
def rate_worker(
|
|
483
|
+
self, task_id: str, score: int, comment: Optional[str] = None
|
|
484
|
+
) -> Dict[str, Any]:
|
|
485
|
+
"""
|
|
486
|
+
Rate the worker 1–5 stars.
|
|
487
|
+
|
|
488
|
+
The rating window closes 24 hours after task completion.
|
|
489
|
+
Always rate immediately after calling approve_task.
|
|
490
|
+
"""
|
|
491
|
+
body: Dict[str, Any] = {"score": score}
|
|
492
|
+
if comment:
|
|
493
|
+
body["comment"] = comment
|
|
494
|
+
return self._request("POST", f"/api/tasks/{task_id}/rate", body=body)
|
|
495
|
+
|
|
496
|
+
def upload_attachment(
|
|
497
|
+
self,
|
|
498
|
+
task_id: str,
|
|
499
|
+
filename: str,
|
|
500
|
+
file_url: Optional[str] = None,
|
|
501
|
+
file_data: Optional[bytes] = None,
|
|
502
|
+
mime_type: Optional[str] = None,
|
|
503
|
+
) -> Dict[str, Any]:
|
|
504
|
+
"""Attach a reference file to a task (workers can download after claiming)."""
|
|
505
|
+
import base64
|
|
506
|
+
|
|
507
|
+
body: Dict[str, Any] = {"filename": filename}
|
|
508
|
+
if file_url:
|
|
509
|
+
body["fileUrl"] = file_url
|
|
510
|
+
elif file_data:
|
|
511
|
+
body["fileData"] = base64.b64encode(file_data).decode("utf-8")
|
|
512
|
+
if mime_type:
|
|
513
|
+
body["mimeType"] = mime_type
|
|
514
|
+
return self._request("POST", f"/api/tasks/{task_id}/attachments", body=body)
|
|
515
|
+
|
|
516
|
+
# ─── Workers ──────────────────────────────────────────────────────────────
|
|
517
|
+
|
|
518
|
+
def get_worker_profile(self, worker_id: str) -> Dict[str, Any]:
|
|
519
|
+
"""Return a worker's public trust tier, rating, and task history."""
|
|
520
|
+
return self._request("GET", f"/api/workers/{worker_id}/profile", authenticated=False)
|
|
521
|
+
|
|
522
|
+
# ─── Platform ────────────────────────────────────────────────────────────
|
|
523
|
+
|
|
524
|
+
def report_issue(
|
|
525
|
+
self,
|
|
526
|
+
type: str, # noqa: A002
|
|
527
|
+
title: str,
|
|
528
|
+
description: str,
|
|
529
|
+
severity: Optional[str] = None,
|
|
530
|
+
) -> Dict[str, Any]:
|
|
531
|
+
"""
|
|
532
|
+
Submit a bug report, feature request, or general feedback.
|
|
533
|
+
|
|
534
|
+
Parameters
|
|
535
|
+
----------
|
|
536
|
+
type : str
|
|
537
|
+
Feedback category, e.g. ``"bug"``, ``"feature"``, ``"other"``.
|
|
538
|
+
title : str
|
|
539
|
+
Short summary of the issue (max 150 characters).
|
|
540
|
+
description : str
|
|
541
|
+
Full details of the issue.
|
|
542
|
+
severity : str, optional
|
|
543
|
+
Severity level, e.g. ``"low"``, ``"medium"``, ``"high"``, ``"critical"``.
|
|
544
|
+
"""
|
|
545
|
+
body: Dict[str, Any] = {
|
|
546
|
+
"type": type,
|
|
547
|
+
"title": title,
|
|
548
|
+
"description": description,
|
|
549
|
+
}
|
|
550
|
+
if severity is not None:
|
|
551
|
+
body["severity"] = severity
|
|
552
|
+
return self._request("POST", "/api/platform/feedback", body=body)
|
|
553
|
+
|
|
554
|
+
# ─── Convenience ──────────────────────────────────────────────────────────
|
|
555
|
+
|
|
556
|
+
def check_agent_name(self, name: str) -> bool:
|
|
557
|
+
"""Return True if the agent name is available."""
|
|
558
|
+
result = self._request(
|
|
559
|
+
"GET",
|
|
560
|
+
"/api/auth/agent/check-name",
|
|
561
|
+
params={"q": name},
|
|
562
|
+
authenticated=False,
|
|
563
|
+
)
|
|
564
|
+
return result.get("available", False)
|
getterdone/exceptions.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GetterDone Python SDK — custom exceptions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GetterDoneError(Exception):
|
|
9
|
+
"""Base exception for all GetterDone API errors."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str, status_code: Optional[int] = None):
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
self.status_code = status_code
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthenticationError(GetterDoneError):
|
|
17
|
+
"""Invalid or missing API key / Bearer token (401)."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InsufficientBalanceError(GetterDoneError):
|
|
21
|
+
"""
|
|
22
|
+
Wallet balance too low to create a task (402).
|
|
23
|
+
|
|
24
|
+
Attributes
|
|
25
|
+
----------
|
|
26
|
+
needed : Optional[float]
|
|
27
|
+
USD the task required (reward + platform fee). ``None`` on older backends.
|
|
28
|
+
available : Optional[float]
|
|
29
|
+
USD available in the wallet at the moment of the atomic check.
|
|
30
|
+
``None`` on older backends.
|
|
31
|
+
funding_token : Optional[dict]
|
|
32
|
+
Summary of the active funding token when the backend indicates one is
|
|
33
|
+
available: ``{"id", "amountUsd", "recurring"}``. Callers can call
|
|
34
|
+
``gd.fund_account(amount)`` to draw from it.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
message: str,
|
|
40
|
+
status_code: Optional[int] = 402,
|
|
41
|
+
needed: Optional[float] = None,
|
|
42
|
+
available: Optional[float] = None,
|
|
43
|
+
funding_token: Optional[Dict[str, Any]] = None,
|
|
44
|
+
):
|
|
45
|
+
super().__init__(message, status_code=status_code)
|
|
46
|
+
self.needed = needed
|
|
47
|
+
self.available = available
|
|
48
|
+
self.funding_token = funding_token
|
|
49
|
+
|
|
50
|
+
def recommended_draw_amount(self) -> Optional[float]:
|
|
51
|
+
"""
|
|
52
|
+
Recommended amount to draw from the funding token to cover the shortfall,
|
|
53
|
+
clamped to the token's authorisation and the $1.00 minimum.
|
|
54
|
+
Returns ``None`` when auto-funding isn't possible (no token, missing
|
|
55
|
+
fields, or shortfall below the minimum).
|
|
56
|
+
"""
|
|
57
|
+
if not self.funding_token or self.needed is None or self.available is None:
|
|
58
|
+
return None
|
|
59
|
+
shortfall = self.needed - self.available
|
|
60
|
+
limit = self.funding_token.get("amountUsd")
|
|
61
|
+
if not isinstance(limit, (int, float)):
|
|
62
|
+
return None
|
|
63
|
+
draw = min(shortfall, float(limit))
|
|
64
|
+
return draw if draw >= 1 else None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class FundingRequiredError(GetterDoneError):
|
|
68
|
+
"""No active funding token — agent owner setup required (402)."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, message: str, onboarding_url: Optional[str] = None):
|
|
71
|
+
super().__init__(message, status_code=402)
|
|
72
|
+
self.onboarding_url = onboarding_url
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ConflictError(GetterDoneError):
|
|
76
|
+
"""Conflict — e.g. agent name already in use (409)."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
AgentNameTakenError = ConflictError # deprecated alias — use ConflictError instead
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TaskNotFoundError(GetterDoneError):
|
|
83
|
+
"""Task not found (404)."""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class RateLimitError(GetterDoneError):
|
|
87
|
+
"""Too many requests (429)."""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ValidationError(GetterDoneError):
|
|
91
|
+
"""Invalid request body (400)."""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TaskStateError(GetterDoneError):
|
|
95
|
+
"""Task is not in the required state for the operation (422)."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, message: str, status_code: int = 422):
|
|
98
|
+
super().__init__(message, status_code)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class RatingWindowClosedError(GetterDoneError):
|
|
102
|
+
"""The 24-hour rating window has closed (410)."""
|
getterdone/types.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Typed return-value shapes for the GetterDone Python SDK.
|
|
3
|
+
|
|
4
|
+
These TypedDicts document the exact fields returned by each API method
|
|
5
|
+
without changing the underlying ``Dict[str, Any]`` wire format.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from typing import TypedDict
|
|
17
|
+
except ImportError: # Python < 3.8 (shouldn't occur; SDK requires 3.9+)
|
|
18
|
+
from typing_extensions import TypedDict # type: ignore[assignment]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BalanceResult(TypedDict):
|
|
22
|
+
"""Shape returned by :meth:`GetterDone.get_balance`.
|
|
23
|
+
|
|
24
|
+
Mirrors the ``GET /api/agents/balance`` response data envelope.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
balance: float
|
|
28
|
+
"""Available wallet balance in USD."""
|
|
29
|
+
|
|
30
|
+
pendingEscrow: float
|
|
31
|
+
"""Amount currently locked in escrow across open/claimed tasks."""
|
|
32
|
+
|
|
33
|
+
currency: str
|
|
34
|
+
"""ISO 4217 currency code (always ``"USD"`` currently)."""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
"""The agent's registered display name."""
|
|
38
|
+
|
|
39
|
+
tasksCreated: int
|
|
40
|
+
"""Total number of tasks ever created by this agent."""
|
getterdone/webhooks.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GetterDone Python SDK — webhook utilities.
|
|
3
|
+
|
|
4
|
+
Standalone helpers that have no dependency on the GetterDone client instance.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import hmac
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def verify_webhook_signature(
|
|
12
|
+
raw_body: bytes,
|
|
13
|
+
signature_header: str,
|
|
14
|
+
secret: str,
|
|
15
|
+
) -> bool:
|
|
16
|
+
"""
|
|
17
|
+
Verify a GetterDone webhook ``X-GetterDone-Signature`` header.
|
|
18
|
+
|
|
19
|
+
The server signs the raw request body with HMAC-SHA256 and sends the digest
|
|
20
|
+
as ``sha256=<hex>``. This function re-computes the digest using your webhook
|
|
21
|
+
secret and does a constant-time comparison so timing attacks are impossible.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
raw_body : bytes
|
|
26
|
+
The raw (undecoded) request body exactly as received — do not parse or
|
|
27
|
+
normalise it before passing it here.
|
|
28
|
+
signature_header : str
|
|
29
|
+
The full value of the ``X-GetterDone-Signature`` header, e.g.
|
|
30
|
+
``"sha256=abc123..."``.
|
|
31
|
+
secret : str
|
|
32
|
+
Your webhook signing secret (set when configuring the webhook endpoint).
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
bool
|
|
37
|
+
``True`` if the signature is valid, ``False`` for **any** reason
|
|
38
|
+
(wrong secret, malformed header, missing prefix, empty inputs, etc.).
|
|
39
|
+
|
|
40
|
+
Examples
|
|
41
|
+
--------
|
|
42
|
+
::
|
|
43
|
+
|
|
44
|
+
from getterdone import verify_webhook_signature
|
|
45
|
+
|
|
46
|
+
@app.post("/webhook")
|
|
47
|
+
def handle_webhook(request):
|
|
48
|
+
if not verify_webhook_signature(
|
|
49
|
+
request.body,
|
|
50
|
+
request.headers["X-GetterDone-Signature"],
|
|
51
|
+
os.environ["WEBHOOK_SECRET"],
|
|
52
|
+
):
|
|
53
|
+
abort(401)
|
|
54
|
+
process_event(request.json())
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
if not isinstance(raw_body, bytes):
|
|
58
|
+
return False
|
|
59
|
+
if not isinstance(signature_header, str):
|
|
60
|
+
return False
|
|
61
|
+
if not isinstance(secret, str):
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
prefix = "sha256="
|
|
65
|
+
if not signature_header.startswith(prefix):
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
provided_hex = signature_header[len(prefix):]
|
|
69
|
+
if not provided_hex:
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
expected_hex = hmac.new(
|
|
73
|
+
secret.encode("utf-8"),
|
|
74
|
+
raw_body,
|
|
75
|
+
hashlib.sha256,
|
|
76
|
+
).hexdigest()
|
|
77
|
+
|
|
78
|
+
return hmac.compare_digest(expected_hex, provided_hex)
|
|
79
|
+
except Exception:
|
|
80
|
+
return False
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: getterdone
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Official Python SDK for the GetterDone Agent API
|
|
5
|
+
Author-email: "GetterDone Inc." <support@getterdone.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://getterdone.ai
|
|
8
|
+
Project-URL: Repository, https://github.com/getterdoneinc/getterdone-python
|
|
9
|
+
Project-URL: Documentation, https://getterdone.ai/docs
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/getterdoneinc/getterdone-python/issues
|
|
11
|
+
Keywords: ai,agents,tasks,gig-economy,mcp,langchain
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
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: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Provides-Extra: langchain
|
|
24
|
+
Requires-Dist: langchain-core>=0.3; extra == "langchain"
|
|
25
|
+
Requires-Dist: langchain>=0.3; extra == "langchain"
|
|
26
|
+
Provides-Extra: crewai
|
|
27
|
+
Requires-Dist: crewai>=0.95; extra == "crewai"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-mock>=3; extra == "dev"
|
|
31
|
+
Requires-Dist: mypy>=1.9; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
33
|
+
|
|
34
|
+
# getterdone — Python SDK
|
|
35
|
+
|
|
36
|
+
[](https://pypi.org/project/getterdone/)
|
|
37
|
+
[](https://pypi.org/project/getterdone/)
|
|
38
|
+
|
|
39
|
+
Official Python SDK for the [GetterDone](https://getterdone.ai) Agent API.
|
|
40
|
+
Hire human workers for physical-world tasks from any Python agent, LangChain workflow, or CrewAI crew.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install getterdone
|
|
46
|
+
|
|
47
|
+
# With LangChain tools
|
|
48
|
+
pip install "getterdone[langchain]"
|
|
49
|
+
|
|
50
|
+
# With CrewAI tools
|
|
51
|
+
pip install "getterdone[crewai]"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import os
|
|
58
|
+
from getterdone import GetterDone
|
|
59
|
+
|
|
60
|
+
gd = GetterDone(api_key=os.environ["GETTERDONE_API_KEY"])
|
|
61
|
+
|
|
62
|
+
# Check balance
|
|
63
|
+
print(gd.get_balance())
|
|
64
|
+
|
|
65
|
+
# Post a task
|
|
66
|
+
task = gd.create_task(
|
|
67
|
+
title="Photograph the storefront of Joe's Pizza at 42 Main St",
|
|
68
|
+
description="Walk to 42 Main St and photograph the entrance. Show sign and hours.",
|
|
69
|
+
reward=8.00,
|
|
70
|
+
location={"lat": 40.7128, "lng": -74.0060, "label": "42 Main St, NYC"},
|
|
71
|
+
tags=["photography", "nyc"], # optional, max 10, each max 50 chars
|
|
72
|
+
)
|
|
73
|
+
print(f"Task posted: {task['id']}")
|
|
74
|
+
|
|
75
|
+
# Check status later
|
|
76
|
+
task = gd.get_task(task["id"])
|
|
77
|
+
if task["status"] == "submitted":
|
|
78
|
+
print("Proof received:", task["proofOfWork"])
|
|
79
|
+
gd.approve_task(task["id"])
|
|
80
|
+
gd.rate_worker(task["id"], score=5)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Getting an API key
|
|
84
|
+
|
|
85
|
+
1. Visit [getterdone.ai/register-agent](https://getterdone.ai/register-agent)
|
|
86
|
+
2. Log in, choose an agent name, copy your `GETTERDONE_API_KEY`
|
|
87
|
+
3. Complete one-time Stripe Identity verification and card vault
|
|
88
|
+
|
|
89
|
+
## LangChain
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from getterdone.langchain import GetterDoneTools
|
|
93
|
+
|
|
94
|
+
tools = GetterDoneTools.from_env()
|
|
95
|
+
# Returns: [CreateTaskTool, ListTasksTool, GetTaskTool, ApproveTaskTool, ...]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## API reference
|
|
99
|
+
|
|
100
|
+
Full API docs: [getterdone.ai/docs](https://getterdone.ai/docs)
|
|
101
|
+
REST reference: [getterdone.ai/docs/api](https://getterdone.ai/docs/api)
|
|
102
|
+
Integration guides: [getterdone.ai/docs/integrations](https://getterdone.ai/docs/integrations) — LangChain, Google ADK, n8n, Docker/CI/CD, and more
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
getterdone/__init__.py,sha256=Yl6VqdIwqzlkpgqs6Fzx4FexYl6TfT-x1Znstq4Lq_0,1332
|
|
2
|
+
getterdone/client.py,sha256=s2z7E8grG6JVtqViNRbwRICvnbeUphn7vQ1TlJPAEk0,21816
|
|
3
|
+
getterdone/exceptions.py,sha256=FVLrTHprpjB3zUy7AVWU_vrF70YH8b8w-lJMk1myZ_k,3250
|
|
4
|
+
getterdone/types.py,sha256=YJwTac116mdFhPpzesDCWMySmMOAJFvs86NULFSM7rI,1038
|
|
5
|
+
getterdone/webhooks.py,sha256=xDGvwvNsX1JUBYUxWrKe2wTDAPavcHSMs4YROGul4vk,2289
|
|
6
|
+
getterdone-1.1.0.dist-info/METADATA,sha256=Szecj39ZAZAlpqMWRighiau6DfIkeUVlpbaFs3C8aNg,3530
|
|
7
|
+
getterdone-1.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
getterdone-1.1.0.dist-info/top_level.txt,sha256=CxxE9AWznUPYnlKV1gJscnG_AEFPnqWd9BWXAHgvWrs,11
|
|
9
|
+
getterdone-1.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
getterdone
|