willisapi-client 1.9.4__tar.gz → 1.9.5__tar.gz

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 (32) hide show
  1. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/PKG-INFO +1 -1
  2. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client/__version__.py +1 -1
  3. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client/services/metadata/upload.py +44 -29
  4. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client/services/metadata/utils.py +45 -7
  5. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client.egg-info/PKG-INFO +1 -1
  6. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/LICENSE +0 -0
  7. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/MANIFEST.in +0 -0
  8. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/README.md +0 -0
  9. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/RELEASE.md +0 -0
  10. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/requirements.txt +0 -0
  11. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/setup.cfg +0 -0
  12. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/setup.py +0 -0
  13. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/tests/test_diarize.py +0 -0
  14. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/tests/test_diarize_call_remaining.py +0 -0
  15. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client/__init__.py +0 -0
  16. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client/logging_setup.py +0 -0
  17. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client/services/__init__.py +0 -0
  18. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client/services/api.py +0 -0
  19. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client/services/diarize/__init__.py +0 -0
  20. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client/services/diarize/diarize_utils.py +0 -0
  21. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client/services/diarize/willisdiarize.py +0 -0
  22. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client/services/diarize/willisdiarize_call_remaining.py +0 -0
  23. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client/services/exceptions.py +0 -0
  24. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client/services/metadata/__init__.py +0 -0
  25. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client/services/metadata/language_choices.py +0 -0
  26. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client/timer.py +0 -0
  27. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client/willisapi_client.py +0 -0
  28. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client.egg-info/SOURCES.txt +0 -0
  29. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client.egg-info/dependency_links.txt +0 -0
  30. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client.egg-info/not-zip-safe +0 -0
  31. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client.egg-info/requires.txt +0 -0
  32. {willisapi_client-1.9.4 → willisapi_client-1.9.5}/willisapi_client.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: willisapi_client
3
- Version: 1.9.4
3
+ Version: 1.9.5
4
4
  Summary: A Python client for willisapi
5
5
  Home-page: https://github.com/bklynhlth/willsiapi_client
6
6
  Author: bklynhlth
@@ -1,7 +1,7 @@
1
1
  """Version details for willisapi_client"""
2
2
 
3
3
  __client__ = "willisapi_client"
4
- __latestVersion__ = "1.9.4"
4
+ __latestVersion__ = "1.9.5"
5
5
  __url__ = "https://github.com/bklynhlth/willsiapi_client"
6
6
  __short_description__ = "A Python client for willisapi"
7
7
  __content_type__ = "text/markdown"
@@ -16,6 +16,8 @@ from willisapi_client.services.metadata.utils import (
16
16
  get_last_n_directories,
17
17
  )
18
18
 
19
+ VALID_SCORE_TYPES = ["rater", "reviewer"]
20
+
19
21
 
20
22
  @measure
21
23
  def upload(api_key: str, csv_path: str, **kwargs):
@@ -101,8 +103,17 @@ def upload(api_key: str, csv_path: str, **kwargs):
101
103
  @measure
102
104
  def processed_upload(api_key: str, csv_path: str, output_path: str, **kwargs):
103
105
 
106
+ score_type = kwargs.get("score_type", "rater")
107
+ if score_type not in VALID_SCORE_TYPES:
108
+ logger.error(
109
+ f"Invalid score_type '{score_type}'. Allowed values: {', '.join(VALID_SCORE_TYPES)}"
110
+ )
111
+ return None
112
+
104
113
  force_upload = kwargs.get("force_upload", False)
105
- csv = ProcessedMetadataValidation(csv_path=csv_path, force_upload=force_upload)
114
+ csv = ProcessedMetadataValidation(
115
+ csv_path=csv_path, force_upload=force_upload, score_type=score_type
116
+ )
106
117
  if csv.load_and_validate():
107
118
  logger.info(f'{datetime.now().strftime("%H:%M:%S")}: csv check passed')
108
119
  csv.create_final_csv()
@@ -121,28 +132,39 @@ def processed_upload(api_key: str, csv_path: str, output_path: str, **kwargs):
121
132
  valid, err = u.validate_processed_data_row()
122
133
  result_row = row.to_dict()
123
134
  if valid:
