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,219 @@
|
|
|
1
|
+
import pydantic
|
|
2
|
+
|
|
3
|
+
import boto3
|
|
4
|
+
import botocore.client
|
|
5
|
+
import botocore.exceptions
|
|
6
|
+
from typing import BinaryIO
|
|
7
|
+
import hashlib
|
|
8
|
+
import mimetypes
|
|
9
|
+
|
|
10
|
+
from nodekit._internal.types.value import SHA256, MediaType
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from urllib.parse import quote
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# %%
|
|
18
|
+
class UploadAssetResult(pydantic.BaseModel):
|
|
19
|
+
sha256: SHA256
|
|
20
|
+
mime_type: MediaType
|
|
21
|
+
asset_url: pydantic.HttpUrl
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class S3Client:
|
|
25
|
+
class Config(pydantic.BaseModel):
|
|
26
|
+
bucket_name: str
|
|
27
|
+
region_name: str
|
|
28
|
+
aws_access_key_id: str
|
|
29
|
+
aws_secret_access_key: pydantic.SecretStr
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
bucket_name: str,
|
|
34
|
+
region_name: str,
|
|
35
|
+
aws_access_key_id: str,
|
|
36
|
+
aws_secret_access_key: str,
|
|
37
|
+
):
|
|
38
|
+
self.config = self.Config(
|
|
39
|
+
bucket_name=bucket_name,
|
|
40
|
+
region_name=region_name,
|
|
41
|
+
aws_access_key_id=aws_access_key_id,
|
|
42
|
+
aws_secret_access_key=pydantic.SecretStr(aws_secret_access_key),
|
|
43
|
+
)
|
|
44
|
+
self._client: botocore.client.BaseClient = boto3.client(
|
|
45
|
+
"s3",
|
|
46
|
+
region_name=self.config.region_name,
|
|
47
|
+
aws_access_key_id=self.config.aws_access_key_id,
|
|
48
|
+
aws_secret_access_key=self.config.aws_secret_access_key.get_secret_value(),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _derive_s3_key(
|
|
53
|
+
sha256: SHA256,
|
|
54
|
+
mime_type: MediaType,
|
|
55
|
+
) -> str:
|
|
56
|
+
ext = mimetypes.guess_extension(mime_type)
|
|
57
|
+
if ext is None:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"Could not determine file extension for mime type {mime_type}"
|
|
60
|
+
)
|
|
61
|
+
return f"assets/{mime_type}/{sha256}{ext}"
|
|
62
|
+
|
|
63
|
+
def _assemble_s3_url(self, key: str) -> pydantic.HttpUrl:
|
|
64
|
+
url = f"https://{self.config.bucket_name}.s3.{self.config.region_name}.amazonaws.com/{key}"
|
|
65
|
+
return pydantic.HttpUrl(url)
|
|
66
|
+
|
|
67
|
+
def maybe_resolve_asset(
|
|
68
|
+
self,
|
|
69
|
+
sha256: SHA256,
|
|
70
|
+
mime_type: MediaType,
|
|
71
|
+
) -> UploadAssetResult | None:
|
|
72
|
+
# Derive S3 key
|
|
73
|
+
key = self._derive_s3_key(sha256=sha256, mime_type=mime_type)
|
|
74
|
+
|
|
75
|
+
# Check if it exists
|
|
76
|
+
try:
|
|
77
|
+
self._client.head_object(Bucket=self.config.bucket_name, Key=key)
|
|
78
|
+
except botocore.exceptions.ClientError as e:
|
|
79
|
+
if e.response["Error"]["Code"] == "404":
|
|
80
|
+
return None
|
|
81
|
+
else:
|
|
82
|
+
raise RuntimeError(
|
|
83
|
+
f"S3 head_object failed: {e.response.get('Error', {}).get('Message', str(e))}"
|
|
84
|
+
) from e
|
|
85
|
+
|
|
86
|
+
# Return resolved asset ref
|
|
87
|
+
return UploadAssetResult(
|
|
88
|
+
mime_type=mime_type,
|
|
89
|
+
sha256=sha256,
|
|
90
|
+
asset_url=self._assemble_s3_url(key),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def upload_asset(
|
|
94
|
+
self,
|
|
95
|
+
claimed_sha256: SHA256,
|
|
96
|
+
file: BinaryIO,
|
|
97
|
+
mime_type: MediaType,
|
|
98
|
+
) -> UploadAssetResult:
|
|
99
|
+
"""
|
|
100
|
+
Stream `file` directly to S3 with the given MIME type, and returns it public link.
|
|
101
|
+
"""
|
|
102
|
+
# ---- derive a safe S3 key
|
|
103
|
+
ext = mimetypes.guess_extension(mime_type)
|
|
104
|
+
if ext is None:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"Could not determine file extension for mime type {mime_type}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# First I/O pass: compute SHA256
|
|
110
|
+
try:
|
|
111
|
+
file.seek(0)
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
sha256 = hashlib.sha256()
|
|
116
|
+
try:
|
|
117
|
+
file.seek(0)
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
while True:
|
|
121
|
+
chunk = file.read(8192) # 8 MB
|
|
122
|
+
if not chunk:
|
|
123
|
+
break
|
|
124
|
+
sha256.update(chunk)
|
|
125
|
+
sha256 = sha256.hexdigest()
|
|
126
|
+
|
|
127
|
+
if not claimed_sha256 == sha256:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f"SHA256 mismatch: claimed {claimed_sha256}, computed {sha256}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Return immediately if already uploaded:
|
|
133
|
+
key = self._derive_s3_key(sha256=sha256, mime_type=mime_type)
|
|
134
|
+
asset_url = self._assemble_s3_url(key)
|
|
135
|
+
|
|
136
|
+
# Second I/O pass: upload to S3
|
|
137
|
+
file.seek(0)
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
self._client.upload_fileobj(
|
|
141
|
+
Fileobj=file,
|
|
142
|
+
Bucket=self.config.bucket_name,
|
|
143
|
+
Key=key,
|
|
144
|
+
ExtraArgs={
|
|
145
|
+
"ContentType": str(mime_type),
|
|
146
|
+
"ACL": "public-read",
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
except botocore.exceptions.ClientError as e:
|
|
150
|
+
raise RuntimeError(
|
|
151
|
+
f"S3 upload failed: {e.response.get('Error', {}).get('Message', str(e))}"
|
|
152
|
+
) from e
|
|
153
|
+
|
|
154
|
+
# Package return model
|
|
155
|
+
return UploadAssetResult(
|
|
156
|
+
sha256=sha256,
|
|
157
|
+
asset_url=asset_url,
|
|
158
|
+
mime_type=mime_type,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def sync_file(
|
|
162
|
+
self,
|
|
163
|
+
local_path: os.PathLike | str,
|
|
164
|
+
local_root: os.PathLike | str,
|
|
165
|
+
bucket_root: os.PathLike | str,
|
|
166
|
+
force: bool = False,
|
|
167
|
+
) -> str:
|
|
168
|
+
"""
|
|
169
|
+
Upload a single file to S3 under the key derived from its path relative to `local_root`,
|
|
170
|
+
prefixed by `bucket_root`. Skips upload if an object already exists with the same size,
|
|
171
|
+
unless `force=True`. Makes the object public-read and sets ContentType.
|
|
172
|
+
|
|
173
|
+
Returns the public URL of the object.
|
|
174
|
+
"""
|
|
175
|
+
p = Path(local_path)
|
|
176
|
+
local_root = Path(local_root)
|
|
177
|
+
if not p.is_file() or p.is_symlink():
|
|
178
|
+
raise ValueError(f"Unsupported file type: {p}")
|
|
179
|
+
|
|
180
|
+
# Derive S3 key (prefix + rel path), then URL-encode it (keep '/' separators)
|
|
181
|
+
rel = p.relative_to(local_root).as_posix()
|
|
182
|
+
prefix = str(bucket_root).strip("/")
|
|
183
|
+
key = f"{prefix}/{rel}" if prefix else rel
|
|
184
|
+
encoded_key = quote(key, safe="/")
|
|
185
|
+
|
|
186
|
+
# Compute URL (prefer virtual-hosted-style for AWS, path-style for custom endpoints)
|
|
187
|
+
endpoint = self._client.meta.endpoint_url.rstrip("/")
|
|
188
|
+
if "amazonaws.com" in endpoint:
|
|
189
|
+
url = f"https://{self.config.bucket_name}.s3.{self.config.region_name}.amazonaws.com/{encoded_key}"
|
|
190
|
+
else:
|
|
191
|
+
# e.g., MinIO or custom S3-compatible endpoint
|
|
192
|
+
url = f"{endpoint}/{self.config.bucket_name}/{encoded_key}"
|
|
193
|
+
|
|
194
|
+
# Skip if already present with same size (unless force)
|
|
195
|
+
try:
|
|
196
|
+
head = self._client.head_object(Bucket=self.config.bucket_name, Key=key)
|
|
197
|
+
if head.get("ContentLength") == p.stat().st_size and not force:
|
|
198
|
+
return url
|
|
199
|
+
except self._client.exceptions.ClientError as e:
|
|
200
|
+
if e.response.get("Error", {}).get("Code") != "404":
|
|
201
|
+
raise # unexpected error → bubble up
|
|
202
|
+
|
|
203
|
+
# Upload
|
|
204
|
+
mime, _ = mimetypes.guess_type(p.name)
|
|
205
|
+
extra = {
|
|
206
|
+
"ContentType": mime or "application/octet-stream",
|
|
207
|
+
"ACL": "public-read",
|
|
208
|
+
}
|
|
209
|
+
with p.open("rb") as f:
|
|
210
|
+
self._client.upload_fileobj(
|
|
211
|
+
Fileobj=f,
|
|
212
|
+
Bucket=self.config.bucket_name,
|
|
213
|
+
Key=key,
|
|
214
|
+
ExtraArgs=extra,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
print(f"Uploaded {p.name} to S3")
|
|
218
|
+
|
|
219
|
+
return url
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import uuid
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
import glob
|
|
7
|
+
import pydantic
|
|
8
|
+
|
|
9
|
+
import nodekit as nk
|
|
10
|
+
from nodekit.experimental.recruitment_services.base import (
|
|
11
|
+
RecruiterServiceClient,
|
|
12
|
+
CreateHitRequest,
|
|
13
|
+
SendBonusPaymentRequest,
|
|
14
|
+
)
|
|
15
|
+
from nodekit.experimental.s3 import S3Client
|
|
16
|
+
|
|
17
|
+
# %%
|
|
18
|
+
type HitId = str
|
|
19
|
+
type AssignmentId = str
|
|
20
|
+
type WorkerId = str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# %%
|
|
24
|
+
class TraceResult(pydantic.BaseModel):
|
|
25
|
+
hit_id: HitId
|
|
26
|
+
assignment_id: AssignmentId
|
|
27
|
+
worker_id: WorkerId
|
|
28
|
+
trace: nk.Trace | None # If None, validation failed
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class HitRequest(pydantic.BaseModel):
|
|
32
|
+
graph: nk.Graph
|
|
33
|
+
num_assignments: int
|
|
34
|
+
base_payment_usd: str
|
|
35
|
+
title: str
|
|
36
|
+
duration_sec: int = pydantic.Field(gt=0)
|
|
37
|
+
unique_request_token: str | None
|
|
38
|
+
hit_id: HitId
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Helper:
|
|
42
|
+
"""
|
|
43
|
+
Experimental; this might be moved to PsyHub / PsychoScope.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
recruiter_service_client: RecruiterServiceClient,
|
|
49
|
+
s3_client: S3Client,
|
|
50
|
+
local_cachedir: os.PathLike | str,
|
|
51
|
+
):
|
|
52
|
+
self.recruiter_service_client = recruiter_service_client
|
|
53
|
+
self.s3_client = s3_client
|
|
54
|
+
self.local_cachedir = Path(local_cachedir)
|
|
55
|
+
|
|
56
|
+
def _get_hit_cachedir(self) -> Path:
|
|
57
|
+
return (
|
|
58
|
+
self.local_cachedir
|
|
59
|
+
/ "hits"
|
|
60
|
+
/ self.recruiter_service_client.get_recruiter_service_name()
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def create_hit(
|
|
64
|
+
self,
|
|
65
|
+
graph: nk.Graph,
|
|
66
|
+
num_assignments: int,
|
|
67
|
+
base_payment_usd: str,
|
|
68
|
+
title: str,
|
|
69
|
+
duration_sec: int,
|
|
70
|
+
project_name: str,
|
|
71
|
+
unique_request_token: str | None = None,
|
|
72
|
+
) -> HitId:
|
|
73
|
+
"""
|
|
74
|
+
Creates a HIT based on the given Graph.
|
|
75
|
+
Automatically ensures a public site for the Graph exists on S3.
|
|
76
|
+
Caches the HIT (and its Graph) in the local cache.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
graph_site_url = self.upload_graph_site(graph=graph)
|
|
80
|
+
|
|
81
|
+
if unique_request_token is None:
|
|
82
|
+
unique_request_token = uuid.uuid4().hex
|
|
83
|
+
|
|
84
|
+
response = self.recruiter_service_client.create_hit(
|
|
85
|
+
request=CreateHitRequest(
|
|
86
|
+
entrypoint_url=graph_site_url,
|
|
87
|
+
title=title,
|
|
88
|
+
description=title,
|
|
89
|
+
keywords=["psychology", "task", "cognitive", "science", "game"],
|
|
90
|
+
num_assignments=num_assignments,
|
|
91
|
+
duration_sec=duration_sec,
|
|
92
|
+
completion_reward_usd=Decimal(base_payment_usd),
|
|
93
|
+
unique_request_token=unique_request_token,
|
|
94
|
+
allowed_participant_ids=[],
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
hit_id: HitId = response.hit_id
|
|
98
|
+
|
|
99
|
+
# Just save the raw wire model, and hope the asset refs don't change. Todo: !
|
|
100
|
+
try:
|
|
101
|
+
hit_request = HitRequest(
|
|
102
|
+
graph=graph,
|
|
103
|
+
num_assignments=num_assignments,
|
|
104
|
+
base_payment_usd=base_payment_usd,
|
|
105
|
+
title=title,
|
|
106
|
+
duration_sec=duration_sec,
|
|
107
|
+
unique_request_token=unique_request_token,
|
|
108
|
+
hit_id=hit_id,
|
|
109
|
+
)
|
|
110
|
+
savepath = self._get_hit_cachedir() / project_name / f"{hit_id}.json"
|
|
111
|
+
if not savepath.parent.exists():
|
|
112
|
+
savepath.parent.mkdir(parents=True)
|
|
113
|
+
savepath.write_text(hit_request.model_dump_json(indent=2))
|
|
114
|
+
except Exception as e:
|
|
115
|
+
raise Exception(
|
|
116
|
+
f"Could not save Graph for HIT ({hit_id}) to local cache."
|
|
117
|
+
) from e
|
|
118
|
+
|
|
119
|
+
return hit_id
|
|
120
|
+
|
|
121
|
+
def list_hits(self, project_name: str | None = None) -> list[HitId]:
|
|
122
|
+
# Just read off the local cache
|
|
123
|
+
savedir = self._get_hit_cachedir()
|
|
124
|
+
savedir.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
hit_ids: list[HitId] = []
|
|
126
|
+
|
|
127
|
+
if project_name is None:
|
|
128
|
+
search_results = glob.glob(str(savedir / "**/*.json"), recursive=True)
|
|
129
|
+
else:
|
|
130
|
+
search_results = glob.glob(str(savedir / f"{project_name}/*.json"))
|
|
131
|
+
|
|
132
|
+
for path in search_results:
|
|
133
|
+
hit_ids.append(Path(path).stem.split("*.json")[0])
|
|
134
|
+
return hit_ids
|
|
135
|
+
|
|
136
|
+
def upload_graph_site(self, graph: nk.Graph) -> str:
|
|
137
|
+
"""
|
|
138
|
+
Returns a URL to a public Graph site.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
# Build the Graph site
|
|
142
|
+
build_site_result = nk.build_site(graph=graph, savedir=self.local_cachedir)
|
|
143
|
+
|
|
144
|
+
# Ensure index is sync'd
|
|
145
|
+
index_path = build_site_result.site_root / build_site_result.entrypoint
|
|
146
|
+
index_url = self.s3_client.sync_file(
|
|
147
|
+
local_path=index_path,
|
|
148
|
+
local_root=build_site_result.site_root,
|
|
149
|
+
bucket_root="",
|
|
150
|
+
force=False,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Ensure deps are sync'd
|
|
154
|
+
for dep in build_site_result.dependencies:
|
|
155
|
+
self.s3_client.sync_file(
|
|
156
|
+
local_path=build_site_result.site_root / dep,
|
|
157
|
+
local_root=build_site_result.site_root,
|
|
158
|
+
bucket_root="",
|
|
159
|
+
force=False,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return index_url
|
|
163
|
+
|
|
164
|
+
def iter_traces(
|
|
165
|
+
self,
|
|
166
|
+
hit_id: HitId,
|
|
167
|
+
) -> Iterable[TraceResult]:
|
|
168
|
+
"""
|
|
169
|
+
Iterate the Traces collected under the given HIT ID.
|
|
170
|
+
Automatically approves any unapproved assignments.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
# Pull new assignments
|
|
174
|
+
for asn in self.recruiter_service_client.iter_assignments(hit_id=hit_id):
|
|
175
|
+
# Ensure assignment is approved
|
|
176
|
+
if asn.status != "Approved":
|
|
177
|
+
self.recruiter_service_client.approve_assignment(
|
|
178
|
+
assignment_id=asn.assignment_id,
|
|
179
|
+
)
|
|
180
|
+
try:
|
|
181
|
+
trace = nk.Trace.model_validate_json(asn.submission_payload)
|
|
182
|
+
except pydantic.ValidationError:
|
|
183
|
+
print(
|
|
184
|
+
f"\n\n{asn.assignment_id}: Error validating submission payload:",
|
|
185
|
+
asn.submission_payload,
|
|
186
|
+
)
|
|
187
|
+
trace = None
|
|
188
|
+
|
|
189
|
+
yield TraceResult(
|
|
190
|
+
hit_id=hit_id,
|
|
191
|
+
assignment_id=asn.assignment_id,
|
|
192
|
+
worker_id=asn.worker_id,
|
|
193
|
+
trace=trace,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def pay_bonus(
|
|
197
|
+
self,
|
|
198
|
+
worker_id: WorkerId,
|
|
199
|
+
assignment_id: AssignmentId,
|
|
200
|
+
amount_usd: str,
|
|
201
|
+
) -> None:
|
|
202
|
+
self.recruiter_service_client.send_bonus_payment(
|
|
203
|
+
request=SendBonusPaymentRequest(
|
|
204
|
+
assignment_id=assignment_id,
|
|
205
|
+
amount_usd=Decimal(amount_usd),
|
|
206
|
+
worker_id=worker_id,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def get_hit(
|
|
211
|
+
self,
|
|
212
|
+
hit_id: HitId,
|
|
213
|
+
) -> HitRequest:
|
|
214
|
+
"""
|
|
215
|
+
Loads the Graph associated with the given HIT ID.
|
|
216
|
+
(Hit the local cache)
|
|
217
|
+
"""
|
|
218
|
+
savepath = self._get_hit_cachedir() / f"{hit_id}.json"
|
|
219
|
+
if not savepath.parent.exists():
|
|
220
|
+
raise Exception(f"Could not save Graph for HIT {hit_id}.")
|
|
221
|
+
|
|
222
|
+
hit_request = HitRequest.model_validate_json(savepath.read_text())
|
|
223
|
+
return hit_request
|
|
Binary file
|
|
File without changes
|