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.
Files changed (62) hide show
  1. nodekit/.DS_Store +0 -0
  2. nodekit/__init__.py +53 -0
  3. nodekit/_internal/__init__.py +0 -0
  4. nodekit/_internal/ops/__init__.py +0 -0
  5. nodekit/_internal/ops/build_site/__init__.py +117 -0
  6. nodekit/_internal/ops/build_site/harness.j2 +158 -0
  7. nodekit/_internal/ops/concat.py +51 -0
  8. nodekit/_internal/ops/open_asset_save_asset.py +125 -0
  9. nodekit/_internal/ops/play/__init__.py +4 -0
  10. nodekit/_internal/ops/play/local_runner/__init__.py +0 -0
  11. nodekit/_internal/ops/play/local_runner/main.py +234 -0
  12. nodekit/_internal/ops/play/local_runner/site-template.j2 +38 -0
  13. nodekit/_internal/ops/save_graph_load_graph.py +131 -0
  14. nodekit/_internal/ops/topological_sorting.py +122 -0
  15. nodekit/_internal/types/__init__.py +0 -0
  16. nodekit/_internal/types/actions/__init__.py +0 -0
  17. nodekit/_internal/types/actions/actions.py +89 -0
  18. nodekit/_internal/types/assets/__init__.py +151 -0
  19. nodekit/_internal/types/cards/__init__.py +85 -0
  20. nodekit/_internal/types/events/__init__.py +0 -0
  21. nodekit/_internal/types/events/events.py +145 -0
  22. nodekit/_internal/types/expressions/__init__.py +0 -0
  23. nodekit/_internal/types/expressions/expressions.py +242 -0
  24. nodekit/_internal/types/graph.py +42 -0
  25. nodekit/_internal/types/node.py +21 -0
  26. nodekit/_internal/types/regions/__init__.py +13 -0
  27. nodekit/_internal/types/sensors/__init__.py +0 -0
  28. nodekit/_internal/types/sensors/sensors.py +156 -0
  29. nodekit/_internal/types/trace.py +17 -0
  30. nodekit/_internal/types/transition.py +68 -0
  31. nodekit/_internal/types/value.py +145 -0
  32. nodekit/_internal/utils/__init__.py +0 -0
  33. nodekit/_internal/utils/get_browser_bundle.py +35 -0
  34. nodekit/_internal/utils/get_extension_from_media_type.py +15 -0
  35. nodekit/_internal/utils/hashing.py +46 -0
  36. nodekit/_internal/utils/iter_assets.py +61 -0
  37. nodekit/_internal/version.py +1 -0
  38. nodekit/_static/nodekit.css +10 -0
  39. nodekit/_static/nodekit.js +59 -0
  40. nodekit/actions/__init__.py +25 -0
  41. nodekit/assets/__init__.py +7 -0
  42. nodekit/cards/__init__.py +15 -0
  43. nodekit/events/__init__.py +30 -0
  44. nodekit/experimental/.DS_Store +0 -0
  45. nodekit/experimental/__init__.py +0 -0
  46. nodekit/experimental/recruitment_services/__init__.py +0 -0
  47. nodekit/experimental/recruitment_services/base.py +77 -0
  48. nodekit/experimental/recruitment_services/mechanical_turk/__init__.py +0 -0
  49. nodekit/experimental/recruitment_services/mechanical_turk/client.py +359 -0
  50. nodekit/experimental/recruitment_services/mechanical_turk/models.py +116 -0
  51. nodekit/experimental/s3.py +219 -0
  52. nodekit/experimental/turk_helper.py +223 -0
  53. nodekit/experimental/visualization/.DS_Store +0 -0
  54. nodekit/experimental/visualization/__init__.py +0 -0
  55. nodekit/experimental/visualization/pointer.py +443 -0
  56. nodekit/expressions/__init__.py +55 -0
  57. nodekit/sensors/__init__.py +25 -0
  58. nodekit/transitions/__init__.py +15 -0
  59. nodekit/values/__init__.py +63 -0
  60. nodekit-0.2.0.dist-info/METADATA +221 -0
  61. nodekit-0.2.0.dist-info/RECORD +62 -0
  62. 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: ...
@@ -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]