willisapi-client 1.9.6__tar.gz → 1.9.8__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 (34) hide show
  1. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/PKG-INFO +1 -1
  2. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/__version__.py +1 -1
  3. willisapi_client-1.9.8/willisapi_client/services/metadata/archive.py +83 -0
  4. willisapi_client-1.9.8/willisapi_client/services/metadata/language_choices.py +124 -0
  5. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/metadata/upload.py +110 -28
  6. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/metadata/utils.py +39 -5
  7. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/willisapi_client.py +15 -8
  8. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client.egg-info/PKG-INFO +1 -1
  9. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client.egg-info/SOURCES.txt +1 -0
  10. willisapi_client-1.9.6/willisapi_client/services/metadata/language_choices.py +0 -86
  11. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/LICENSE +0 -0
  12. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/MANIFEST.in +0 -0
  13. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/README.md +0 -0
  14. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/RELEASE.md +0 -0
  15. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/requirements.txt +0 -0
  16. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/setup.cfg +0 -0
  17. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/setup.py +0 -0
  18. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/tests/test_diarize.py +0 -0
  19. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/tests/test_diarize_call_remaining.py +0 -0
  20. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/__init__.py +0 -0
  21. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/logging_setup.py +0 -0
  22. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/__init__.py +0 -0
  23. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/api.py +0 -0
  24. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/diarize/__init__.py +0 -0
  25. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/diarize/diarize_utils.py +0 -0
  26. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/diarize/willisdiarize.py +0 -0
  27. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/diarize/willisdiarize_call_remaining.py +0 -0
  28. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/exceptions.py +0 -0
  29. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/metadata/__init__.py +0 -0
  30. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/timer.py +0 -0
  31. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client.egg-info/dependency_links.txt +0 -0
  32. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client.egg-info/not-zip-safe +0 -0
  33. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client.egg-info/requires.txt +0 -0
  34. {willisapi_client-1.9.6 → willisapi_client-1.9.8}/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.6
3
+ Version: 1.9.8
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.6"
4
+ __latestVersion__ = "1.9.8"
5
5
  __url__ = "https://github.com/bklynhlth/willsiapi_client"
6
6
  __short_description__ = "A Python client for willisapi"
7
7
  __content_type__ = "text/markdown"
