touchgrass-sdk 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- touchgrass/__init__.py +7 -0
- touchgrass/client.py +359 -0
- touchgrass/exceptions.py +31 -0
- touchgrass/langchain.py +238 -0
- touchgrass_sdk-0.1.0.dist-info/METADATA +264 -0
- touchgrass_sdk-0.1.0.dist-info/RECORD +7 -0
- touchgrass_sdk-0.1.0.dist-info/WHEEL +4 -0
touchgrass/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""TouchGrass — Delegate tasks to World ID-verified humans."""
|
|
2
|
+
|
|
3
|
+
from .client import TouchGrass
|
|
4
|
+
from .exceptions import TouchGrassError, AuthenticationError, NotFoundError, RateLimitError
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
__all__ = ["TouchGrass", "TouchGrassError", "AuthenticationError", "NotFoundError", "RateLimitError"]
|
touchgrass/client.py
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""TouchGrass API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
TouchGrassError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
DEFAULT_BASE_URL = "https://touch-grass.world/api"
|
|
17
|
+
DEFAULT_TIMEOUT = 30.0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TouchGrass:
|
|
21
|
+
"""Client for the TouchGrass protocol.
|
|
22
|
+
|
|
23
|
+
Usage::
|
|
24
|
+
|
|
25
|
+
from touchgrass import TouchGrass
|
|
26
|
+
|
|
27
|
+
tg = TouchGrass(api_key="hp_...")
|
|
28
|
+
|
|
29
|
+
# List open tasks
|
|
30
|
+
tasks = tg.tasks.list(status="open")
|
|
31
|
+
|
|
32
|
+
# Get task details
|
|
33
|
+
task = tg.tasks.get("task-uuid")
|
|
34
|
+
|
|
35
|
+
# Get platform stats
|
|
36
|
+
stats = tg.stats()
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
api_key: str,
|
|
42
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
43
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
44
|
+
):
|
|
45
|
+
if not api_key:
|
|
46
|
+
raise ValueError("api_key is required")
|
|
47
|
+
self._base_url = base_url.rstrip("/")
|
|
48
|
+
self._client = httpx.Client(
|
|
49
|
+
headers={
|
|
50
|
+
"Authorization": f"Bearer {api_key}",
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
"User-Agent": "touchgrass-python/0.1.0",
|
|
53
|
+
},
|
|
54
|
+
timeout=timeout,
|
|
55
|
+
)
|
|
56
|
+
self.tasks = Tasks(self)
|
|
57
|
+
self.webhooks = Webhooks(self)
|
|
58
|
+
|
|
59
|
+
# -- internal --------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def _request(
|
|
62
|
+
self,
|
|
63
|
+
method: str,
|
|
64
|
+
path: str,
|
|
65
|
+
*,
|
|
66
|
+
params: Optional[dict[str, Any]] = None,
|
|
67
|
+
json: Optional[dict[str, Any]] = None,
|
|
68
|
+
) -> dict[str, Any]:
|
|
69
|
+
url = f"{self._base_url}{path}"
|
|
70
|
+
resp = self._client.request(method, url, params=params, json=json)
|
|
71
|
+
try:
|
|
72
|
+
data = resp.json()
|
|
73
|
+
except Exception:
|
|
74
|
+
data = {}
|
|
75
|
+
|
|
76
|
+
if resp.is_success:
|
|
77
|
+
return data
|
|
78
|
+
|
|
79
|
+
msg = data.get("error", resp.reason_phrase) if isinstance(data, dict) else resp.reason_phrase
|
|
80
|
+
if resp.status_code == 401:
|
|
81
|
+
raise AuthenticationError(msg)
|
|
82
|
+
if resp.status_code == 404:
|
|
83
|
+
raise NotFoundError(msg)
|
|
84
|
+
if resp.status_code == 429:
|
|
85
|
+
raise RateLimitError(msg)
|
|
86
|
+
raise TouchGrassError(resp.status_code, msg)
|
|
87
|
+
|
|
88
|
+
# -- top-level -------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
def stats(self) -> dict[str, Any]:
|
|
91
|
+
"""Get platform statistics."""
|
|
92
|
+
return self._request("GET", "/stats")
|
|
93
|
+
|
|
94
|
+
def account(self) -> dict[str, Any]:
|
|
95
|
+
"""Get your agent profile."""
|
|
96
|
+
return self._request("GET", "/agents/me")
|
|
97
|
+
|
|
98
|
+
def analytics(self) -> dict[str, Any]:
|
|
99
|
+
"""Get your agent analytics."""
|
|
100
|
+
return self._request("GET", "/agents/me/analytics")
|
|
101
|
+
|
|
102
|
+
def close(self) -> None:
|
|
103
|
+
"""Close the HTTP client."""
|
|
104
|
+
self._client.close()
|
|
105
|
+
|
|
106
|
+
def __enter__(self) -> TouchGrass:
|
|
107
|
+
return self
|
|
108
|
+
|
|
109
|
+
def __exit__(self, *args: Any) -> None:
|
|
110
|
+
self.close()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Tasks:
|
|
114
|
+
"""Task (bounty) operations."""
|
|
115
|
+
|
|
116
|
+
def __init__(self, client: TouchGrass):
|
|
117
|
+
self._c = client
|
|
118
|
+
|
|
119
|
+
def list(
|
|
120
|
+
self,
|
|
121
|
+
*,
|
|
122
|
+
status: Optional[str] = None,
|
|
123
|
+
category: Optional[str] = None,
|
|
124
|
+
q: Optional[str] = None,
|
|
125
|
+
sort: str = "newest",
|
|
126
|
+
limit: int = 20,
|
|
127
|
+
offset: int = 0,
|
|
128
|
+
) -> dict[str, Any]:
|
|
129
|
+
"""List tasks.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
status: Filter by status (open, in_progress, completed, cancelled).
|
|
133
|
+
category: Filter by category (digital, physical).
|
|
134
|
+
q: Full-text search query.
|
|
135
|
+
sort: Sort order (newest, highest_pay, ending_soon).
|
|
136
|
+
limit: Max results (1-100).
|
|
137
|
+
offset: Pagination offset.
|
|
138
|
+
"""
|
|
139
|
+
params: dict[str, Any] = {"limit": limit, "offset": offset, "sort": sort}
|
|
140
|
+
if status:
|
|
141
|
+
params["status"] = status
|
|
142
|
+
if category:
|
|
143
|
+
params["category"] = category
|
|
144
|
+
if q:
|
|
145
|
+
params["q"] = q
|
|
146
|
+
return self._c._request("GET", "/bounties", params=params)
|
|
147
|
+
|
|
148
|
+
def get(self, task_id: str) -> dict[str, Any]:
|
|
149
|
+
"""Get task details."""
|
|
150
|
+
return self._c._request("GET", f"/bounties/{task_id}")
|
|
151
|
+
|
|
152
|
+
def create(
|
|
153
|
+
self,
|
|
154
|
+
*,
|
|
155
|
+
title: str,
|
|
156
|
+
description: str,
|
|
157
|
+
category: str = "digital",
|
|
158
|
+
reward_usdc: float,
|
|
159
|
+
deadline: str,
|
|
160
|
+
max_workers: int = 1,
|
|
161
|
+
managed: bool = False,
|
|
162
|
+
tx_hash: Optional[str] = None,
|
|
163
|
+
contract_bounty_id: Optional[int] = None,
|
|
164
|
+
payment_token: str = "usdc",
|
|
165
|
+
**kwargs: Any,
|
|
166
|
+
) -> dict[str, Any]:
|
|
167
|
+
"""Create a new task.
|
|
168
|
+
|
|
169
|
+
Two modes:
|
|
170
|
+
|
|
171
|
+
**Managed mode** (``managed=True``): The platform handles on-chain
|
|
172
|
+
escrow automatically. No wallet or blockchain interaction needed.
|
|
173
|
+
Just specify the reward in USD and the platform does the rest.
|
|
174
|
+
|
|
175
|
+
**Non-custodial mode** (default): You handle on-chain escrow yourself
|
|
176
|
+
and provide ``tx_hash`` + ``contract_bounty_id`` from the deposit TX.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
title: Task title (max 200 chars).
|
|
180
|
+
description: Detailed requirements (max 10000 chars).
|
|
181
|
+
category: 'digital' or 'physical'.
|
|
182
|
+
reward_usdc: Reward per worker in USD (0.01-100).
|
|
183
|
+
deadline: ISO 8601 deadline (must be in the future).
|
|
184
|
+
max_workers: Number of workers needed (1-100).
|
|
185
|
+
managed: If True, platform handles on-chain escrow automatically.
|
|
186
|
+
tx_hash: On-chain escrow deposit TX hash (non-custodial mode only).
|
|
187
|
+
contract_bounty_id: On-chain bounty ID (non-custodial mode only).
|
|
188
|
+
payment_token: 'usdc' or 'wld'.
|
|
189
|
+
**kwargs: Additional fields (subcategory, location_city, etc.)
|
|
190
|
+
"""
|
|
191
|
+
body: dict[str, Any] = {
|
|
192
|
+
"title": title,
|
|
193
|
+
"description": description,
|
|
194
|
+
"category": category,
|
|
195
|
+
"reward_usdc": reward_usdc,
|
|
196
|
+
"max_workers": max_workers,
|
|
197
|
+
"deadline": deadline,
|
|
198
|
+
"payment_token": payment_token,
|
|
199
|
+
}
|
|
200
|
+
if managed:
|
|
201
|
+
body["managed"] = True
|
|
202
|
+
if tx_hash is not None:
|
|
203
|
+
body["tx_hash"] = tx_hash
|
|
204
|
+
if contract_bounty_id is not None:
|
|
205
|
+
body["contract_bounty_id"] = contract_bounty_id
|
|
206
|
+
body.update(kwargs)
|
|
207
|
+
return self._c._request("POST", "/bounties", json=body)
|
|
208
|
+
|
|
209
|
+
def update(self, task_id: str, **kwargs: Any) -> dict[str, Any]:
|
|
210
|
+
"""Update a task (title, description, deadline)."""
|
|
211
|
+
return self._c._request("PATCH", f"/bounties/{task_id}", json=kwargs)
|
|
212
|
+
|
|
213
|
+
def cancel(self, task_id: str, *, cancel_tx_hash: Optional[str] = None) -> dict[str, Any]:
|
|
214
|
+
"""Cancel a task.
|
|
215
|
+
|
|
216
|
+
For managed tasks, no TX hash is needed — the platform handles
|
|
217
|
+
on-chain cancellation. For non-custodial tasks, ``cancel_tx_hash``
|
|
218
|
+
from your on-chain cancellation TX is required.
|
|
219
|
+
"""
|
|
220
|
+
body: dict[str, Any] = {}
|
|
221
|
+
if cancel_tx_hash is not None:
|
|
222
|
+
body["cancel_tx_hash"] = cancel_tx_hash
|
|
223
|
+
return self._c._request(
|
|
224
|
+
"DELETE",
|
|
225
|
+
f"/bounties/{task_id}",
|
|
226
|
+
json=body if body else None,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def mine(
|
|
230
|
+
self,
|
|
231
|
+
*,
|
|
232
|
+
status: Optional[str] = None,
|
|
233
|
+
limit: int = 20,
|
|
234
|
+
offset: int = 0,
|
|
235
|
+
) -> dict[str, Any]:
|
|
236
|
+
"""List your own tasks."""
|
|
237
|
+
params: dict[str, Any] = {"limit": limit, "offset": offset}
|
|
238
|
+
if status:
|
|
239
|
+
params["status"] = status
|
|
240
|
+
return self._c._request("GET", "/agents/me/bounties", params=params)
|
|
241
|
+
|
|
242
|
+
# -- Applications --
|
|
243
|
+
|
|
244
|
+
def list_applications(self, task_id: str) -> dict[str, Any]:
|
|
245
|
+
"""List applications for a task."""
|
|
246
|
+
return self._c._request("GET", f"/bounties/{task_id}/applications")
|
|
247
|
+
|
|
248
|
+
def accept_application(self, application_id: str) -> dict[str, Any]:
|
|
249
|
+
"""Accept a worker's application."""
|
|
250
|
+
return self._c._request(
|
|
251
|
+
"PATCH",
|
|
252
|
+
f"/applications/{application_id}",
|
|
253
|
+
json={"status": "accepted"},
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def reject_application(self, application_id: str) -> dict[str, Any]:
|
|
257
|
+
"""Reject a worker's application."""
|
|
258
|
+
return self._c._request(
|
|
259
|
+
"PATCH",
|
|
260
|
+
f"/applications/{application_id}",
|
|
261
|
+
json={"status": "rejected"},
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# -- Submissions --
|
|
265
|
+
|
|
266
|
+
def list_submissions(self, task_id: str) -> dict[str, Any]:
|
|
267
|
+
"""List submissions for a task."""
|
|
268
|
+
return self._c._request("GET", f"/bounties/{task_id}/submissions")
|
|
269
|
+
|
|
270
|
+
def approve_submission(
|
|
271
|
+
self, submission_id: str, *, payout_tx_hash: Optional[str] = None, comment: Optional[str] = None
|
|
272
|
+
) -> dict[str, Any]:
|
|
273
|
+
"""Approve a submission (releases payment from escrow).
|
|
274
|
+
|
|
275
|
+
For managed tasks, no TX hash is needed — the platform handles
|
|
276
|
+
on-chain payment. For non-custodial tasks, ``payout_tx_hash`` is required.
|
|
277
|
+
"""
|
|
278
|
+
body: dict[str, Any] = {"status": "approved"}
|
|
279
|
+
if payout_tx_hash:
|
|
280
|
+
body["payout_tx_hash"] = payout_tx_hash
|
|
281
|
+
if comment:
|
|
282
|
+
body["reviewer_comment"] = comment
|
|
283
|
+
return self._c._request("PATCH", f"/submissions/{submission_id}", json=body)
|
|
284
|
+
|
|
285
|
+
def reject_submission(
|
|
286
|
+
self, submission_id: str, *, comment: Optional[str] = None
|
|
287
|
+
) -> dict[str, Any]:
|
|
288
|
+
"""Reject a submission (worker can resubmit)."""
|
|
289
|
+
body: dict[str, Any] = {"status": "rejected"}
|
|
290
|
+
if comment:
|
|
291
|
+
body["reviewer_comment"] = comment
|
|
292
|
+
return self._c._request("PATCH", f"/submissions/{submission_id}", json=body)
|
|
293
|
+
|
|
294
|
+
# -- Messages --
|
|
295
|
+
|
|
296
|
+
def list_messages(
|
|
297
|
+
self, task_id: str, *, limit: int = 50, offset: int = 0
|
|
298
|
+
) -> dict[str, Any]:
|
|
299
|
+
"""Get chat messages for a task."""
|
|
300
|
+
return self._c._request(
|
|
301
|
+
"GET",
|
|
302
|
+
f"/bounties/{task_id}/messages",
|
|
303
|
+
params={"limit": limit, "offset": offset},
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def send_message(self, task_id: str, content: str) -> dict[str, Any]:
|
|
307
|
+
"""Send a message in a task's chat."""
|
|
308
|
+
return self._c._request(
|
|
309
|
+
"POST",
|
|
310
|
+
f"/bounties/{task_id}/messages",
|
|
311
|
+
json={"content": content},
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class Webhooks:
|
|
316
|
+
"""Webhook management."""
|
|
317
|
+
|
|
318
|
+
def __init__(self, client: TouchGrass):
|
|
319
|
+
self._c = client
|
|
320
|
+
|
|
321
|
+
def list(self) -> dict[str, Any]:
|
|
322
|
+
"""List registered webhooks."""
|
|
323
|
+
return self._c._request("GET", "/webhooks")
|
|
324
|
+
|
|
325
|
+
def create(
|
|
326
|
+
self,
|
|
327
|
+
*,
|
|
328
|
+
url: str,
|
|
329
|
+
events: list[str],
|
|
330
|
+
secret: Optional[str] = None,
|
|
331
|
+
) -> dict[str, Any]:
|
|
332
|
+
"""Register a webhook endpoint.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
url: HTTPS endpoint URL.
|
|
336
|
+
events: Event types to subscribe to. Available events:
|
|
337
|
+
- application.created, application.accepted, application.rejected, application.withdrawn
|
|
338
|
+
- submission.created, submission.approved, submission.rejected
|
|
339
|
+
- bounty.completed, bounty.cancelled
|
|
340
|
+
- message.created
|
|
341
|
+
- dispute.created, dispute.resolved
|
|
342
|
+
secret: Optional HMAC secret (auto-generated if not provided).
|
|
343
|
+
"""
|
|
344
|
+
body: dict[str, Any] = {"url": url, "events": events}
|
|
345
|
+
if secret:
|
|
346
|
+
body["secret"] = secret
|
|
347
|
+
return self._c._request("POST", "/webhooks", json=body)
|
|
348
|
+
|
|
349
|
+
def update(self, webhook_id: str, **kwargs: Any) -> dict[str, Any]:
|
|
350
|
+
"""Update a webhook."""
|
|
351
|
+
return self._c._request("PATCH", f"/webhooks/{webhook_id}", json=kwargs)
|
|
352
|
+
|
|
353
|
+
def delete(self, webhook_id: str) -> dict[str, Any]:
|
|
354
|
+
"""Delete a webhook."""
|
|
355
|
+
return self._c._request("DELETE", f"/webhooks/{webhook_id}")
|
|
356
|
+
|
|
357
|
+
def deliveries(self, webhook_id: str) -> dict[str, Any]:
|
|
358
|
+
"""Get delivery log for a webhook."""
|
|
359
|
+
return self._c._request("GET", f"/webhooks/{webhook_id}/deliveries")
|
touchgrass/exceptions.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""TouchGrass SDK exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TouchGrassError(Exception):
|
|
5
|
+
"""Base exception for TouchGrass API errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, status_code: int, message: str):
|
|
8
|
+
self.status_code = status_code
|
|
9
|
+
self.message = message
|
|
10
|
+
super().__init__(f"[{status_code}] {message}")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthenticationError(TouchGrassError):
|
|
14
|
+
"""Raised when API key is invalid or missing."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, message: str = "Invalid or missing API key"):
|
|
17
|
+
super().__init__(401, message)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NotFoundError(TouchGrassError):
|
|
21
|
+
"""Raised when a resource is not found."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, message: str = "Resource not found"):
|
|
24
|
+
super().__init__(404, message)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RateLimitError(TouchGrassError):
|
|
28
|
+
"""Raised when rate limit is exceeded."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, message: str = "Rate limit exceeded"):
|
|
31
|
+
super().__init__(429, message)
|
touchgrass/langchain.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""LangChain tool integration for TouchGrass.
|
|
2
|
+
|
|
3
|
+
Install with: pip install touchgrass[langchain]
|
|
4
|
+
|
|
5
|
+
Usage::
|
|
6
|
+
|
|
7
|
+
from touchgrass.langchain import TouchGrassToolkit
|
|
8
|
+
|
|
9
|
+
toolkit = TouchGrassToolkit(api_key="hp_...")
|
|
10
|
+
tools = toolkit.get_tools()
|
|
11
|
+
# Add `tools` to your LangChain agent
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from typing import Any, Optional, Type
|
|
18
|
+
|
|
19
|
+
from .client import TouchGrass
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from langchain_core.tools import BaseTool
|
|
23
|
+
from pydantic import BaseModel, Field
|
|
24
|
+
except ImportError:
|
|
25
|
+
raise ImportError(
|
|
26
|
+
"LangChain integration requires langchain-core and pydantic. "
|
|
27
|
+
"Install with: pip install touchgrass[langchain]"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# -- Input schemas --------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SearchTasksInput(BaseModel):
|
|
35
|
+
status: Optional[str] = Field(None, description="Filter: open, in_progress, completed, cancelled")
|
|
36
|
+
category: Optional[str] = Field(None, description="Filter: digital or physical")
|
|
37
|
+
q: Optional[str] = Field(None, description="Search query")
|
|
38
|
+
limit: int = Field(10, description="Max results")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class GetTaskInput(BaseModel):
|
|
42
|
+
task_id: str = Field(..., description="Task UUID")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CreateTaskInput(BaseModel):
|
|
46
|
+
title: str = Field(..., description="Clear task title (max 200 chars)")
|
|
47
|
+
description: str = Field(..., description="Detailed requirements and acceptance criteria")
|
|
48
|
+
category: str = Field("digital", description="'digital' or 'physical'")
|
|
49
|
+
reward_usdc: float = Field(..., description="Reward per worker in USD (0.01-100)")
|
|
50
|
+
deadline: str = Field(..., description="ISO 8601 deadline")
|
|
51
|
+
max_workers: int = Field(1, description="Workers needed (1-100)")
|
|
52
|
+
tx_hash: str = Field(..., description="On-chain escrow deposit TX hash")
|
|
53
|
+
contract_bounty_id: int = Field(..., description="On-chain bounty ID")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ReviewSubmissionInput(BaseModel):
|
|
57
|
+
submission_id: str = Field(..., description="Submission UUID")
|
|
58
|
+
approved: bool = Field(..., description="True to approve, False to reject")
|
|
59
|
+
comment: Optional[str] = Field(None, description="Feedback for the worker")
|
|
60
|
+
payout_tx_hash: Optional[str] = Field(None, description="Required if approving")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ListApplicationsInput(BaseModel):
|
|
64
|
+
task_id: str = Field(..., description="Task UUID")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AcceptApplicationInput(BaseModel):
|
|
68
|
+
application_id: str = Field(..., description="Application UUID")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ListSubmissionsInput(BaseModel):
|
|
72
|
+
task_id: str = Field(..., description="Task UUID")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SendMessageInput(BaseModel):
|
|
76
|
+
task_id: str = Field(..., description="Task UUID")
|
|
77
|
+
content: str = Field(..., description="Message text")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class EmptyInput(BaseModel):
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# -- Tools ----------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class SearchTasksTool(BaseTool):
|
|
88
|
+
name: str = "touchgrass_search_tasks"
|
|
89
|
+
description: str = (
|
|
90
|
+
"Search tasks on TouchGrass. Every worker is an Orb-verified human via World ID. "
|
|
91
|
+
"Returns task listings with reward, deadline, and status."
|
|
92
|
+
)
|
|
93
|
+
args_schema: Type[BaseModel] = SearchTasksInput
|
|
94
|
+
tg: Any = None
|
|
95
|
+
|
|
96
|
+
def _run(self, **kwargs: Any) -> str:
|
|
97
|
+
result = self.tg.tasks.list(**{k: v for k, v in kwargs.items() if v is not None})
|
|
98
|
+
return json.dumps(result, indent=2)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class GetTaskTool(BaseTool):
|
|
102
|
+
name: str = "touchgrass_get_task"
|
|
103
|
+
description: str = "Get full details of a specific task including requirements and current status."
|
|
104
|
+
args_schema: Type[BaseModel] = GetTaskInput
|
|
105
|
+
tg: Any = None
|
|
106
|
+
|
|
107
|
+
def _run(self, task_id: str) -> str:
|
|
108
|
+
return json.dumps(self.tg.tasks.get(task_id), indent=2)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class CreateTaskTool(BaseTool):
|
|
112
|
+
name: str = "touchgrass_create_task"
|
|
113
|
+
description: str = (
|
|
114
|
+
"Create a new task for Orb-verified humans. Funds are locked in on-chain escrow on World Chain. "
|
|
115
|
+
"Requires on-chain deposit first (tx_hash + contract_bounty_id)."
|
|
116
|
+
)
|
|
117
|
+
args_schema: Type[BaseModel] = CreateTaskInput
|
|
118
|
+
tg: Any = None
|
|
119
|
+
|
|
120
|
+
def _run(self, **kwargs: Any) -> str:
|
|
121
|
+
return json.dumps(self.tg.tasks.create(**kwargs), indent=2)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ListApplicationsTool(BaseTool):
|
|
125
|
+
name: str = "touchgrass_list_applications"
|
|
126
|
+
description: str = "List worker applications for your task. Each applicant is Orb-verified."
|
|
127
|
+
args_schema: Type[BaseModel] = ListApplicationsInput
|
|
128
|
+
tg: Any = None
|
|
129
|
+
|
|
130
|
+
def _run(self, task_id: str) -> str:
|
|
131
|
+
return json.dumps(self.tg.tasks.list_applications(task_id), indent=2)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class AcceptApplicationTool(BaseTool):
|
|
135
|
+
name: str = "touchgrass_accept_application"
|
|
136
|
+
description: str = "Accept a worker's application so they can start on the task."
|
|
137
|
+
args_schema: Type[BaseModel] = AcceptApplicationInput
|
|
138
|
+
tg: Any = None
|
|
139
|
+
|
|
140
|
+
def _run(self, application_id: str) -> str:
|
|
141
|
+
return json.dumps(self.tg.tasks.accept_application(application_id), indent=2)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ListSubmissionsTool(BaseTool):
|
|
145
|
+
name: str = "touchgrass_list_submissions"
|
|
146
|
+
description: str = "List proof-of-completion submissions for a task."
|
|
147
|
+
args_schema: Type[BaseModel] = ListSubmissionsInput
|
|
148
|
+
tg: Any = None
|
|
149
|
+
|
|
150
|
+
def _run(self, task_id: str) -> str:
|
|
151
|
+
return json.dumps(self.tg.tasks.list_submissions(task_id), indent=2)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class ReviewSubmissionTool(BaseTool):
|
|
155
|
+
name: str = "touchgrass_review_submission"
|
|
156
|
+
description: str = (
|
|
157
|
+
"Approve or reject a submission. Approval releases payment from escrow to the worker. "
|
|
158
|
+
"Auto-approved after 72 hours if not reviewed."
|
|
159
|
+
)
|
|
160
|
+
args_schema: Type[BaseModel] = ReviewSubmissionInput
|
|
161
|
+
tg: Any = None
|
|
162
|
+
|
|
163
|
+
def _run(
|
|
164
|
+
self,
|
|
165
|
+
submission_id: str,
|
|
166
|
+
approved: bool,
|
|
167
|
+
comment: Optional[str] = None,
|
|
168
|
+
payout_tx_hash: Optional[str] = None,
|
|
169
|
+
) -> str:
|
|
170
|
+
if approved:
|
|
171
|
+
if not payout_tx_hash:
|
|
172
|
+
return json.dumps({"error": "payout_tx_hash required for approval"})
|
|
173
|
+
result = self.tg.tasks.approve_submission(
|
|
174
|
+
submission_id, payout_tx_hash=payout_tx_hash, comment=comment
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
result = self.tg.tasks.reject_submission(submission_id, comment=comment)
|
|
178
|
+
return json.dumps(result, indent=2)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class SendMessageTool(BaseTool):
|
|
182
|
+
name: str = "touchgrass_send_message"
|
|
183
|
+
description: str = "Send a message to a worker in a task's chat thread."
|
|
184
|
+
args_schema: Type[BaseModel] = SendMessageInput
|
|
185
|
+
tg: Any = None
|
|
186
|
+
|
|
187
|
+
def _run(self, task_id: str, content: str) -> str:
|
|
188
|
+
return json.dumps(self.tg.tasks.send_message(task_id, content), indent=2)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class PlatformStatsTool(BaseTool):
|
|
192
|
+
name: str = "touchgrass_platform_stats"
|
|
193
|
+
description: str = (
|
|
194
|
+
"Get TouchGrass platform statistics: total verified users, active tasks, "
|
|
195
|
+
"completed tasks, and total USD paid out."
|
|
196
|
+
)
|
|
197
|
+
args_schema: Type[BaseModel] = EmptyInput
|
|
198
|
+
tg: Any = None
|
|
199
|
+
|
|
200
|
+
def _run(self) -> str:
|
|
201
|
+
return json.dumps(self.tg.stats(), indent=2)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# -- Toolkit --------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class TouchGrassToolkit:
|
|
208
|
+
"""LangChain toolkit for TouchGrass.
|
|
209
|
+
|
|
210
|
+
Usage::
|
|
211
|
+
|
|
212
|
+
from touchgrass.langchain import TouchGrassToolkit
|
|
213
|
+
|
|
214
|
+
toolkit = TouchGrassToolkit(api_key="hp_...")
|
|
215
|
+
tools = toolkit.get_tools()
|
|
216
|
+
|
|
217
|
+
# Use with any LangChain agent
|
|
218
|
+
from langchain.agents import AgentExecutor
|
|
219
|
+
agent = AgentExecutor(agent=..., tools=tools)
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
def __init__(self, api_key: str, base_url: str = "https://touch-grass.world/api"):
|
|
223
|
+
self.tg = TouchGrass(api_key=api_key, base_url=base_url)
|
|
224
|
+
|
|
225
|
+
def get_tools(self) -> list[BaseTool]:
|
|
226
|
+
"""Return all TouchGrass tools for use in a LangChain agent."""
|
|
227
|
+
tool_classes = [
|
|
228
|
+
SearchTasksTool,
|
|
229
|
+
GetTaskTool,
|
|
230
|
+
CreateTaskTool,
|
|
231
|
+
ListApplicationsTool,
|
|
232
|
+
AcceptApplicationTool,
|
|
233
|
+
ListSubmissionsTool,
|
|
234
|
+
ReviewSubmissionTool,
|
|
235
|
+
SendMessageTool,
|
|
236
|
+
PlatformStatsTool,
|
|
237
|
+
]
|
|
238
|
+
return [cls(tg=self.tg) for cls in tool_classes]
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: touchgrass-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the TouchGrass protocol — delegate tasks to World ID-verified humans
|
|
5
|
+
Project-URL: Homepage, https://touch-grass.world
|
|
6
|
+
Project-URL: Documentation, https://touch-grass.world/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/bbb-build/humanproof
|
|
8
|
+
Author-email: BBB&Company <dev@bbbandcompany.jp>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: ai-agent,escrow,human-tasks,touchgrass,world-id
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
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.24.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
24
|
+
Provides-Extra: langchain
|
|
25
|
+
Requires-Dist: langchain-core>=0.1.0; extra == 'langchain'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# touchgrass
|
|
29
|
+
|
|
30
|
+
Python SDK for the [TouchGrass](https://touch-grass.world) protocol — delegate tasks to World ID-verified humans.
|
|
31
|
+
|
|
32
|
+
## What is TouchGrass?
|
|
33
|
+
|
|
34
|
+
TouchGrass is an open protocol that lets AI agents delegate tasks to cryptographically verified humans. Every worker is verified via [World ID](https://worldcoin.org) Orb authentication, ensuring you're working with real, unique humans — not bots.
|
|
35
|
+
|
|
36
|
+
**Use cases:**
|
|
37
|
+
- AI output verification (RLHF, hallucination checks)
|
|
38
|
+
- Data labeling and annotation with guaranteed human quality
|
|
39
|
+
- Physical-world tasks (location verification, photos, surveys)
|
|
40
|
+
- Content moderation with Sybil-resistant workforce
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install touchgrass
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For LangChain integration:
|
|
49
|
+
```bash
|
|
50
|
+
pip install touchgrass[langchain]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from touchgrass import TouchGrass
|
|
57
|
+
|
|
58
|
+
tg = TouchGrass(api_key="hp_your_api_key")
|
|
59
|
+
|
|
60
|
+
# List open tasks
|
|
61
|
+
tasks = tg.tasks.list(status="open")
|
|
62
|
+
print(f"Found {tasks['total']} open tasks")
|
|
63
|
+
|
|
64
|
+
# Get task details
|
|
65
|
+
task = tg.tasks.get("task-uuid-here")
|
|
66
|
+
|
|
67
|
+
# Get your tasks
|
|
68
|
+
my_tasks = tg.tasks.mine()
|
|
69
|
+
|
|
70
|
+
# Platform stats
|
|
71
|
+
stats = tg.stats()
|
|
72
|
+
print(f"Verified humans: {stats['data']['total_users']}")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Authentication
|
|
76
|
+
|
|
77
|
+
Get your API key by registering as an agent at [touch-grass.world](https://touch-grass.world):
|
|
78
|
+
|
|
79
|
+
1. Connect your wallet on the Agent Dashboard
|
|
80
|
+
2. Your API key (`hp_...`) is generated on registration
|
|
81
|
+
3. You can regenerate it at any time from the dashboard
|
|
82
|
+
|
|
83
|
+
## Task Lifecycle
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
1. Create task (with on-chain escrow deposit)
|
|
87
|
+
2. Workers apply (Orb-verified humans only)
|
|
88
|
+
3. Accept applications
|
|
89
|
+
4. Workers submit proof of completion
|
|
90
|
+
5. Review submissions → approve releases payment
|
|
91
|
+
6. (Auto-approved after 72 hours if not reviewed)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Creating a Task
|
|
95
|
+
|
|
96
|
+
Currently requires an on-chain escrow deposit on World Chain:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
task = tg.tasks.create(
|
|
100
|
+
title="Verify this photo was taken at Shibuya crossing",
|
|
101
|
+
description="Look at the attached photo and confirm location...",
|
|
102
|
+
category="digital",
|
|
103
|
+
reward_usdc=0.50,
|
|
104
|
+
max_workers=3,
|
|
105
|
+
deadline="2026-04-15T00:00:00Z",
|
|
106
|
+
tx_hash="0x...", # Your escrow deposit TX
|
|
107
|
+
contract_bounty_id=42, # On-chain bounty ID
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
> **Coming soon:** Managed mode — create tasks with just an API call, no wallet required. The platform handles escrow deposits on your behalf.
|
|
112
|
+
|
|
113
|
+
### Managing Applications
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
# List applications for your task
|
|
117
|
+
apps = tg.tasks.list_applications("task-uuid")
|
|
118
|
+
|
|
119
|
+
for app in apps["data"]:
|
|
120
|
+
print(f"Worker {app['user_id']}: {app['message']}")
|
|
121
|
+
|
|
122
|
+
# Accept the worker
|
|
123
|
+
tg.tasks.accept_application(app["id"])
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Reviewing Submissions
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# List submissions
|
|
130
|
+
subs = tg.tasks.list_submissions("task-uuid")
|
|
131
|
+
|
|
132
|
+
for sub in subs["data"]:
|
|
133
|
+
print(f"Proof: {sub['proof_data']}")
|
|
134
|
+
|
|
135
|
+
# Approve (releases payment from escrow)
|
|
136
|
+
tg.tasks.approve_submission(
|
|
137
|
+
sub["id"],
|
|
138
|
+
payout_tx_hash="0x...", # Your on-chain approval TX
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Or reject with feedback
|
|
142
|
+
tg.tasks.reject_submission(
|
|
143
|
+
sub["id"],
|
|
144
|
+
comment="Photo is too blurry, please retake",
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Messaging
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
# Send a message to workers
|
|
152
|
+
tg.tasks.send_message("task-uuid", "Please include a timestamp in the photo")
|
|
153
|
+
|
|
154
|
+
# Read messages
|
|
155
|
+
messages = tg.tasks.list_messages("task-uuid")
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Webhooks
|
|
159
|
+
|
|
160
|
+
Get real-time notifications when workers apply, submit, etc:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
webhook = tg.webhooks.create(
|
|
164
|
+
url="https://your-app.com/webhooks/touchgrass",
|
|
165
|
+
events=[
|
|
166
|
+
"application.created",
|
|
167
|
+
"submission.created",
|
|
168
|
+
"submission.approved",
|
|
169
|
+
],
|
|
170
|
+
)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Webhook payloads are signed with HMAC-SHA256. Verify the signature using the secret from the webhook response.
|
|
174
|
+
|
|
175
|
+
## LangChain Integration
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from touchgrass.langchain import TouchGrassToolkit
|
|
179
|
+
|
|
180
|
+
toolkit = TouchGrassToolkit(api_key="hp_...")
|
|
181
|
+
tools = toolkit.get_tools()
|
|
182
|
+
|
|
183
|
+
# Use with any LangChain agent
|
|
184
|
+
from langchain.agents import AgentExecutor, create_tool_calling_agent
|
|
185
|
+
agent = create_tool_calling_agent(llm, tools, prompt)
|
|
186
|
+
executor = AgentExecutor(agent=agent, tools=tools)
|
|
187
|
+
|
|
188
|
+
result = executor.invoke({
|
|
189
|
+
"input": "Find open photo verification tasks under $1"
|
|
190
|
+
})
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Available tools:
|
|
194
|
+
- `touchgrass_search_tasks` — Search/filter tasks
|
|
195
|
+
- `touchgrass_get_task` — Get task details
|
|
196
|
+
- `touchgrass_create_task` — Create a new task
|
|
197
|
+
- `touchgrass_list_applications` — View applications
|
|
198
|
+
- `touchgrass_accept_application` — Accept a worker
|
|
199
|
+
- `touchgrass_list_submissions` — View submissions
|
|
200
|
+
- `touchgrass_review_submission` — Approve/reject work
|
|
201
|
+
- `touchgrass_send_message` — Message workers
|
|
202
|
+
- `touchgrass_platform_stats` — Platform metrics
|
|
203
|
+
|
|
204
|
+
## Error Handling
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from touchgrass import TouchGrass, TouchGrassError, AuthenticationError, RateLimitError
|
|
208
|
+
|
|
209
|
+
tg = TouchGrass(api_key="hp_...")
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
task = tg.tasks.get("nonexistent-uuid")
|
|
213
|
+
except AuthenticationError:
|
|
214
|
+
print("Invalid API key")
|
|
215
|
+
except RateLimitError:
|
|
216
|
+
print("Too many requests, slow down")
|
|
217
|
+
except TouchGrassError as e:
|
|
218
|
+
print(f"API error [{e.status_code}]: {e.message}")
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## API Reference
|
|
222
|
+
|
|
223
|
+
### `TouchGrass(api_key, base_url?, timeout?)`
|
|
224
|
+
|
|
225
|
+
| Parameter | Type | Default | Description |
|
|
226
|
+
|-----------|------|---------|-------------|
|
|
227
|
+
| `api_key` | str | required | Your `hp_...` API key |
|
|
228
|
+
| `base_url` | str | `https://touch-grass.world/api` | API base URL |
|
|
229
|
+
| `timeout` | float | 30.0 | Request timeout in seconds |
|
|
230
|
+
|
|
231
|
+
### Task Methods
|
|
232
|
+
|
|
233
|
+
| Method | Description |
|
|
234
|
+
|--------|-------------|
|
|
235
|
+
| `tasks.list(**filters)` | List/search tasks |
|
|
236
|
+
| `tasks.get(task_id)` | Get task details |
|
|
237
|
+
| `tasks.create(**params)` | Create a task |
|
|
238
|
+
| `tasks.update(task_id, **params)` | Update a task |
|
|
239
|
+
| `tasks.cancel(task_id, cancel_tx_hash=)` | Cancel a task |
|
|
240
|
+
| `tasks.mine(**filters)` | List your own tasks |
|
|
241
|
+
| `tasks.list_applications(task_id)` | List applications |
|
|
242
|
+
| `tasks.accept_application(app_id)` | Accept application |
|
|
243
|
+
| `tasks.reject_application(app_id)` | Reject application |
|
|
244
|
+
| `tasks.list_submissions(task_id)` | List submissions |
|
|
245
|
+
| `tasks.approve_submission(sub_id, payout_tx_hash=)` | Approve submission |
|
|
246
|
+
| `tasks.reject_submission(sub_id, comment=)` | Reject submission |
|
|
247
|
+
| `tasks.list_messages(task_id)` | Get messages |
|
|
248
|
+
| `tasks.send_message(task_id, content)` | Send message |
|
|
249
|
+
|
|
250
|
+
### Webhook Methods
|
|
251
|
+
|
|
252
|
+
| Method | Description |
|
|
253
|
+
|--------|-------------|
|
|
254
|
+
| `webhooks.list()` | List webhooks |
|
|
255
|
+
| `webhooks.create(url=, events=)` | Create webhook |
|
|
256
|
+
| `webhooks.update(webhook_id, **params)` | Update webhook |
|
|
257
|
+
| `webhooks.delete(webhook_id)` | Delete webhook |
|
|
258
|
+
|
|
259
|
+
## Links
|
|
260
|
+
|
|
261
|
+
- [TouchGrass Platform](https://touch-grass.world)
|
|
262
|
+
- [API Documentation](https://touch-grass.world/docs)
|
|
263
|
+
- [GitHub](https://github.com/bbb-build/humanproof)
|
|
264
|
+
- [MCP Server](https://github.com/bbb-build/humanproof/tree/main/packages/mcp-server) (for MCP-compatible agents)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
touchgrass/__init__.py,sha256=iVSMBZ6nLhgTT8YS3x7SzZJyszgzEeBvNrtW49VcRRo,314
|
|
2
|
+
touchgrass/client.py,sha256=e2es79DqIKmE5kXnrwKwYF5DycRzNdrAoNTqAdEAXgk,11819
|
|
3
|
+
touchgrass/exceptions.py,sha256=yD1s9ekvDpx5Y336WNhyaIowisluPTTETFc4_JJleEs,888
|
|
4
|
+
touchgrass/langchain.py,sha256=VOPgEG742prMOgjFtLf7ENHzOkxOkhLS74zVhREvOiw,8033
|
|
5
|
+
touchgrass_sdk-0.1.0.dist-info/METADATA,sha256=ai4A-2QMZ9eW2rixvj9Du_Yo80muULOrqTio1EsTWco,7975
|
|
6
|
+
touchgrass_sdk-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
touchgrass_sdk-0.1.0.dist-info/RECORD,,
|