pearmut 0.1.2__py3-none-any.whl → 0.1.3__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.
pearmut/app.py CHANGED
@@ -8,8 +8,8 @@ from fastapi.responses import JSONResponse
8
8
  from fastapi.staticfiles import StaticFiles
9
9
  from pydantic import BaseModel
10
10
 
11
- from .assignment import get_next_item, reset_task, update_progress
12
- from .utils import ROOT, load_progress_data, save_progress_data
11
+ from .assignment import get_i_item, get_next_item, reset_task, update_progress
12
+ from .utils import ROOT, load_progress_data, save_db_payload, save_progress_data
13
13
 
14
14
  os.makedirs(f"{ROOT}/data/outputs", exist_ok=True)
15
15
 
@@ -36,7 +36,7 @@ class LogResponseRequest(BaseModel):
36
36
  campaign_id: str
37
37
  user_id: str
38
38
  item_i: int
39
- payload: Any
39
+ payload: dict[str, Any]
40
40
 
41
41
 
42
42
  @app.post("/log-response")
@@ -45,6 +45,7 @@ async def _log_response(request: LogResponseRequest):
45
45
 
46
46
  campaign_id = request.campaign_id
47
47
  user_id = request.user_id
48
+ item_i = request.item_i
48
49
 
49
50
  if campaign_id not in progress_data:
50
51
  return JSONResponse(content={"error": "Unknown campaign ID"}, status_code=400)
@@ -52,8 +53,7 @@ async def _log_response(request: LogResponseRequest):
52
53
  return JSONResponse(content={"error": "Unknown user ID"}, status_code=400)
53
54
 
54
55
  # append response to the output log
55
- with open(f"{ROOT}/data/outputs/{campaign_id}.jsonl", "a") as log_file:
56
- log_file.write(json.dumps(request.payload, ensure_ascii=False) + "\n")
56
+ save_db_payload(campaign_id, request.payload | {"user_id": user_id, "item_i": item_i})
57
57
 
58
58
  # if actions were submitted, we can log time data
59
59
  if "actions" in request.payload:
@@ -97,6 +97,32 @@ async def _get_next_item(request: NextItemRequest):
97
97
  )
98
98
 
99
99
 
100
+ class GetItemRequest(BaseModel):
101
+ campaign_id: str
102
+ user_id: str
103
+ item_i: int
104
+
105
+
106
+ @app.post("/get-i-item")
107
+ async def _get_i_item(request: GetItemRequest):
108
+ campaign_id = request.campaign_id
109
+ user_id = request.user_id
110
+ item_i = request.item_i
111
+
112
+ if campaign_id not in progress_data:
113
+ return JSONResponse(content={"error": "Unknown campaign ID"}, status_code=400)
114
+ if user_id not in progress_data[campaign_id]:
115
+ return JSONResponse(content={"error": "Unknown user ID"}, status_code=400)
116
+
117
+ return get_i_item(
118
+ campaign_id,
119
+ user_id,
120
+ tasks_data,
121
+ progress_data,
122
+ item_i,
123
+ )
124
+
125
+
100
126
  class DashboardDataRequest(BaseModel):
101
127
  campaign_id: str
102
128
  token: str | None = None
pearmut/assignment.py CHANGED
@@ -3,6 +3,8 @@ from typing import Any
3
3
 
4
4
  from fastapi.responses import JSONResponse
5
5
 
6
+ from .utils import get_db_log_item
7
+
6
8
 