124
- filename = os.path.basename(row.recording).split(".")[0]
125
135
  files = []
126
- for file in find_files_with_pattern(output_path, filename):
127
- key, error = get_last_n_directories(file, n=2)
128
- if error:
129
- continue
130
- checksum = u.calculate_file_checksum(file)
131
- files.append(
132
- {
133
- "index": index,
134
- "recording": file,
135
- "key": key,
136
- "checksum": checksum,
137
- }
136
+ if score_type != "reviewer":
137
+ recording_val = getattr(row, "recording", None)
138
+ filename = (
139
+ os.path.basename(recording_val).split(".")[0]
140
+ if recording_val
141
+ else None
138
142
  )
139
- payload = u.generate_processed_payload(files)
143
+ for file in (
144
+ find_files_with_pattern(output_path, filename)
145
+ if filename
146
+ else []
147
+ ):
148
+ key, error = get_last_n_directories(file, n=2)
149
+ if error:
150
+ continue
151
+ checksum = u.calculate_file_checksum(file)
152
+ files.append(
153
+ {
154
+ "index": index,
155
+ "recording": file,
156
+ "key": key,
157
+ "checksum": checksum,
158
+ }
159
+ )
160
+ payload = u.generate_processed_payload(files, score_type=score_type)
140
161
  res = u.post(api_key, url, headers, payload)
141
162
  if res.get("upload_status") == "Success":
142
163
  result_row["upload_status"] = "Success"
143
164
  result_row["error"] = None
144
165
 
145
166
  # Handle S3 upload if presigned URL is provided
167
+ s3_errors = []
146
168
  for file_presigned in res.get("response", []):
147
169
  presigned = file_presigned.get("presigned")
148
170
  checksum = file_presigned.get("checksum")
@@ -162,22 +184,15 @@ def processed_upload(api_key: str, csv_path: str, output_path: str, **kwargs):
162
184
  "Content-Type": content_type,
163
185
  },
164
186
  )
165
- if response.status_code == 200:
166
- result_row["upload_status"] = "Success"
167
- else:
168
- result_row["upload_status"] = "Failed"
169
- if result_row["error"] is None:
170
- result_row["error"] = ""
171
- result_row["error"] = (
172
- result_row["error"]
173
- + "\n"
174
- + f"S3 upload failed with status code {response.status_code} for file {recording}"
187
+ if response.status_code != 200:
188
+ s3_errors.append(
189
+ f"S3 upload failed with status code {response.status_code} for file {recording}"
175
190
  )
176
191
  except Exception as ex:
177
- result_row["upload_status"] = "Failed"
178
- if result_row["error"] is None:
179
- result_row["error"] = ""
180
- result_row["error"] = result_row["error"] + "\n" + str(ex)
192
+ s3_errors.append(str(ex))
193
+ if s3_errors:
194
+ result_row["upload_status"] = "Failed"
195
+ result_row["error"] = "\n".join(s3_errors)
181
196
  else:
182
197
  result_row["upload_status"] = "Failed"
183
198
  result_row["error"] = res.get("error")
@@ -12,9 +12,16 @@ from .language_choices import (
12
12
  )
13
13
  from dateutil import parser
14
14
 
15
- ALLOWED_COA_NAMES = ["MADRS", "YMRS", "PHQ-9", "GAD-7", "HAM-D17"]
15
+ ALLOWED_COA_NAMES = ["MADRS", "YMRS", "PHQ-9", "GAD-7", "HAM-D17", "HAMD17"]
16
16
 
17
- COA_ITEM_COUNTS = {"MADRS": 10, "YMRS": 10, "PHQ-9": 9, "GAD-7": 7, "HAM-D17": 17}
17
+ COA_ITEM_COUNTS = {
18
+ "MADRS": 10,
19
+ "YMRS": 10,
20
+ "PHQ-9": 9,
21
+ "GAD-7": 7,
22
+ "HAM-D17": 17,
23
+ "HAMD17": 17,
24
+ }
18
25
 
19
26
 
20
27
  class MetadataValidation:
@@ -453,11 +460,14 @@ class UploadUtils:
453
460
  "checksum": self.calculate_file_checksum(self.row.file_path),
454
461
  "recording_order": int(self.row.recording_order),
455
462
  "is_last_recording": self.row.is_last_recording,
456
- "timestamp": parser.parse(self.row.timestamp).isoformat(),
463
+ # "time_collected": parser.parse(self.row.time_collected).isoformat(),
457
464
  }