@@ -0,0 +1,83 @@
1
+ import os
2
+ import requests
3
+
4
+ from willisapi_client.willisapi_client import WillisapiClient
5
+ from willisapi_client.logging_setup import logger as logger
6
+
7
+
8
+ def _archive_headers(api_key):
9
+ return {
10
+ "Content-Type": "application/json",
11
+ "Accept": "application/json",
12
+ "Authorization": f"token {api_key}",
13
+ }
14
+
15
+
16
+ def archive_metadata_csv(api_key, csv_path, total_rows, upload_type, env=None):
17
+ """Archive the source metadata CSV alongside the data upload.
18
+
19
+ Creates a server-side tracking row, then PUTs the raw CSV to the returned
20
+ presigned S3 URL. Returns the tracking ``record_id`` on success (so the
21
+ caller can finalize it with row counts), or ``None`` on failure.
22
+
23
+ All failures are logged and swallowed — archiving must never abort the
24
+ actual data upload.
25
+ """
26
+ try:
27
+ wc = WillisapiClient(env=env)
28
+ payload = {
29
+ "filename": os.path.basename(csv_path),
30
+ "total_rows": total_rows,
31
+ "upload_type": upload_type,
32
+ }
33
+ res = requests.post(
34
+ wc.get_csv_archive_url(), headers=_archive_headers(api_key), json=payload
35
+ )
36
+ if res.status_code != 201:
37
+ logger.warning(f"CSV archive init failed: {res.status_code} {res.text}")
38
+ return None
39
+
40
+ body = res.json()
41
+ record_id = body.get("record_id")
42
+ presigned = body.get("presigned_url")
43
+ if not presigned:
44
+ logger.warning("CSV archive: no presigned URL returned")
45
+ return None
46
+
47
+ with open(csv_path, "rb") as f:
48
+ put_res = requests.put(
49
+ presigned, data=f, headers={"Content-Type": "text/csv"}
50
+ )
51
+ if put_res.status_code not in (200, 204):
52
+ logger.warning(f"CSV archive S3 upload failed: {put_res.status_code}")
53
+ finalize_metadata_csv(api_key, record_id, "failed", 0, 0, env=env)
54
+ return None
55
+
56
+ return record_id
57
+ except Exception as ex:
58
+ logger.warning(f"CSV archive error: {ex}")
59
+ return None
60
+
61
+
62
+ def finalize_metadata_csv(
63
+ api_key, record_id, status, successful_rows, failed_rows, env=None
64
+ ):
65
+ """Report the CSV archive outcome and parsed row counts to the server."""
66
+ if not record_id:
67
+ return
68
+ try:
69
+ wc = WillisapiClient(env=env)
70
+ payload = {
71
+ "status": status,
72
+ "successful_rows": successful_rows,
73
+ "failed_rows": failed_rows,
74
+ }
75
+ res = requests.patch(
76
+ wc.get_csv_archive_finalize_url(record_id),
77
+ headers=_archive_headers(api_key),
78
+ json=payload,
79
+ )
80
+ if res.status_code != 200:
81
+ logger.warning(f"CSV archive finalize failed: {res.status_code} {res.text}")
82
+ except Exception as ex:
83
+ logger.warning(f"CSV archive finalize error: {ex}")
@@ -0,0 +1,124 @@
1
+ # https://docs.aws.amazon.com/transcribe/latest/dg/supported-languages.html
2
+ # (language_code, display_name)
3
+ SUPPORTED_LANGUAGES = [
4
+ ("en-US", "English (US)"),
5
+ ("es-US", "Spanish (US)"),
6
+ ("ab-GE", "Abkhaz"),
7
+ ("af-ZA", "Afrikaans"),
8
+ ("sq-AL", "Albanian"),
9
+ ("am-ET", "Amharic"),
10
+ ("ar-AE", "Arabic, Gulf"),
11
+ ("ar-SA", "Arabic, Modern Standard"),
12
+ ("hy-AM", "Armenian"),
13
+ ("ast-ES", "Asturian"),
14
+ ("az-AZ", "Azerbaijani"),
15
+ ("ba-RU", "Bashkir"),
16
+ ("eu-ES", "Basque"),
17
+ ("be-BY", "Belarusian"),
18
+ ("bn-IN", "Bengali"),
19
+ ("bs-BA", "Bosnian"),
20
+ ("my-MM", "Burmese"),
21
+ ("bg-BG", "Bulgarian"),
22
+ ("ca-ES", "Catalan"),
23
+ ("ckb-IR", "Central Kurdish (Iran)"),
24
+ ("ckb-IQ", "Central Kurdish (Iraq)"),
25
+ ("zh-HK", "Chinese, Cantonese"),
26
+ ("zh-CN", "Chinese, Simplified"),
27
+ ("zh-TW", "Chinese, Traditional"),
28
+ ("hr-HR", "Croatian"),
29
+ ("cs-CZ", "Czech"),
30
+ ("da-DK", "Danish"),
31
+ ("nl-NL", "Dutch"),
32
+ ("en-AU", "English, Australian"),
33
+ ("en-GB", "English, British"),
34
+ ("en-IN", "English, Indian"),
35
+ ("en-IE", "English, Irish"),
36
+ ("en-NZ", "English, New Zealand"),
37
+ ("en-AB", "English, Scottish"),
38
+ ("en-ZA", "English, South African"),
39
+ ("en-WL", "English, Welsh"),
40
+ ("et-EE", "Estonian"),
41
+ ("et-ET", "Estonian (et-ET)"),
42
+ ("fa-IR", "Farsi"),
43
+ ("fa-AF", "Farsi, Afghan"),
44
+ ("fi-FI", "Finnish"),
45
+ ("fr-FR", "French"),
46
+ ("fr-CA", "French, Canadian"),
47
+ ("gl-ES", "Galician"),
48
+ ("ka-GE", "Georgian"),
49
+ ("de-DE", "German"),
50
+ ("de-CH", "German, Swiss"),
51
+ ("el-GR", "Greek"),
52
+ ("gu-IN", "Gujarati"),
53
+ ("ht-HT", "Haitian Creole"),
54
+ ("ha-NG", "Hausa"),
55
+ ("he-IL", "Hebrew"),
56
+ ("hi-IN", "Hindi, Indian"),
57
+ ("hu-HU", "Hungarian"),
58
+ ("is-IS", "Icelandic"),
59
+ ("id-ID", "Indonesian"),
60
+ ("it-IT", "Italian"),
61
+ ("ja-JP", "Japanese"),
62
+ ("jv-ID", "Javanese"),
63
+ ("kab-DZ", "Kabyle"),
64
+ ("kn-IN", "Kannada"),
65
+ ("kk-KZ", "Kazakh"),
66
+ ("km-KH", "Khmer"),
67
+ ("rw-RW", "Kinyarwanda"),
68
+ ("ko-KR", "Korean"),
69
+ ("ky-KG", "Kyrgyz"),
70
+ ("lv-LV", "Latvian"),
71
+ ("lt-LT", "Lithuanian"),
72
+ ("lg-IN", "Luganda"),
73
+ ("mk-MK", "Macedonian"),
74
+ ("ms-MY", "Malay"),
75
+ ("ml-IN", "Malayalam"),
76
+ ("mt-MT", "Maltese"),
77
+ ("mr-IN", "Marathi"),
78
+ ("mhr-RU", "Meadow Mari"),
79
+ ("mn-MN", "Mongolian"),
80
+ ("ne-NP", "Nepali"),
81
+ ("no-NO", "Norwegian Bokmål"),
82
+ ("or-IN", "Odia/Oriya"),
83
+ ("ps-AF", "Pashto"),
84
+ ("pl-PL", "Polish"),
85
+ ("pt-PT", "Portuguese"),
86
+ ("pt-BR", "Portuguese, Brazilian"),
87
+ ("pa-IN", "Punjabi"),
88
+ ("ro-RO", "Romanian"),
89
+ ("ru-RU", "Russian"),
90
+ ("sr-RS", "Serbian"),
91
+ ("si-LK", "Sinhala"),
92
+ ("sk-SK", "Slovak"),
93
+ ("sl-SI", "Slovenian"),
94
+ ("so-SO", "Somali"),
95
+ ("es-ES", "Spanish"),
96
+ ("es-MX", "Spanish, Mexican"),
97
+ ("su-ID", "Sundanese"),
98
+ ("sw-KE", "Swahili, Kenya"),
99
+ ("sw-BI", "Swahili, Burundi"),
100
+ ("sw-RW", "Swahili, Rwanda"),
101
+ ("sw-TZ", "Swahili, Tanzania"),
102
+ ("sw-UG", "Swahili, Uganda"),
103
+ ("sv-SE", "Swedish"),
104
+ ("tl-PH", "Tagalog/Filipino"),
105
+ ("ta-IN", "Tamil"),
106
+ ("tt-RU", "Tatar"),
107
+ ("te-IN", "Telugu"),
108
+ ("th-TH", "Thai"),
109
+ ("tr-TR", "Turkish"),
110
+ ("uk-UA", "Ukrainian"),
111
+ ("ug-CN", "Uyghur"),
112
+ ("uz-UZ", "Uzbek"),
113
+ ("vi-VN", "Vietnamese"),
114
+ ("cy-WL", "Welsh"),
115
+ ("wo-SN", "Wolof"),
116
+ ("zu-ZA", "Zulu"),
117
+ ]
118
+
119
+ LANGUAGE_CHOICES = [code for code, _ in SUPPORTED_LANGUAGES]
120
+
121
+ SEX_CHOICES = [
122
+ ("Male", "M"),
123
+ ("Female", "F"),
124
+ ]
@@ -4,6 +4,8 @@ from datetime import datetime
4
4
  import requests
