nodekit 0.2.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.
- nodekit/.DS_Store +0 -0
- nodekit/__init__.py +53 -0
- nodekit/_internal/__init__.py +0 -0
- nodekit/_internal/ops/__init__.py +0 -0
- nodekit/_internal/ops/build_site/__init__.py +117 -0
- nodekit/_internal/ops/build_site/harness.j2 +158 -0
- nodekit/_internal/ops/concat.py +51 -0
- nodekit/_internal/ops/open_asset_save_asset.py +125 -0
- nodekit/_internal/ops/play/__init__.py +4 -0
- nodekit/_internal/ops/play/local_runner/__init__.py +0 -0
- nodekit/_internal/ops/play/local_runner/main.py +234 -0
- nodekit/_internal/ops/play/local_runner/site-template.j2 +38 -0
- nodekit/_internal/ops/save_graph_load_graph.py +131 -0
- nodekit/_internal/ops/topological_sorting.py +122 -0
- nodekit/_internal/types/__init__.py +0 -0
- nodekit/_internal/types/actions/__init__.py +0 -0
- nodekit/_internal/types/actions/actions.py +89 -0
- nodekit/_internal/types/assets/__init__.py +151 -0
- nodekit/_internal/types/cards/__init__.py +85 -0
- nodekit/_internal/types/events/__init__.py +0 -0
- nodekit/_internal/types/events/events.py +145 -0
- nodekit/_internal/types/expressions/__init__.py +0 -0
- nodekit/_internal/types/expressions/expressions.py +242 -0
- nodekit/_internal/types/graph.py +42 -0
- nodekit/_internal/types/node.py +21 -0
- nodekit/_internal/types/regions/__init__.py +13 -0
- nodekit/_internal/types/sensors/__init__.py +0 -0
- nodekit/_internal/types/sensors/sensors.py +156 -0
- nodekit/_internal/types/trace.py +17 -0
- nodekit/_internal/types/transition.py +68 -0
- nodekit/_internal/types/value.py +145 -0
- nodekit/_internal/utils/__init__.py +0 -0
- nodekit/_internal/utils/get_browser_bundle.py +35 -0
- nodekit/_internal/utils/get_extension_from_media_type.py +15 -0
- nodekit/_internal/utils/hashing.py +46 -0
- nodekit/_internal/utils/iter_assets.py +61 -0
- nodekit/_internal/version.py +1 -0
- nodekit/_static/nodekit.css +10 -0
- nodekit/_static/nodekit.js +59 -0
- nodekit/actions/__init__.py +25 -0
- nodekit/assets/__init__.py +7 -0
- nodekit/cards/__init__.py +15 -0
- nodekit/events/__init__.py +30 -0
- nodekit/experimental/.DS_Store +0 -0
- nodekit/experimental/__init__.py +0 -0
- nodekit/experimental/recruitment_services/__init__.py +0 -0
- nodekit/experimental/recruitment_services/base.py +77 -0
- nodekit/experimental/recruitment_services/mechanical_turk/__init__.py +0 -0
- nodekit/experimental/recruitment_services/mechanical_turk/client.py +359 -0
- nodekit/experimental/recruitment_services/mechanical_turk/models.py +116 -0
- nodekit/experimental/s3.py +219 -0
- nodekit/experimental/turk_helper.py +223 -0
- nodekit/experimental/visualization/.DS_Store +0 -0
- nodekit/experimental/visualization/__init__.py +0 -0
- nodekit/experimental/visualization/pointer.py +443 -0
- nodekit/expressions/__init__.py +55 -0
- nodekit/sensors/__init__.py +25 -0
- nodekit/transitions/__init__.py +15 -0
- nodekit/values/__init__.py +63 -0
- nodekit-0.2.0.dist-info/METADATA +221 -0
- nodekit-0.2.0.dist-info/RECORD +62 -0
- nodekit-0.2.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"EventTypeEnum",
|
|
3
|
+
"Event",
|
|
4
|
+
# Concrete classes:
|
|
5
|
+
"TraceStartedEvent",
|
|
6
|
+
"TraceEndedEvent",
|
|
7
|
+
"PageSuspendedEvent",
|
|
8
|
+
"PageResumedEvent",
|
|
9
|
+
"NodeStartedEvent",
|
|
10
|
+
"ActionTakenEvent",
|
|
11
|
+
"NodeEndedEvent",
|
|
12
|
+
"BrowserContextSampledEvent",
|
|
13
|
+
"KeySampledEvent",
|
|
14
|
+
"PointerSampledEvent",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
from nodekit._internal.types.events.events import (
|
|
18
|
+
Event,
|
|
19
|
+
EventTypeEnum,
|
|
20
|
+
TraceStartedEvent,
|
|
21
|
+
TraceEndedEvent,
|
|
22
|
+
NodeStartedEvent,
|
|
23
|
+
ActionTakenEvent,
|
|
24
|
+
NodeEndedEvent,
|
|
25
|
+
KeySampledEvent,
|
|
26
|
+
PointerSampledEvent,
|
|
27
|
+
PageSuspendedEvent,
|
|
28
|
+
PageResumedEvent,
|
|
29
|
+
BrowserContextSampledEvent,
|
|
30
|
+
)
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from typing import List, Iterable, Literal
|
|
4
|
+
|
|
5
|
+
import pydantic
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# %%
|
|
9
|
+
class RecruiterCredentialsError(Exception):
|
|
10
|
+
"""Exception raised for errors in the recruiter credentials."""
|
|
11
|
+
|
|
12
|
+
...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# %%
|
|
16
|
+
class ListAssignmentsItem(pydantic.BaseModel):
|
|
17
|
+
hit_id: str
|
|
18
|
+
worker_id: str
|
|
19
|
+
assignment_id: str
|
|
20
|
+
status: Literal["Submitted", "Approved", "Rejected"]
|
|
21
|
+
submission_payload: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CreateHitRequest(pydantic.BaseModel):
|
|
25
|
+
entrypoint_url: str
|
|
26
|
+
title: str
|
|
27
|
+
description: str
|
|
28
|
+
keywords: List[str]
|
|
29
|
+
num_assignments: int
|
|
30
|
+
duration_sec: int
|
|
31
|
+
completion_reward_usd: Decimal
|
|
32
|
+
allowed_participant_ids: List[str]
|
|
33
|
+
unique_request_token: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CreateHitResponse(pydantic.BaseModel):
|
|
37
|
+
hit_id: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SendBonusPaymentRequest(pydantic.BaseModel):
|
|
41
|
+
worker_id: str
|
|
42
|
+
assignment_id: str
|
|
43
|
+
amount_usd: Decimal = pydantic.Field(decimal_places=2)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# %%
|
|
47
|
+
class RecruiterServiceClient(ABC):
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def get_recruiter_service_name(self) -> str: ...
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def create_hit(
|
|
53
|
+
self,
|
|
54
|
+
request: CreateHitRequest,
|
|
55
|
+
) -> CreateHitResponse: ...
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def send_bonus_payment(
|
|
59
|
+
self,
|
|
60
|
+
request: SendBonusPaymentRequest,
|
|
61
|
+
) -> None: ...
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def iter_assignments(
|
|
65
|
+
self,
|
|
66
|
+
hit_id: str,
|
|
67
|
+
) -> Iterable[ListAssignmentsItem]:
|
|
68
|
+
raise NotImplementedError
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def cleanup_hit(self, hit_id: str) -> None: ...
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def approve_assignment(
|
|
75
|
+
self,
|
|
76
|
+
assignment_id: str,
|
|
77
|
+
) -> None: ...
|
|
File without changes
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from typing import List, Iterable
|
|
3
|
+
|
|
4
|
+
from boto3.session import Session
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from nodekit.experimental.recruitment_services.base import (
|
|
8
|
+
RecruiterServiceClient,
|
|
9
|
+
CreateHitRequest,
|
|
10
|
+
CreateHitResponse,
|
|
11
|
+
SendBonusPaymentRequest,
|
|
12
|
+
ListAssignmentsItem,
|
|
13
|
+
RecruiterCredentialsError,
|
|
14
|
+
)
|
|
15
|
+
import nodekit.experimental.recruitment_services.mechanical_turk.models as boto3_models
|
|
16
|
+
from uuid import uuid4
|
|
17
|
+
|
|
18
|
+
import datetime
|
|
19
|
+
import re
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def extract_trace(xml: str) -> str:
|
|
23
|
+
# pull the contents of the <FreeText> element
|
|
24
|
+
m = re.search(r"<FreeText>(.*?)</FreeText>", xml, re.DOTALL)
|
|
25
|
+
if not m:
|
|
26
|
+
raise ValueError("No <FreeText> found")
|
|
27
|
+
raw = m.group(1)
|
|
28
|
+
|
|
29
|
+
# MTurk sometimes form-encodes it (+ for space, %xx etc.)
|
|
30
|
+
return raw
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# %%
|
|
34
|
+
class MturkClient(RecruiterServiceClient):
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
aws_access_key_id: str,
|
|
38
|
+
aws_secret_access_key: str,
|
|
39
|
+
sandbox: bool,
|
|
40
|
+
):
|
|
41
|
+
# Initialize MTurk client
|
|
42
|
+
if sandbox:
|
|
43
|
+
endpoint_url = "https://mturk-requester-sandbox.us-east-1.amazonaws.com"
|
|
44
|
+
else:
|
|
45
|
+
endpoint_url = "https://mturk-requester.us-east-1.amazonaws.com"
|
|
46
|
+
|
|
47
|
+
session = Session(
|
|
48
|
+
aws_access_key_id=aws_access_key_id,
|
|
49
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
self.boto3_client = session.client(
|
|
53
|
+
service_name="mturk", endpoint_url=endpoint_url, region_name="us-east-1"
|
|
54
|
+
)
|
|
55
|
+
self.sandbox = sandbox
|
|
56
|
+
|
|
57
|
+
# Try verifying the credentials; throw if invalid:
|
|
58
|
+
try:
|
|
59
|
+
self.boto3_client.get_account_balance()
|
|
60
|
+
except Exception as e:
|
|
61
|
+
raise RecruiterCredentialsError from e
|
|
62
|
+
|
|
63
|
+
def get_recruiter_service_name(self) -> str:
|
|
64
|
+
if self.sandbox:
|
|
65
|
+
return "MTurkSandbox"
|
|
66
|
+
else:
|
|
67
|
+
return "MTurk"
|
|
68
|
+
|
|
69
|
+
def create_hit(
|
|
70
|
+
self,
|
|
71
|
+
request: CreateHitRequest,
|
|
72
|
+
) -> CreateHitResponse:
|
|
73
|
+
# Unpack:
|
|
74
|
+
entrypoint_url = request.entrypoint_url
|
|
75
|
+
title = request.title
|
|
76
|
+
description = request.description
|
|
77
|
+
keywords = request.keywords
|
|
78
|
+
num_assignments = request.num_assignments
|
|
79
|
+
duration_sec = request.duration_sec
|
|
80
|
+
completion_reward_usd = request.completion_reward_usd
|
|
81
|
+
allowed_participant_ids = request.allowed_participant_ids
|
|
82
|
+
|
|
83
|
+
# Calculate minimum approval cost
|
|
84
|
+
min_approval_cost = (
|
|
85
|
+
completion_reward_usd * Decimal("1.2") * Decimal(num_assignments)
|
|
86
|
+
) # Turk fees are 20% of the base completion reward.
|
|
87
|
+
current_balance = Decimal(
|
|
88
|
+
self.boto3_client.get_account_balance()["AvailableBalance"]
|
|
89
|
+
)
|
|
90
|
+
if current_balance < min_approval_cost:
|
|
91
|
+
raise RuntimeError(
|
|
92
|
+
f"Insufficient balance to create HIT. Minimum required: ${min_approval_cost:.2f}, current balance: ${current_balance:.2f}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
qualification_requirements: List[boto3_models.QualificationRequirement] = []
|
|
96
|
+
if len(allowed_participant_ids) > 0:
|
|
97
|
+
# Create qualification type for this TaskRequest:
|
|
98
|
+
qual_type = self.create_qualification_type(unique_name=f"psy:{uuid4()}")
|
|
99
|
+
|
|
100
|
+
# Grant each worker ID the qualification:
|
|
101
|
+
for worker_id in allowed_participant_ids:
|
|
102
|
+
self.grant_qualification(
|
|
103
|
+
qualification_type_id=qual_type.QualificationTypeId,
|
|
104
|
+
worker_id=worker_id,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Create qualification requirement for this HIT:
|
|
108
|
+
qual_requirement = self.package_qualification_exists_requirement(
|
|
109
|
+
qual_type=qual_type
|
|
110
|
+
)
|
|
111
|
+
qualification_requirements.append(qual_requirement)
|
|
112
|
+
|
|
113
|
+
hit_type_response = self.boto3_client.create_hit_type(
|
|
114
|
+
AutoApprovalDelayInSeconds=1,
|
|
115
|
+
AssignmentDurationInSeconds=duration_sec,
|
|
116
|
+
Reward=str(completion_reward_usd),
|
|
117
|
+
Title=title,
|
|
118
|
+
Keywords=",".join(keywords),
|
|
119
|
+
Description=description,
|
|
120
|
+
QualificationRequirements=[
|
|
121
|
+
qr.model_dump(mode="json") for qr in qualification_requirements
|
|
122
|
+
],
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
q = (
|
|
126
|
+
f'<ExternalQuestion xmlns="http://mechanicalturk.amazonaws.com/AWSMechanicalTurkDataSchemas/2006-07-14/ExternalQuestion.xsd">'
|
|
127
|
+
f"<ExternalURL>{entrypoint_url}</ExternalURL>"
|
|
128
|
+
f"<FrameHeight>{0}</FrameHeight></ExternalQuestion>"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
hit_info = self.boto3_client.create_hit_with_hit_type(
|
|
132
|
+
HITTypeId=hit_type_response["HITTypeId"],
|
|
133
|
+
Question=q,
|
|
134
|
+
MaxAssignments=num_assignments,
|
|
135
|
+
LifetimeInSeconds=1209600, # 1209600 seconds = 2 weeks is max allowed by MTurk
|
|
136
|
+
UniqueRequestToken=request.unique_request_token,
|
|
137
|
+
RequesterAnnotation=request.unique_request_token,
|
|
138
|
+
)["HIT"]
|
|
139
|
+
hit = boto3_models.HIT.model_validate(obj=hit_info)
|
|
140
|
+
|
|
141
|
+
# Idempotency error: botocore.errorfactory.RequestError: An error occurred (RequestError) when calling the CreateHIT operation: The HIT with ID "{hit_id}" already exists.
|
|
142
|
+
return CreateHitResponse(hit_id=hit.HITId)
|
|
143
|
+
|
|
144
|
+
def send_bonus_payment(
|
|
145
|
+
self,
|
|
146
|
+
request: SendBonusPaymentRequest,
|
|
147
|
+
) -> None:
|
|
148
|
+
unique_request_token = f"{request.worker_id}:{request.assignment_id}"
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
self.boto3_client.send_bonus(
|
|
152
|
+
WorkerId=request.worker_id,
|
|
153
|
+
BonusAmount=str(request.amount_usd),
|
|
154
|
+
AssignmentId=request.assignment_id,
|
|
155
|
+
Reason="Assignment-based bonus.",
|
|
156
|
+
UniqueRequestToken=unique_request_token, # For idempotency
|
|
157
|
+
)
|
|
158
|
+
except Exception as e:
|
|
159
|
+
if "idempotency" in str(e):
|
|
160
|
+
# Expected error for duplicate request
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
def approve_assignment(self, assignment_id: str) -> None:
|
|
164
|
+
self.boto3_client.approve_assignment(
|
|
165
|
+
AssignmentId=assignment_id,
|
|
166
|
+
OverrideRejection=False,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def iter_assignments(
|
|
170
|
+
self,
|
|
171
|
+
hit_id: str,
|
|
172
|
+
) -> Iterable[ListAssignmentsItem]:
|
|
173
|
+
AssignmentStatuses = ["Submitted", "Approved", "Rejected"]
|
|
174
|
+
|
|
175
|
+
request_kwargs = dict(
|
|
176
|
+
HITId=hit_id,
|
|
177
|
+
MaxResults=100,
|
|
178
|
+
AssignmentStatuses=AssignmentStatuses,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Paginate over boto3 results:
|
|
182
|
+
next_token = ""
|
|
183
|
+
while next_token is not None:
|
|
184
|
+
if next_token != "":
|
|
185
|
+
request_kwargs["NextToken"] = next_token
|
|
186
|
+
else:
|
|
187
|
+
if "NextToken" in request_kwargs:
|
|
188
|
+
del request_kwargs["NextToken"]
|
|
189
|
+
|
|
190
|
+
call_return = self.boto3_client.list_assignments_for_hit(**request_kwargs)
|
|
191
|
+
for asn_info in call_return["Assignments"]:
|
|
192
|
+
assignment = boto3_models.Assignment.model_validate(obj=asn_info)
|
|
193
|
+
item = ListAssignmentsItem(
|
|
194
|
+
hit_id=assignment.HITId,
|
|
195
|
+
worker_id=assignment.WorkerId,
|
|
196
|
+
assignment_id=assignment.AssignmentId,
|
|
197
|
+
status=assignment.AssignmentStatus,
|
|
198
|
+
submission_payload=extract_trace(assignment.Answer),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
yield item
|
|
202
|
+
|
|
203
|
+
if "NextToken" in call_return:
|
|
204
|
+
next_token = call_return["NextToken"]
|
|
205
|
+
else:
|
|
206
|
+
# Will break
|
|
207
|
+
next_token = None
|
|
208
|
+
|
|
209
|
+
def cleanup_hit(self, hit_id: str) -> None:
|
|
210
|
+
# First, retrieve the HIT to ensure it exists
|
|
211
|
+
hit_info = self.boto3_client.get_hit(HITId=hit_id)
|
|
212
|
+
hit = boto3_models.HIT.model_validate(obj=hit_info["HIT"])
|
|
213
|
+
|
|
214
|
+
# See if this HIT has any QualificationRequirements
|
|
215
|
+
qual_reqs = hit.QualificationRequirements
|
|
216
|
+
for qual_req in qual_reqs:
|
|
217
|
+
# Get workers associated with this qualification:
|
|
218
|
+
worker_ids = self.list_workers_with_qualification_type(
|
|
219
|
+
qual_type_id=qual_req.QualificationTypeId
|
|
220
|
+
)
|
|
221
|
+
# Dissociate any qualifications from workers that were previously granted
|
|
222
|
+
for worker_id in worker_ids:
|
|
223
|
+
self.boto3_client.disassociate_qualification_from_worker(
|
|
224
|
+
WorkerId=worker_id,
|
|
225
|
+
QualificationTypeId=qual_req.QualificationTypeId,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Delete the qualification type
|
|
229
|
+
self.delete_qualification_type(
|
|
230
|
+
qualification_type_id=qual_req.QualificationTypeId
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Update the expiration for the HIT to *now*
|
|
234
|
+
self.boto3_client.update_expiration_for_hit(
|
|
235
|
+
HITId=hit_id, ExpireAt=datetime.datetime.now(tz=datetime.timezone.utc)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# %% Quals:
|
|
239
|
+
def list_workers_with_qualification_type(
|
|
240
|
+
self,
|
|
241
|
+
qual_type_id: str,
|
|
242
|
+
) -> List[str]:
|
|
243
|
+
next_token = ""
|
|
244
|
+
request_kwargs = dict(
|
|
245
|
+
QualificationTypeId=qual_type_id,
|
|
246
|
+
Status="Granted",
|
|
247
|
+
MaxResults=100,
|
|
248
|
+
)
|
|
249
|
+
worker_ids = []
|
|
250
|
+
while next_token is not None:
|
|
251
|
+
if next_token != "":
|
|
252
|
+
request_kwargs["NextToken"] = next_token
|
|
253
|
+
else:
|
|
254
|
+
if "NextToken" in request_kwargs:
|
|
255
|
+
del request_kwargs["NextToken"]
|
|
256
|
+
|
|
257
|
+
call_return = self.boto3_client.list_workers_with_qualification_type(
|
|
258
|
+
**request_kwargs
|
|
259
|
+
)
|
|
260
|
+
for worker_info in call_return["Qualifications"]:
|
|
261
|
+
worker_ids.append(worker_info["WorkerId"])
|
|
262
|
+
|
|
263
|
+
if "NextToken" in call_return:
|
|
264
|
+
next_token = call_return["NextToken"]
|
|
265
|
+
else:
|
|
266
|
+
# Will break
|
|
267
|
+
next_token = None
|
|
268
|
+
return worker_ids
|
|
269
|
+
|
|
270
|
+
def create_qualification_type(
|
|
271
|
+
self,
|
|
272
|
+
unique_name: str,
|
|
273
|
+
) -> boto3_models.QualificationType:
|
|
274
|
+
response = self.boto3_client.create_qualification_type(
|
|
275
|
+
Name=unique_name,
|
|
276
|
+
Description=unique_name,
|
|
277
|
+
QualificationTypeStatus="Active",
|
|
278
|
+
)
|
|
279
|
+
# Validate response:
|
|
280
|
+
return boto3_models.QualificationType.model_validate(
|
|
281
|
+
obj=response["QualificationType"]
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def package_qualification_exists_requirement(
|
|
285
|
+
self,
|
|
286
|
+
qual_type: boto3_models.QualificationType,
|
|
287
|
+
) -> boto3_models.QualificationRequirement:
|
|
288
|
+
return boto3_models.QualificationRequirement(
|
|
289
|
+
QualificationTypeId=qual_type.QualificationTypeId,
|
|
290
|
+
Comparator="Exists",
|
|
291
|
+
ActionsGuarded="DiscoverPreviewAndAccept",
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def list_qualification_types(self) -> List[boto3_models.QualificationType]:
|
|
295
|
+
qualification_types = []
|
|
296
|
+
|
|
297
|
+
next_token = ""
|
|
298
|
+
|
|
299
|
+
call_kwargs = {}
|
|
300
|
+
while next_token is not None:
|
|
301
|
+
res = self.boto3_client.list_qualification_types(
|
|
302
|
+
MustBeRequestable=False,
|
|
303
|
+
MustBeOwnedByCaller=True,
|
|
304
|
+
MaxResults=100,
|
|
305
|
+
**call_kwargs,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if "NextToken" in res:
|
|
309
|
+
next_token = res["NextToken"]
|
|
310
|
+
else:
|
|
311
|
+
next_token = None
|
|
312
|
+
|
|
313
|
+
call_kwargs["NextToken"] = next_token
|
|
314
|
+
|
|
315
|
+
qreturn = res["QualificationTypes"]
|
|
316
|
+
for q in qreturn:
|
|
317
|
+
qual_type = boto3_models.QualificationType.model_validate(obj=q)
|
|
318
|
+
qualification_types.append(qual_type)
|
|
319
|
+
|
|
320
|
+
return qualification_types
|
|
321
|
+
|
|
322
|
+
def grant_qualification(
|
|
323
|
+
self,
|
|
324
|
+
qualification_type_id: str,
|
|
325
|
+
worker_id: str,
|
|
326
|
+
integer_value: int = 1,
|
|
327
|
+
):
|
|
328
|
+
self.boto3_client.associate_qualification_with_worker(
|
|
329
|
+
QualificationTypeId=qualification_type_id,
|
|
330
|
+
WorkerId=worker_id,
|
|
331
|
+
IntegerValue=integer_value,
|
|
332
|
+
SendNotification=False,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def revoke_qualification(
|
|
336
|
+
self,
|
|
337
|
+
worker_id: str,
|
|
338
|
+
qualification_type_id: str,
|
|
339
|
+
):
|
|
340
|
+
try:
|
|
341
|
+
self.boto3_client.disassociate_qualification_from_worker(
|
|
342
|
+
QualificationTypeId=qualification_type_id,
|
|
343
|
+
WorkerId=worker_id,
|
|
344
|
+
Reason="",
|
|
345
|
+
)
|
|
346
|
+
except Exception as e:
|
|
347
|
+
message = str(e)
|
|
348
|
+
if "RequestError" in message:
|
|
349
|
+
print(message) # todo; already revoked?
|
|
350
|
+
else:
|
|
351
|
+
raise e
|
|
352
|
+
|
|
353
|
+
def delete_qualification_type(
|
|
354
|
+
self,
|
|
355
|
+
qualification_type_id: str,
|
|
356
|
+
):
|
|
357
|
+
self.boto3_client.delete_qualification_type(
|
|
358
|
+
QualificationTypeId=qualification_type_id
|
|
359
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from typing import List, Optional, Literal, Dict, Any
|
|
4
|
+
|
|
5
|
+
import pydantic
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# %%
|
|
9
|
+
class HIT(pydantic.BaseModel):
|
|
10
|
+
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/mturk/client/create_hit.html
|
|
11
|
+
HITId: str
|
|
12
|
+
HITTypeId: Optional[str]
|
|
13
|
+
HITGroupId: Optional[str] = None
|
|
14
|
+
CreationTime: datetime.datetime
|
|
15
|
+
Title: str
|
|
16
|
+
Description: str
|
|
17
|
+
Question: str
|
|
18
|
+
Keywords: str
|
|
19
|
+
HITStatus: Literal[
|
|
20
|
+
"Assignable", "Unassignable", "Reviewable", "Reviewing", "Disposed"
|
|
21
|
+
]
|
|
22
|
+
MaxAssignments: int
|
|
23
|
+
Reward: Decimal = pydantic.Field(decimal_places=2)
|
|
24
|
+
AutoApprovalDelayInSeconds: int
|
|
25
|
+
Expiration: datetime.datetime
|
|
26
|
+
AssignmentDurationInSeconds: int
|
|
27
|
+
RequesterAnnotation: Optional[str] = None
|
|
28
|
+
QualificationRequirements: Optional[List["QualificationRequirement"]] = None
|
|
29
|
+
HITReviewStatus: Literal[
|
|
30
|
+
"NotReviewed", "MarkedForReview", "ReviewedAppropriate", "ReviewedInappropriate"
|
|
31
|
+
]
|
|
32
|
+
NumberOfAssignmentsPending: int
|
|
33
|
+
NumberOfAssignmentsAvailable: int
|
|
34
|
+
NumberOfAssignmentsCompleted: int
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Assignment(pydantic.BaseModel):
|
|
38
|
+
# https://boto3.amazonaws.com/v1/documentation/api/1.26.93/reference/services/mturk/client/list_assignments_for_hit.html
|
|
39
|
+
AssignmentId: str
|
|
40
|
+
WorkerId: str
|
|
41
|
+
HITId: str
|
|
42
|
+
AssignmentStatus: Literal["Submitted", "Approved", "Rejected"]
|
|
43
|
+
AutoApprovalTime: Optional[datetime.datetime] = None
|
|
44
|
+
AcceptTime: Optional[datetime.datetime] = None
|
|
45
|
+
SubmitTime: Optional[datetime.datetime] = None
|
|
46
|
+
ApprovalTime: Optional[datetime.datetime] = None
|
|
47
|
+
RejectionTime: Optional[datetime.datetime] = None
|
|
48
|
+
Deadline: Optional[datetime.datetime] = None
|
|
49
|
+
Answer: str
|
|
50
|
+
RequesterFeedback: Optional[str] = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BonusPayment(pydantic.BaseModel):
|
|
54
|
+
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/mturk/client/list_bonus_payments.html
|
|
55
|
+
WorkerId: str
|
|
56
|
+
AssignmentId: str
|
|
57
|
+
BonusAmount: Decimal = pydantic.Field(decimal_places=2)
|
|
58
|
+
Reason: Optional[str] = None
|
|
59
|
+
GrantTime: Optional[datetime.datetime] = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# %% Qualifications
|
|
63
|
+
class QualificationType(pydantic.BaseModel):
|
|
64
|
+
# https://docs.aws.amazon.com/AWSMechTurk/latest/AWSMturkAPI/ApiReference_QualificationTypeDataStructureArticle.html
|
|
65
|
+
QualificationTypeId: str
|
|
66
|
+
CreationTime: Optional[datetime.datetime] = None
|
|
67
|
+
Name: Optional[str] = None
|
|
68
|
+
Description: Optional[str] = None
|
|
69
|
+
Keywords: Optional[str] = None
|
|
70
|
+
QualificationTypeStatus: Literal["Active", "Inactive"]
|
|
71
|
+
RetryDelayInSeconds: Optional[int] = None
|
|
72
|
+
Test: Optional[str] = None
|
|
73
|
+
TestDurationInSeconds: Optional[int] = None
|
|
74
|
+
AnswerKey: Optional[str] = None
|
|
75
|
+
AutoGranted: Optional[bool] = None
|
|
76
|
+
AutoGrantedValue: Optional[int] = 1
|
|
77
|
+
IsRequestable: Optional[bool] = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class QualificationRequirement(pydantic.BaseModel):
|
|
81
|
+
QualificationTypeId: str
|
|
82
|
+
|
|
83
|
+
Comparator: Literal[
|
|
84
|
+
"LessThan",
|
|
85
|
+
"LessThanOrEqualTo",
|
|
86
|
+
"GreaterThan",
|
|
87
|
+
"GreaterThanOrEqualTo",
|
|
88
|
+
"EqualTo",
|
|
89
|
+
"NotEqualTo",
|
|
90
|
+
"Exists",
|
|
91
|
+
"DoesNotExist",
|
|
92
|
+
"In",
|
|
93
|
+
"NotIn",
|
|
94
|
+
]
|
|
95
|
+
ActionsGuarded: Literal["Accept", "PreviewAndAccept", "DiscoverPreviewAndAccept"]
|
|
96
|
+
IntegerValues: List[int] | None = pydantic.Field(default=None)
|
|
97
|
+
LocaleValues: List["LocaleValue"] | None = pydantic.Field(default=None)
|
|
98
|
+
|
|
99
|
+
@pydantic.model_serializer()
|
|
100
|
+
def ensure_exclude_none(self):
|
|
101
|
+
# boto3 expects IntegerValues and LocaleValues to not be present as fields if they are None:
|
|
102
|
+
base: Dict[str, Any] = dict(
|
|
103
|
+
QualificationTypeId=self.QualificationTypeId,
|
|
104
|
+
Comparator=self.Comparator,
|
|
105
|
+
ActionsGuarded=self.ActionsGuarded,
|
|
106
|
+
)
|
|
107
|
+
if self.IntegerValues is not None:
|
|
108
|
+
base["IntegerValues"] = self.IntegerValues
|
|
109
|
+
if self.LocaleValues is not None:
|
|
110
|
+
base["LocaleValues"] = self.LocaleValues
|
|
111
|
+
return base
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class LocaleValue(pydantic.BaseModel):
|
|
115
|
+
Country: str
|
|
116
|
+
Subdivision: Optional[str]
|