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.
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/PKG-INFO +1 -1
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/__version__.py +1 -1
- willisapi_client-1.9.8/willisapi_client/services/metadata/archive.py +83 -0
- willisapi_client-1.9.8/willisapi_client/services/metadata/language_choices.py +124 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/metadata/upload.py +110 -28
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/metadata/utils.py +39 -5
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/willisapi_client.py +15 -8
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client.egg-info/PKG-INFO +1 -1
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client.egg-info/SOURCES.txt +1 -0
- willisapi_client-1.9.6/willisapi_client/services/metadata/language_choices.py +0 -86
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/LICENSE +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/MANIFEST.in +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/README.md +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/RELEASE.md +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/requirements.txt +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/setup.cfg +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/setup.py +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/tests/test_diarize.py +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/tests/test_diarize_call_remaining.py +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/__init__.py +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/logging_setup.py +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/__init__.py +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/api.py +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/diarize/__init__.py +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/diarize/diarize_utils.py +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/diarize/willisdiarize.py +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/diarize/willisdiarize_call_remaining.py +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/exceptions.py +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/metadata/__init__.py +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/timer.py +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client.egg-info/dependency_links.txt +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client.egg-info/not-zip-safe +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client.egg-info/requires.txt +0 -0
- {willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Version details for willisapi_client"""
|
|
2
2
|
|
|
3
3
|
__client__ = "willisapi_client"
|
|
4
|
-
__latestVersion__ = "1.9.
|
|
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
|
+
]
|
{willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/metadata/upload.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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:
|
{willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/metadata/utils.py
RENAMED
|
@@ -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
|
-
|
|
425
|
-
|
|
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
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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.
|
|
17
|
+
self.api_uri = "api.willis.health"
|
|
18
18
|
self.env = kwargs["env"] if "env" in kwargs else None
|
|
19
19
|
|
|
20
|
-
def
|
|
20
|
+
def get_api_host(self):
|
|
21
21
|
if self.env:
|
|
22
|
-
return f"
|
|
23
|
-
return
|
|
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
|
-
|
|
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"}
|
|
@@ -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
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/diarize/__init__.py
RENAMED
|
File without changes
|
{willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/diarize/diarize_utils.py
RENAMED
|
File without changes
|
{willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/diarize/willisdiarize.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client/services/metadata/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{willisapi_client-1.9.6 → willisapi_client-1.9.8}/willisapi_client.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|