5
5
  import mimetypes
6
6
  import os
7
+ import time
8
+ from concurrent.futures import ThreadPoolExecutor
7
9
 
8
10
  # Register extensions that Python's default mimetypes table doesn't know about.
9
11
  # The server signs presigned URLs with these types, so the client must match.
@@ -16,13 +18,54 @@ from willisapi_client.services.metadata.utils import (
16
18
  MetadataValidation,
17
19
  ProcessedMetadataValidation,
18
20
  UploadUtils,
21
+ build_retry_session,
19
22
  find_files_with_pattern,
20
23
  get_last_n_directories,
21
24
  )
25
+ from willisapi_client.services.metadata.archive import (
26
+ archive_metadata_csv,
27
+ finalize_metadata_csv,
28
+ )
22
29
 
23
30
  VALID_SCORE_TYPES = ["rater", "reviewer"]
24
31
 
25
32
 
33
+ def _put_file_to_s3(file_presigned: dict):
34
+ """Upload a single file to its presigned S3 URL.
35
+
36
+ Returns an error string if the upload fails, otherwise None. Presigned
37
+ URLs from a single recording are all signed at once and share one expiry
38
+ window, so callers should run these concurrently to fit within it.
39
+ """
40
+ presigned = file_presigned.get("presigned")
41
+ checksum = file_presigned.get("checksum")
42
+ recording = file_presigned.get("recording")
43
+ try:
44
+ content_type, _ = mimetypes.guess_type(recording)
45
+ if not content_type:
46
+ content_type = "text/csv"
47
+ with open(recording, "rb") as f:
48
+ session = build_retry_session()
49
+ response = session.put(
50
+ presigned,
51
+ data=f,
52
+ headers={
53
+ "x-amz-checksum-sha256": checksum,
54
+ "x-amz-sdk-checksum-algorithm": "SHA256",
55
+ "Content-Type": content_type,
56
+ },
57
+ timeout=(10, 300),
58
+ )
59
+ if response.status_code != 200:
60
+ return (
61
+ f"S3 upload failed with status code {response.status_code} "
62
+ f"for file {recording}: {response.text}"
63
+ )
64
+ except Exception as ex:
65
+ return str(ex)
66
+ return None
67
+
68
+
26
69
  @measure