458
465
  return payload
459
466
 
460
- def generate_processed_payload(self, files: List[Dict[str, str]]) -> Dict[str, Any]:
467
+ def generate_processed_payload(
468
+ self, files: List[Dict[str, str]], score_type: str = "rater"
469
+ ) -> Dict[str, Any]:
470
+ recording_val = getattr(self.row, "recording", None)
461
471
  payload = {
462
472
  "study_id": self.row.study_id,
463
473
  "site_id": self.row.site_id,
@@ -467,12 +477,16 @@ class UploadUtils:
467
477
  "visit_id": self.row.visit_id,
468
478
  "visit_order": int(self.row.visit_order),
469
479
  "coa_id": self.row.coa_id,
470
- "filename": os.path.basename(self.row.recording),
480
+ "filename": os.path.basename(recording_val) if recording_val else "",
471
481
  "actual_scores": json.loads(self.row.scores_actual),
472
482
  "files": files,
473
483
  "force_upload": self.row.force_upload,
474
484
  "timestamp": parser.parse(self.row.timestamp).isoformat(),
485
+ "score_type": score_type,
475
486
  }
487
+ site_country = getattr(self.row, "site_country", None)
488
+ if site_country:
489
+ payload["site_country"] = site_country
476
490
  return payload
477
491
 
478
492
  def post(
@@ -500,17 +514,17 @@ class ProcessedMetadataValidation:
500
514
  "visit_order",
501
515
  "coa_id",
502
516
  "timestamp",
503
- "recording",
504
517
  "recording_order",
505
518
  "workflow",
506
519
  ]
507
520
 
508
- OPTIONAL_COLUMNS = ["rater_id", "language"]
521
+ OPTIONAL_COLUMNS = ["rater_id", "language", "site_country", "age", "sex", "race"]
509
522
 
510
523
  def __init__(
511
524
  self,
512
525
  csv_path: str,
513
526
  force_upload: bool = False,
527
+ score_type: str = "rater",
514
528
  ):
515
529
  """
516
530
  Initialize validator with CSV file path.
@@ -523,6 +537,7 @@ class ProcessedMetadataValidation:
523
537
  self.errors = []
524
538
  self.transformed_df = None
525
539
  self.force_upload = force_upload
540
+ self.score_type = score_type
526
541
 
527
542
  def validate_columns(self) -> bool:
528
543
  """
@@ -540,6 +555,28 @@ class ProcessedMetadataValidation:
540
555
  return False
541
556
  return True
542
557
 
558
+ def validate_recording_field(self) -> bool:
559
+ """
560
+ Validate the recording column based on score_type.
561
+ - rater: recording column must be present and non-blank for every row.
562
+ - reviewer: recording column may be absent or blank.
563
+ """
564
+ if self.score_type == "rater":
565
+ if "recording" not in self.df.columns:
566
+ self.errors.append(
567
+ "Missing required column: recording (required when score_type='rater')"
568
+ )
569
+ return False
570
+ blank_mask = self.df["recording"].isna() | (
571
+ self.df["recording"].astype(str).str.strip() == ""
572
+ )
573
+ if blank_mask.any():
574
+ self.errors.append(
575
+ "recording must not be blank when score_type='rater'"
576
+ )
577
+ return False
578
+ return True
579
+
543
580
  def validate_data_types(self) -> bool:
544
581
  """
545
582
  Validate data types for key columns.
@@ -607,6 +644,7 @@ class ProcessedMetadataValidation:
607
644
 
608
645
  validations = [
609
646
  self.validate_columns(),
647
+ self.validate_recording_field(),
610
648
  self.validate_data_types(),
611
649
  self.validate_coa_names(),
612
650
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: willisapi-client
3
- Version: 1.9.4
3
+ Version: 1.9.5
4
4
  Summary: A Python client for willisapi
5
5
  Home-page: https://github.com/bklynhlth/willsiapi_client
6
6
  Author: bklynhlth