willisapi-client 1.9.4__tar.gz → 1.9.6__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.6}/PKG-INFO +1 -1
  2. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client/__version__.py +1 -1
  3. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client/services/metadata/upload.py +48 -29
  4. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client/services/metadata/utils.py +45 -7
  5. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client.egg-info/PKG-INFO +1 -1
  6. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/LICENSE +0 -0
  7. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/MANIFEST.in +0 -0
  8. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/README.md +0 -0
  9. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/RELEASE.md +0 -0
  10. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/requirements.txt +0 -0
  11. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/setup.cfg +0 -0
  12. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/setup.py +0 -0
  13. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/tests/test_diarize.py +0 -0
  14. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/tests/test_diarize_call_remaining.py +0 -0
  15. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client/__init__.py +0 -0
  16. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client/logging_setup.py +0 -0
  17. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client/services/__init__.py +0 -0
  18. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client/services/api.py +0 -0
  19. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client/services/diarize/__init__.py +0 -0
  20. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client/services/diarize/diarize_utils.py +0 -0
  21. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client/services/diarize/willisdiarize.py +0 -0
  22. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client/services/diarize/willisdiarize_call_remaining.py +0 -0
  23. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client/services/exceptions.py +0 -0
  24. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client/services/metadata/__init__.py +0 -0
  25. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client/services/metadata/language_choices.py +0 -0
  26. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client/timer.py +0 -0
  27. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client/willisapi_client.py +0 -0
  28. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client.egg-info/SOURCES.txt +0 -0
  29. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client.egg-info/dependency_links.txt +0 -0
  30. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client.egg-info/not-zip-safe +0 -0
  31. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/willisapi_client.egg-info/requires.txt +0 -0
  32. {willisapi_client-1.9.4 → willisapi_client-1.9.6}/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.6
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.6"
5
5
  __url__ = "https://github.com/bklynhlth/willsiapi_client"
6
6
  __short_description__ = "A Python client for willisapi"
7
7
  __content_type__ = "text/markdown"
@@ -5,6 +5,10 @@ import requests
5
5
  import mimetypes
6
6
  import os
7
7
 
8
+ # Register extensions that Python's default mimetypes table doesn't know about.
9
+ # The server signs presigned URLs with these types, so the client must match.
10
+ mimetypes.add_type("application/vnd.apache.parquet", ".parquet")
11
+
8
12
  from willisapi_client.timer import measure
9
13
  from willisapi_client.willisapi_client import WillisapiClient
10
14
  from willisapi_client.logging_setup import logger as logger
@@ -16,6 +20,8 @@ from willisapi_client.services.metadata.utils import (
16
20
  get_last_n_directories,
17
21
  )
18
22
 
23
+ VALID_SCORE_TYPES = ["rater", "reviewer"]
24
+
19
25
 
20
26
  @measure
21
27
  def upload(api_key: str, csv_path: str, **kwargs):
@@ -101,8 +107,17 @@ def upload(api_key: str, csv_path: str, **kwargs):
101
107
  @measure
102
108
  def processed_upload(api_key: str, csv_path: str, output_path: str, **kwargs):
103
109
 
110
+ score_type = kwargs.get("score_type", "rater")
111
+ if score_type not in VALID_SCORE_TYPES:
112
+ logger.error(
113
+ f"Invalid score_type '{score_type}'. Allowed values: {', '.join(VALID_SCORE_TYPES)}"
114
+ )
115
+ return None
116
+
104
117
  force_upload = kwargs.get("force_upload", False)
105
- csv = ProcessedMetadataValidation(csv_path=csv_path, force_upload=force_upload)
118
+ csv = ProcessedMetadataValidation(
119
+ csv_path=csv_path, force_upload=force_upload, score_type=score_type
120
+ )
106
121
  if csv.load_and_validate():
107
122
  logger.info(f'{datetime.now().strftime("%H:%M:%S")}: csv check passed')
108
123
  csv.create_final_csv()
@@ -121,28 +136,39 @@ def processed_upload(api_key: str, csv_path: str, output_path: str, **kwargs):
121
136
  valid, err = u.validate_processed_data_row()
122
137
  result_row = row.to_dict()
123
138
  if valid:
124
- filename = os.path.basename(row.recording).split(".")[0]
125
139
  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
- }
140
+ if score_type != "reviewer":
141
+ recording_val = getattr(row, "recording", None)
142
+ filename = (
143
+ os.path.basename(recording_val).split(".")[0]
144
+ if recording_val
145
+ else None
138
146
  )
139
- payload = u.generate_processed_payload(files)
147
+ for file in (
148
+ find_files_with_pattern(output_path, filename)
149
+ if filename
150
+ else []
151
+ ):
152
+ key, error = get_last_n_directories(file, n=2)
153
+ if error:
154
+ continue
155
+ checksum = u.calculate_file_checksum(file)
156
+ files.append(
157
+ {
158
+ "index": index,
159
+ "recording": file,
160
+ "key": key,
161
+ "checksum": checksum,
162
+ }
163
+ )
164
+ payload = u.generate_processed_payload(files, score_type=score_type)
140
165
  res = u.post(api_key, url, headers, payload)
141
166
  if res.get("upload_status") == "Success":
142
167
  result_row["upload_status"] = "Success"
143
168
  result_row["error"] = None
144
169
 
145
170
  # Handle S3 upload if presigned URL is provided
171
+ s3_errors = []
146
172
  for file_presigned in res.get("response", []):
147
173
  presigned = file_presigned.get("presigned")
148
174
  checksum = file_presigned.get("checksum")
@@ -162,22 +188,15 @@ def processed_upload(api_key: str, csv_path: str, output_path: str, **kwargs):
162
188
  "Content-Type": content_type,
163
189
  },
164
190
  )
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}"
191
+ if response.status_code != 200:
192
+ s3_errors.append(
193
+ f"S3 upload failed with status code {response.status_code} for file {recording}"
175
194
  )
176
195
  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)
196
+ s3_errors.append(str(ex))
197
+ if s3_errors:
198
+ result_row["upload_status"] = "Failed"
199
+ result_row["error"] = "\n".join(s3_errors)
181
200
  else:
182
201
  result_row["upload_status"] = "Failed"
183
202
  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.6
4
4
  Summary: A Python client for willisapi
5
5
  Home-page: https://github.com/bklynhlth/willsiapi_client
6
6
  Author: bklynhlth