27
70
  def upload(api_key: str, csv_path: str, **kwargs):
28
71
 
@@ -38,6 +81,15 @@ def upload(api_key: str, csv_path: str, **kwargs):
38
81
  headers["Authorization"] = f"token {api_key}"
39
82
  logger.info(f'{datetime.now().strftime("%H:%M:%S")}: beginning upload')
40
83
 
84
+ # Archive the source metadata CSV and open a tracking record.
85
+ archive_record_id = archive_metadata_csv(
86
+ api_key,
87
+ csv_path,
88
+ int(csv.transformed_df.shape[0]),
89
+ upload_type="data",
90
+ env=kwargs.get("env"),
91
+ )
92
+
41
93
  results = []
42
94
  for index, row in tqdm(
43
95
  csv.transformed_df.iterrows(), total=csv.transformed_df.shape[0]
@@ -62,7 +114,8 @@ def upload(api_key: str, csv_path: str, **kwargs):
62
114
  if not content_type:
63
115
  content_type = "application/octet-stream"
64
116
  with open(row.file_path, "rb") as f:
65
- response = requests.put(
117
+ session = build_retry_session()
118
+ response = session.put(
66
119
  presigned,
67
120
  data=f,
68
121
  headers={
@@ -72,13 +125,14 @@ def upload(api_key: str, csv_path: str, **kwargs):
72
125
  "x-amz-sdk-checksum-algorithm": "SHA256",
73
126
  "Content-Type": content_type,
74
127
  },
128
+ timeout=(10, 300),
75
129
  )
76
130
  if response.status_code == 200:
77
131
  result_row["upload_status"] = "Success"
78
132
  else:
79
133
  result_row["upload_status"] = "Failed"
80
134
  result_row["error"] = (
81
- f"S3 upload failed with status code {response.status_code}"
135
+ f"S3 upload failed with status code {response.status_code}: {response.text}"
82
136
  )
83
137
  except Exception as ex:
84
138
  result_row["upload_status"] = "Failed"
@@ -96,6 +150,16 @@ def upload(api_key: str, csv_path: str, **kwargs):
96
150
  result_row["error"] = f"{err}"
97
151
  results.append(result_row)
98
152
 
153
+ successful_rows = sum(1 for r in results if r.get("upload_status") == "Success")
154
+ finalize_metadata_csv(
155
+ api_key,
156
+ archive_record_id,
157
+ "successful",
158
+ successful_rows,
159
+ len(results) - successful_rows,
160
+ env=kwargs.get("env"),
161
+ )
162
+
99
163
  results_df = pd.DataFrame(results)
100
164
  return results_df
101
165
  else:
@@ -128,7 +192,22 @@ def processed_upload(api_key: str, csv_path: str, output_path: str, **kwargs):
128
192
  headers["Authorization"] = f"token {api_key}"
129
193
  logger.info(f'{datetime.now().strftime("%H:%M:%S")}: beginning upload')
130
194
 
195
+ # Archive the source processed-data metadata CSV and open a tracking record.
196
+ archive_record_id = archive_metadata_csv(
197
+ api_key,
198
+ csv_path,
199
+ int(csv.transformed_df.shape[0]),
200
+ upload_type="processed_data",
201
+ env=kwargs.get("env"),
202
+ )
203
+
131
204
  results = []
205
+ # Throttle S3 uploads: pause 3s each time the running total of files
206
+ # sent to S3 crosses another 100, to avoid overwhelming the connection.
207
+ S3_SLEEP_EVERY = 100
208
+ S3_SLEEP_SECONDS = 3
209
+ s3_uploaded_total = 0
210
+ next_sleep_threshold = S3_SLEEP_EVERY
132
211
  for index, row in tqdm(
133
212
  csv.transformed_df.iterrows(), total=csv.transformed_df.shape[0]
134
213
  ):
@@ -167,33 +246,26 @@ def processed_upload(api_key: str, csv_path: str, output_path: str, **kwargs):
167
246
  result_row["upload_status"] = "Success"
168
247
  result_row["error"] = None
169
248
 
170
- # Handle S3 upload if presigned URL is provided
249
+ # Upload every file for this recording concurrently. All
250
+ # presigned URLs in this response share one short expiry
251
+ # window, so uploading them sequentially risks the later
252
+ # files expiring (403). A thread pool sized to the CPU
253
+ # count keeps them within the window (I/O-bound work).
254
+ files_to_upload = res.get("response", [])
171
255
  s3_errors = []
172
- for file_presigned in res.get("response", []):
173
- presigned = file_presigned.get("presigned")
174
- checksum = file_presigned.get("checksum")
175
- recording = file_presigned.get("recording")
176
- index = file_presigned.get("index")
177
- try:
178
- content_type, _ = mimetypes.guess_type(recording)
179
- if not content_type:
180
- content_type = "text/csv"
181
- with open(recording, "rb") as f:
182
- response = requests.put(
183
- presigned,
184
- data=f,
185
- headers={
186
- "x-amz-checksum-sha256": checksum,
187
- "x-amz-sdk-checksum-algorithm": "SHA256",
188
- "Content-Type": content_type,
189
- },
190
- )
191
- if response.status_code != 200:
192
- s3_errors.append(
193
- f"S3 upload failed with status code {response.status_code} for file {recording}"
194
- )
195
- except Exception as ex:
196
- s3_errors.append(str(ex))
256
+ if files_to_upload:
257
+ max_workers = min(len(files_to_upload), os.cpu_count() or 1)
258
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
259
+ for error in executor.map(
260
+ _put_file_to_s3, files_to_upload
261
+ ):
262
+ if error:
263
+ s3_errors.append(error)
264
+ # Pause after every 100 files uploaded to S3.
265
+ s3_uploaded_total += len(files_to_upload)
266
+ while s3_uploaded_total >= next_sleep_threshold:
267
+ time.sleep(S3_SLEEP_SECONDS)
268
+ next_sleep_threshold += S3_SLEEP_EVERY
197
269
  if s3_errors:
198
270
  result_row["upload_status"] = "Failed"
199
271
  result_row["error"] = "\n".join(s3_errors)
@@ -205,6 +277,16 @@ def processed_upload(api_key: str, csv_path: str, output_path: str, **kwargs):
205
277
  result_row["error"] = f"{err}"
206
278
  results.append(result_row)
207
279
 
280
+ successful_rows = sum(1 for r in results if r.get("upload_status") == "Success")
281
+ finalize_metadata_csv(
282
+ api_key,
283
+ archive_record_id,
284
+ "successful",
285
+ successful_rows,
286
+ len(results) - successful_rows,
287
+ env=kwargs.get("env"),
288
+ )
289
+
208
290
  results_df = pd.DataFrame(results)
209
291
  return results_df
210
292
  else:
@@ -6,6 +6,9 @@ import os
6
6
  import hashlib
7
7
  import base64
8
8
  import requests
9
+ from functools import lru_cache
10
+ from requests.adapters import HTTPAdapter
11
+ from urllib3.util.retry import Retry
9
12
  from urllib.parse import urlsplit
10
13
  from .language_choices import (
11
14
  LANGUAGE_CHOICES,
@@ -14,6 +17,32 @@ from dateutil import parser
14
17
 
15
18
  ALLOWED_COA_NAMES = ["MADRS", "YMRS", "PHQ-9", "GAD-7", "HAM-D17", "HAMD17"]
16
19
 
20
+ @lru_cache(maxsize=None)
21
+ def build_retry_session(total: int = 3, backoff_factor: float = 1) -> requests.Session:
22
+ """Return a requests Session that retries transient network failures.
23
+
24
+ Retries connection errors (e.g. ConnectionResetError / 'Connection reset
25
+ by peer') and 5xx gateway responses with exponential backoff. Applied to
26
+ both the metadata POST and the S3 PUT uploads so a momentary blip on a
27
+ single record no longer fails the whole upload for that row.
28
+ """
29
+ retry = Retry(
30
+ total=total,
31
+ connect=total,
32
+ read=total,
33
+ status=total,
34
+ backoff_factor=backoff_factor,
35
+ status_forcelist=[502, 503, 504],
36
+ allowed_methods=["PUT", "POST"],
37
+ raise_on_status=False,
38
+ )
39
+ # Increased pool size to accommodate concurrent ThreadPoolExecutor uploads
40
+ adapter = HTTPAdapter(max_retries=retry, pool_connections=100, pool_maxsize=100)
41
+ session = requests.Session()
42
+ session.mount("https://", adapter)
43
+ session.mount("http://", adapter)
44
+ return session
45
+
17
46
  COA_ITEM_COUNTS = {
18
47
  "MADRS": 10,
19
48
  "YMRS": 10,
@@ -421,13 +450,15 @@ class UploadUtils:
421
450
  def validate_row(self):
422
451
  if not os.path.exists(self.row.file_path):
423
452
  return (False, "File path does not exist")
424
- if self.row.language not in LANGUAGE_CHOICES:
425
- return (False, f"Invalid language: {self.row.language}")
453
+ language = getattr(self.row, "language", None)
454
+ if language and language not in LANGUAGE_CHOICES:
455
+ return (False, f"Invalid language: {language}")
426
456
  return (True, None)
427
457
 
428
458
  def validate_processed_data_row(self):
429
- if self.row.language not in LANGUAGE_CHOICES:
430
- return (False, f"Invalid language: {self.row.language}")
459
+ language = getattr(self.row, "language", None)
460
+ if language and language not in LANGUAGE_CHOICES:
461
+ return (False, f"Invalid language: {language}")
431
462
  return (True, None)
432
463
 
433
464
  def calculate_file_checksum(self, file_path: str) -> str:
@@ -493,7 +524,10 @@ class UploadUtils:
493
524
  self, api_key: str, url: str, headers: Dict[str, str], payload: Dict[str, Any]
494
525
  ) -> Dict[str, Any]:
495
526
  try:
496
- response = requests.post(url, headers=headers, json=payload)
527
+ session = build_retry_session()
528
+ response = session.post(
529
+ url, headers=headers, json=payload, timeout=(10, 300)
530
+ )
497
531
  res_json = response.json()
498
532
 
499
533
  except Exception as ex:
@@ -14,18 +14,19 @@ class WillisapiClient:
14
14
  def __init__(self, *args, **kwargs) -> None:
15
15
  self.client_version = get_client_version()
16
16
  self.api_version = math.floor(self.client_version)
17
- self.api_uri = "api.brooklyn.health"
17
+ self.api_uri = "api.willis.health"
18
18
  self.env = kwargs["env"] if "env" in kwargs else None
19
19
 
20
- def get_base_url(self):
20
+ def get_api_host(self):
21
21
  if self.env:
22
- return f"https://{self.env}-{self.api_uri}/v{self.api_version}/"
23
- return f"https://{self.api_uri}/v{self.api_version}/"
22
+ return f"api.{self.env}.willis.health"
23
+ return self.api_uri
24
+
25
+ def get_base_url(self):
26
+ return f"https://{self.get_api_host()}/v{self.api_version}/"
24
27
 
25
28
  def get_base_v2_url(self):
26
- if self.env:
27
- return f"https://{self.env}-{self.api_uri}/api/v2/"
28
- return f"https://{self.api_uri}/api/v2/"
29
+ return f"https://{self.get_api_host()}/api/v2/"
29
30
 
30
31
  def get_diarize_remaining_calls_url(self):
31
32
  return self.get_base_url() + "willis-diarize-call-remaining"
@@ -35,9 +36,15 @@ class WillisapiClient:
35
36
 
36
37
  def get_upload_url(self):
37
38
  return self.get_base_v2_url() + "metadata/upload"
38
-
39
+
39
40
  def get_processed_upload_url(self):
40
41
  return self.get_base_v2_url() + "metadata/processed-data/upload"
41
42
 
43
+ def get_csv_archive_url(self):
44
+ return self.get_base_v2_url() + "metadata/csv-archive"
45
+
46
+ def get_csv_archive_finalize_url(self, record_id):
47
+ return self.get_csv_archive_url() + f"/{record_id}"
48
+
42
49
  def get_headers(self):
43
50
  return {"Content-Type": "application/json", "Accept": "application/json"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: willisapi-client
3
- Version: 1.9.6
3
+ Version: 1.9.8
4
4
  Summary: A Python client for willisapi
5
5
  Home-page: https://github.com/bklynhlth/willsiapi_client
6
6
  Author: bklynhlth
@@ -26,6 +26,7 @@ willisapi_client/services/diarize/diarize_utils.py
26
26
  willisapi_client/services/diarize/willisdiarize.py
27
27
  willisapi_client/services/diarize/willisdiarize_call_remaining.py
28
28
  willisapi_client/services/metadata/__init__.py
29
+ willisapi_client/services/metadata/archive.py
29
30
  willisapi_client/services/metadata/language_choices.py
30
31
  willisapi_client/services/metadata/upload.py
31
32
  willisapi_client/services/metadata/utils.py
@@ -1,86 +0,0 @@
1
- # https://docs.aws.amazon.com/transcribe/latest/dg/supported-languages.html
2
- Afrikaans = "af-ZA"
3
- Arabic_gulf = "ar-AE"
4
- Arabic_modern_standard = "ar-SA"
5
- Chinese_simplified = "zh-CN"
6
- Chinese_traditional = "zh-TW"
7
- Danish = "da-DK"
8
- Dutch = "nl-NL"
9
- English_aus = "en-AU"
10
- English_birtish = "en-GB"
11
- English_ind = "en-IN"
12
- English_irish = "en-IE"
13
- English_new_zealand = "en-NZ"
14
- English_scottish = "en-AB"
15
- English_south_african = "en-ZA"
16
- English_us = "en-US"
17
- English_welsh = "en-WL"
18
- French = "fr-FR"
19
- French_canadian = "fr-CA"
20
- Farsi = "fa-IR"
21
- German = "de-DE"
22
- German_swiss = "de-CH"
23
- Hebrew = "he-IL"
24
- Hindi_ind = "hi-IN"
25
- Indonesian = "id-ID"
26
- Italian = "it-IT"
27
- Japanese = "ja-JP"
28
- Korean = "ko-KR"
29
- Malay = "ms-MY"
30
- Portuguese = "pt-PT"
31
- Portuguese_brazilian = "pt-BR"
32
- Russian = "ru-RU"
33
- Spanish = "es-ES"
34
- Spanish_us = "es-US"
35
- Swedish = "sv-SE"
36
- Tamil = "ta-IN"
37
- Telugu = "te-IN"
38
- Thai = "th-TH"
39
- Turkish = "tr-TR"
40
- Vietnamese = "vi-VN"
41
-
42
- LANGUAGE_CHOICES = [
43
- Afrikaans,
44
- Arabic_gulf,
45
- Arabic_modern_standard,
46
- Chinese_simplified,
47
- Chinese_traditional,
48
- Danish,
49
- Dutch,
50
- English_aus,
51
- English_birtish,
52
- English_ind,
53
- English_irish,
54
- English_new_zealand,
55
- English_scottish,
56
- English_south_african,
57
- English_us,
58
- English_welsh,
59
- French,
60
- French_canadian,
61
- Farsi,
62
- German,
63
- German_swiss,
64
- Hebrew,
65
- Hindi_ind,
66
- Indonesian,
67
- Italian,
68
- Japanese,
69
- Korean,
70
- Malay,
71
- Portuguese,
72
- Portuguese_brazilian,
73
- Russian,
74
- Spanish,
75
- Spanish_us,
76
- Swedish,
77
- Tamil,
78
- Telugu,
79
- Thai,
80
- Turkish,
81
- Vietnamese,
82
- ]
83
- SEX_CHOICES = [
84
- ("Male", "M"),
85
- ("Female", "F"),
86
- ]