7
9
  def _completed_response(
8
10
  progress_data: dict,
@@ -37,13 +39,121 @@ def get_next_item(
37
39
  if assignment == "task-based":
38
40
  return get_next_item_taskbased(campaign_id, user_id, tasks_data, progress_data)
39
41
  elif assignment == "single-stream":
40
- return get_next_item_single_stream(campaign_id, user_id, tasks_data, progress_data)
42
+ return get_next_item_singlestream(campaign_id, user_id, tasks_data, progress_data)
41
43
  elif assignment == "dynamic":
42
44
  return get_next_item_dynamic(campaign_id, user_id, tasks_data, progress_data)
43
45
  else:
44
46
  return JSONResponse(content={"error": "Unknown campaign assignment type"}, status_code=400)
45
47
 
46
48
 
49
+ def get_i_item(
50
+ campaign_id: str,
51
+ user_id: str,
52
+ tasks_data: dict,
53
+ progress_data: dict,
54
+ item_i: int,
55
+ ) -> JSONResponse:
56
+ """
57
+ Get a specific item by index for the user in the specified campaign.
58
+ """
59
+ assignment = tasks_data[campaign_id]["info"]["assignment"]
60
+ if assignment == "task-based":
61
+ return get_i_item_taskbased(campaign_id, user_id, tasks_data, progress_data, item_i)
62
+ elif assignment == "single-stream":
63
+ return get_i_item_singlestream(campaign_id, user_id, tasks_data, progress_data, item_i)
64
+ else:
65
+ return JSONResponse(content={"error": "Get item not supported for this assignment type"}, status_code=400)
66
+
67
+
68
+ def get_i_item_taskbased(
69
+ campaign_id: str,
70
+ user_id: str,
71
+ data_all: dict,
72
+ progress_data: dict,
73
+ item_i: int,
74
+ ) -> JSONResponse:
75
+ """
76
+ Get specific item for task-based protocol.
77
+ """
78
+ user_progress = progress_data[campaign_id][user_id]
79
+ if all(user_progress["progress"]):
80
+ return _completed_response(progress_data, campaign_id, user_id)
81
+
82
+ # try to get existing annotations if any
83
+ items_existing = get_db_log_item(campaign_id, user_id, item_i)
84
+ if items_existing:
85
+ # get the latest ones
86
+ payload_existing = items_existing[-1]["annotations"]
87
+
88
+ if item_i < 0 or item_i >= len(data_all[campaign_id]["data"][user_id]):
89
+ return JSONResponse(
90
+ content={"status": "error", "message": "Item index out of range"},
91
+ status_code=400
92
+ )
93
+
94
+ return JSONResponse(
95
+ content={
96
+ "status": "ok",
97
+ "progress": user_progress["progress"],
98
+ "time": user_progress["time"],
99
+ "info": {
100
+ "item_i": item_i,
101
+ } | {
102
+ k: v
103
+ for k, v in data_all[campaign_id]["info"].items()
104
+ if k.startswith("protocol")
105
+ },
106
+ "payload": data_all[campaign_id]["data"][user_id][item_i]
107
+ } | ({"payload_existing": payload_existing} if items_existing else {}),
108
+ status_code=200
109
+ )
110
+
111
+
112
+ def get_i_item_singlestream(
113
+ campaign_id: str,
114
+ user_id: str,
115
+ data_all: dict,
116
+ progress_data: dict,
117
+ item_i: int,
118
+ ) -> JSONResponse:
119
+ """
120
+ Get specific item for single-stream assignment.
121
+ """
122
+ user_progress = progress_data[campaign_id][user_id]
123
+ if all(user_progress["progress"]):
124
+ return _completed_response(progress_data, campaign_id, user_id)
125
+
126
+ # try to get existing annotations if any
127
+ # note the None user_id since it is shared
128
+ items_existing = get_db_log_item(campaign_id, None, item_i)
129
+ if items_existing:
130
+ # get the latest ones
131
+ payload_existing = items_existing[-1]["annotations"]
132
+
133
+ if item_i < 0 or item_i >= len(data_all[campaign_id]["data"]):
134
+ return JSONResponse(
135
+ content={"status": "error", "message": "Item index out of range"},
136
+ status_code=400
137
+ )
138
+
139
+ return JSONResponse(
140
+ content={
141
+ "status": "ok",
142
+ "progress": user_progress["progress"],
143
+ "time": user_progress["time"],
144
+ "info": {
145
+ "item_i": item_i,
146
+ } | {
147
+ k: v
148
+ for k, v in data_all[campaign_id]["info"].items()
149
+ if k.startswith("protocol")
150
+ },
151
+ "payload": data_all[campaign_id]["data"][item_i]
152
+ } | ({"payload_existing": payload_existing} if items_existing else {}),
153
+ status_code=200
154
+ )
155
+
156
+
47
157
  def get_next_item_taskbased(
48
158
  campaign_id: str,
49
159
  user_id: str,
@@ -51,7 +161,7 @@ def get_next_item_taskbased(
51
161
  progress_data: dict,
52
162
  ) -> JSONResponse:
53
163
  """
54
- Get the next item for task-based protocol.
164
+ Get the next item for task-based assignment.
55
165
  """
56
166
  user_progress = progress_data[campaign_id][user_id]
57
167
  if all(user_progress["progress"]):
@@ -59,6 +169,13 @@ def get_next_item_taskbased(
59
169
 
60
170
  # find first incomplete item
61
171
  item_i = min([i for i, v in enumerate(user_progress["progress"]) if not v])
172
+
173
+ # try to get existing annotations if any
174
+ items_existing = get_db_log_item(campaign_id, user_id, item_i)
175
+ if items_existing:
176
+ # get the latest ones
177
+ payload_existing = items_existing[-1]["annotations"]
178
+
62
179
  return JSONResponse(
63
180
  content={
64
181
  "status": "ok",
@@ -71,23 +188,20 @@ def get_next_item_taskbased(
71
188
  for k, v in data_all[campaign_id]["info"].items()
72
189
  if k.startswith("protocol")
73
190
  },
74
- "payload": data_all[campaign_id]["data"][user_id][item_i]},
191
+ "payload": data_all[campaign_id]["data"][user_id][item_i]
192
+ } | ({"payload_existing": payload_existing} if items_existing else {}),
75
193
  status_code=200
76
194
  )
77
195
 
78
196
 
79
- def get_next_item_dynamic(campaign_data: dict, user_id: str, progress_data: dict, data_all: dict):
80
- raise NotImplementedError("Dynamic protocol is not implemented yet.")
81
-
82
-
83
- def get_next_item_single_stream(
197
+ def get_next_item_singlestream(
84
198
  campaign_id: str,
85
199
  user_id: str,
86
200
  data_all: dict,
87
201
  progress_data: dict,
88
202
  ) -> JSONResponse:
89
203
  """
90
- Get the next item for single-stream protocol.
204
+ Get the next item for single-stream assignment.
91
205
  In this mode, all users share the same pool of items.
92
206
  Items are randomly selected from unfinished items.
93
207
 
@@ -104,6 +218,13 @@ def get_next_item_single_stream(
104
218
  incomplete_indices = [i for i, v in enumerate(progress) if not v]
105
219
  item_i = random.choice(incomplete_indices)
106
220
 
221
+ # try to get existing annotations if any
222
+ # note the None user_id since it is shared
223
+ items_existing = get_db_log_item(campaign_id, None, item_i)
224
+ if items_existing:
225
+ # get the latest ones
226
+ payload_existing = items_existing[-1]["annotations"]
227
+
107
228
  return JSONResponse(
108
229
  content={
109
230
  "status": "ok",
@@ -116,11 +237,18 @@ def get_next_item_single_stream(
116
237
  for k, v in data_all[campaign_id]["info"].items()
117
238
  if k.startswith("protocol")
118
239
  },
119
- "payload": data_all[campaign_id]["data"][item_i]},
240
+ "payload": data_all[campaign_id]["data"][item_i]
241
+ } | ({"payload_existing": payload_existing} if items_existing else {}),
120
242
  status_code=200
121
243
  )
122
244
 
123
245
 
246
+
247
+ def get_next_item_dynamic(campaign_data: dict, user_id: str, progress_data: dict, data_all: dict):
248
+ raise NotImplementedError("Dynamic protocol is not implemented yet.")
249
+
250
+
251
+
124
252
  def _reset_user_time(progress_data: dict, campaign_id: str, user_id: str) -> None:
125
253
  """Reset time tracking fields for a user."""
126
254
  progress_data[campaign_id][user_id]["time"] = 0.0