willisapi-client 1.9.1__tar.gz → 1.9.2__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.1 → willisapi_client-1.9.2}/PKG-INFO +2 -10
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/README.md +1 -9
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/__init__.py +2 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/__version__.py +2 -3
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/api.py +1 -0
- willisapi_client-1.9.2/willisapi_client/services/metadata/__init__.py +4 -0
- willisapi_client-1.9.2/willisapi_client/services/metadata/upload.py +83 -0
- willisapi_client-1.9.2/willisapi_client/services/metadata/utils.py +336 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/willisapi_client.py +8 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client.egg-info/PKG-INFO +2 -10
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client.egg-info/SOURCES.txt +3 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/LICENSE +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/MANIFEST.in +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/RELEASE.md +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/requirements.txt +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/setup.cfg +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/setup.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/tests/test_auth.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/tests/test_csv_validation.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/tests/test_diarize.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/tests/test_diarize_call_remaining.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/tests/test_download.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/tests/test_upload.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/logging_setup.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/__init__.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/auth/__init__.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/auth/auth_utils.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/auth/login_manager.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/diarize/__init__.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/diarize/diarize_utils.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/diarize/willisdiarize.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/diarize/willisdiarize_call_remaining.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/download/__init__.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/download/download_handler.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/download/download_utils.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/exceptions.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/upload/__init__.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/upload/csv_validation.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/upload/language_choices.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/upload/multipart_upload_handler.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/upload/upload_utils.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/upload/utils.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/timer.py +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client.egg-info/dependency_links.txt +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client.egg-info/not-zip-safe +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client.egg-info/requires.txt +0 -0
- {willisapi_client-1.9.1 → willisapi_client-1.9.2}/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.
|
|
3
|
+
Version: 1.9.2
|
|
4
4
|
Summary: A Python client for willisapi
|
|
5
5
|
Home-page: https://github.com/bklynhlth/willsiapi_client
|
|
6
6
|
Author: bklynhlth
|
|
@@ -67,21 +67,13 @@ key, expiration = willisapi.login(username, password)
|
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
```
|
|
70
|
-
summary = willisapi.
|
|
70
|
+
summary = willisapi.metadata_upload(key, 'data.csv')
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
For more information on how to organize the `data.csv`, visit the [Github Wiki](http://www.github.com/bklynhlth/willisapi_client/wiki).
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
#### Download
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
measures = willisapi.download(key, 'project_name')
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
|
|
85
77
|
If you run into trouble while using the client, please raise it in the [Issues](http://www.github.com/bklynhlth/willisapi_client/issues) tab.
|
|
86
78
|
|
|
87
79
|
***
|
|
@@ -55,21 +55,13 @@ key, expiration = willisapi.login(username, password)
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
```
|
|
58
|
-
summary = willisapi.
|
|
58
|
+
summary = willisapi.metadata_upload(key, 'data.csv')
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
For more information on how to organize the `data.csv`, visit the [Github Wiki](http://www.github.com/bklynhlth/willisapi_client/wiki).
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
#### Download
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
```
|
|
69
|
-
measures = willisapi.download(key, 'project_name')
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
|
|
73
65
|
If you run into trouble while using the client, please raise it in the [Issues](http://www.github.com/bklynhlth/willisapi_client/issues) tab.
|
|
74
66
|
|
|
75
67
|
***
|
|
@@ -7,6 +7,7 @@ from willisapi_client.services.api import (
|
|
|
7
7
|
download,
|
|
8
8
|
willis_diarize_call_remaining,
|
|
9
9
|
willis_diarize,
|
|
10
|
+
metadata_upload,
|
|
10
11
|
)
|
|
11
12
|
|
|
12
13
|
__all__ = [
|
|
@@ -15,4 +16,5 @@ __all__ = [
|
|
|
15
16
|
"download",
|
|
16
17
|
"willis_diarize_call_remaining",
|
|
17
18
|
"willis_diarize",
|
|
19
|
+
"metadata_upload",
|
|
18
20
|
]
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
"""Version details for willisapi_client
|
|
2
|
-
"""
|
|
1
|
+
"""Version details for willisapi_client"""
|
|
3
2
|
|
|
4
3
|
__client__ = "willisapi_client"
|
|
5
|
-
__latestVersion__ = "1.9.
|
|
4
|
+
__latestVersion__ = "1.9.2"
|
|
6
5
|
__url__ = "https://github.com/bklynhlth/willsiapi_client"
|
|
7
6
|
__short_description__ = "A Python client for willisapi"
|
|
8
7
|
__content_type__ = "text/markdown"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from tqdm import tqdm
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from willisapi_client.timer import measure
|
|
7
|
+
from willisapi_client.willisapi_client import WillisapiClient
|
|
8
|
+
from willisapi_client.logging_setup import logger as logger
|
|
9
|
+
from willisapi_client.services.metadata.utils import MetadataValidation, UploadUtils
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@measure
|
|
13
|
+
def metadata_upload(api_key: str, csv_path: str, **kwargs):
|
|
14
|
+
|
|
15
|
+
force_upload = kwargs.get("force_upload", False)
|
|
16
|
+
csv = MetadataValidation(csv_path=csv_path, force_upload=force_upload)
|
|
17
|
+
if csv.load_and_validate():
|
|
18
|
+
logger.info(f'{datetime.now().strftime("%H:%M:%S")}: csv check passed')
|
|
19
|
+
csv.create_final_csv()
|
|
20
|
+
|
|
21
|
+
wc = WillisapiClient(env=kwargs.get("env"))
|
|
22
|
+
url = wc.get_metadata_upload_url()
|
|
23
|
+
headers = wc.get_headers()
|
|
24
|
+
headers["Authorization"] = f"token {api_key}"
|
|
25
|
+
logger.info(f'{datetime.now().strftime("%H:%M:%S")}: beginning upload')
|
|
26
|
+
|
|
27
|
+
results = []
|
|
28
|
+
for index, row in tqdm(
|
|
29
|
+
csv.transformed_df.iterrows(), total=csv.transformed_df.shape[0]
|
|
30
|
+
):
|
|
31
|
+
u = UploadUtils(row)
|
|
32
|
+
valid, err = u.validate_row()
|
|
33
|
+
result_row = row.to_dict()
|
|
34
|
+
if valid:
|
|
35
|
+
payload = u.generate_payload()
|
|
36
|
+
res = u.post(api_key, url, headers, payload)
|
|
37
|
+
if res.get("upload_status") == "Success":
|
|
38
|
+
result_row["upload_status"] = "Success"
|
|
39
|
+
result_row["error"] = None
|
|
40
|
+
|
|
41
|
+
# Handle S3 upload if presigned URL is provided
|
|
42
|
+
presigned = res.get("response", {}).get("presigned")
|
|
43
|
+
if presigned:
|
|
44
|
+
try:
|
|
45
|
+
with open(row.file_path, "rb") as f:
|
|
46
|
+
files = {"file": f}
|
|
47
|
+
response = requests.put(
|
|
48
|
+
presigned,
|
|
49
|
+
data=files["file"],
|
|
50
|
+
headers={"Content-Type": "audio/ogg"},
|
|
51
|
+
)
|
|
52
|
+
if response.status_code == 200:
|
|
53
|
+
result_row["upload_status"] = "Success"
|
|
54
|
+
else:
|
|
55
|
+
result_row["upload_status"] = "Failed"
|
|
56
|
+
result_row["error"] = (
|
|
57
|
+
f"S3 upload failed with status code {response.status_code}"
|
|
58
|
+
)
|
|
59
|
+
except Exception as ex:
|
|
60
|
+
result_row["upload_status"] = "Failed"
|
|
61
|
+
result_row["error"] = str(ex)
|
|
62
|
+
else:
|
|
63
|
+
result_row["upload_status"] = "Failed"
|
|
64
|
+
result_row["error"] = (
|
|
65
|
+
"Collect recording upload URL not received"
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
result_row["upload_status"] = "Failed"
|
|
69
|
+
result_row["error"] = res.get("error")
|
|
70
|
+
else:
|
|
71
|
+
result_row["upload_status"] = "Failed"
|
|
72
|
+
result_row["error"] = f"{err}"
|
|
73
|
+
results.append(result_row)
|
|
74
|
+
|
|
75
|
+
results_df = pd.DataFrame(results)
|
|
76
|
+
results_df.to_csv(
|
|
77
|
+
f"metadata_upload_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
|
|
78
|
+
index=False,
|
|
79
|
+
)
|
|
80
|
+
return results_df
|
|
81
|
+
else:
|
|
82
|
+
logger.error(f'{datetime.now().strftime("%H:%M:%S")}: csv check failed')
|
|
83
|
+
return None
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from typing import List, Dict, Any, Tuple
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import requests
|
|
6
|
+
from willisapi_client.services.upload.language_choices import (
|
|
7
|
+
LANGUAGE_CHOICES,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MetadataValidation:
|
|
12
|
+
REQUIRED_COLUMNS = [
|
|
13
|
+
"study_id",
|
|
14
|
+
"site_id",
|
|
15
|
+
"rater_email",
|
|
16
|
+
"participant_id",
|
|
17
|
+
"visit_name",
|
|
18
|
+
"visit_order",
|
|
19
|
+
"coa_name",
|
|
20
|
+
"coa_item_number",
|
|
21
|
+
"coa_item_value",
|
|
22
|
+
"file_path",
|
|
23
|
+
"time_collected",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
OPTIONAL_COLUMNS = ["age", "sex", "race", "language"]
|
|
27
|
+
|
|
28
|
+
ALLOWED_COA_NAMES = ["MADRS", "YMRS", "PHQ-9", "GAD-7"]
|
|
29
|
+
|
|
30
|
+
COA_ITEM_COUNTS = {"MADRS": 10, "YMRS": 10, "PHQ-9": 9, "GAD-7": 7}
|
|
31
|
+
|
|
32
|
+
def __init__(self, csv_path: str, force_upload: bool = False):
|
|
33
|
+
"""
|
|
34
|
+
Initialize validator with CSV file path.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
csv_path: Path to the CSV file
|
|
38
|
+
"""
|
|
39
|
+
self.csv_path = csv_path
|
|
40
|
+
self.df = None
|
|
41
|
+
self.errors = []
|
|
42
|
+
self.transformed_df = None
|
|
43
|
+
self.force_upload = force_upload
|
|
44
|
+
|
|
45
|
+
def validate_columns(self) -> bool:
|
|
46
|
+
"""
|
|
47
|
+
Validate that all required columns are present.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
bool: True if validation passes, False otherwise
|
|
51
|
+
"""
|
|
52
|
+
missing_cols = [
|
|
53
|
+
col for col in self.REQUIRED_COLUMNS if col not in self.df.columns
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
if missing_cols:
|
|
57
|
+
self.errors.append(f"Missing required columns: {', '.join(missing_cols)}")
|
|
58
|
+
return False
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
def validate_data_types(self) -> bool:
|
|
62
|
+
"""
|
|
63
|
+
Validate data types for key columns.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
bool: True if validation passes, False otherwise
|
|
67
|
+
"""
|
|
68
|
+
valid = True
|
|
69
|
+
|
|
70
|
+
# Check visit_order is numeric
|
|
71
|
+
if not pd.api.types.is_numeric_dtype(self.df["visit_order"]):
|
|
72
|
+
try:
|
|
73
|
+
self.df["visit_order"] = pd.to_numeric(self.df["visit_order"])
|
|
74
|
+
except:
|
|
75
|
+
self.errors.append("visit_order must be numeric")
|
|
76
|
+
valid = False
|
|
77
|
+
|
|
78
|
+
# Check age is numeric (if present)
|
|
79
|
+
if "age" in self.df.columns and not self.df["age"].isna().all():
|
|
80
|
+
if not pd.api.types.is_numeric_dtype(self.df["age"]):
|
|
81
|
+
try:
|
|
82
|
+
self.df["age"] = pd.to_numeric(self.df["age"], errors="coerce")
|
|
83
|
+
except:
|
|
84
|
+
self.errors.append("age must be numeric")
|
|
85
|
+
valid = False
|
|
86
|
+
|
|
87
|
+
# Check coa_item_number is numeric
|
|
88
|
+
if not pd.api.types.is_numeric_dtype(self.df["coa_item_number"]):
|
|
89
|
+
try:
|
|
90
|
+
self.df["coa_item_number"] = pd.to_numeric(self.df["coa_item_number"])
|
|
91
|
+
except:
|
|
92
|
+
self.errors.append("coa_item_number must be numeric")
|
|
93
|
+
valid = False
|
|
94
|
+
|
|
95
|
+
# Check coa_item_value is numeric
|
|
96
|
+
if not pd.api.types.is_numeric_dtype(self.df["coa_item_value"]):
|
|
97
|
+
try:
|
|
98
|
+
self.df["coa_item_value"] = pd.to_numeric(
|
|
99
|
+
self.df["coa_item_value"], errors="coerce"
|
|
100
|
+
)
|
|
101
|
+
except:
|
|
102
|
+
self.errors.append("coa_item_value must be numeric")
|
|
103
|
+
valid = False
|
|
104
|
+
|
|
105
|
+
return valid
|
|
106
|
+
|
|
107
|
+
def validate_email(self) -> bool:
|
|
108
|
+
"""
|
|
109
|
+
Basic email validation for rater_email column.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
bool: True if validation passes, False otherwise
|
|
113
|
+
"""
|
|
114
|
+
invalid_emails = self.df[~self.df["rater_email"].str.contains("@", na=False)]
|
|
115
|
+
|
|
116
|
+
if not invalid_emails.empty:
|
|
117
|
+
self.errors.append(
|
|
118
|
+
f"Invalid email addresses found in rows: {invalid_emails.index.tolist()}"
|
|
119
|
+
)
|
|
120
|
+
return False
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
def validate_coa_names(self) -> bool:
|
|
124
|
+
"""
|
|
125
|
+
Validate that coa_name values are in the allowed list.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
bool: True if validation passes, False otherwise
|
|
129
|
+
"""
|
|
130
|
+
# Convert to lowercase for comparison
|
|
131
|
+
self.df["coa_name"] = self.df["coa_name"].str.strip()
|
|
132
|
+
|
|
133
|
+
invalid_coa = self.df[~self.df["coa_name"].isin(self.ALLOWED_COA_NAMES)]
|
|
134
|
+
|
|
135
|
+
if not invalid_coa.empty:
|
|
136
|
+
invalid_values = invalid_coa["coa_name"].unique().tolist()
|
|
137
|
+
self.errors.append(
|
|
138
|
+
f"Invalid coa_name values found: {invalid_values}. "
|
|
139
|
+
f"Allowed values are: {', '.join(self.ALLOWED_COA_NAMES)}"
|
|
140
|
+
)
|
|
141
|
+
return False
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
def load_and_validate(self) -> bool:
|
|
145
|
+
"""
|
|
146
|
+
Load CSV and run all validations.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
bool: True if all validations pass, False otherwise
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
self.df = pd.read_csv(self.csv_path)
|
|
153
|
+
except Exception as e:
|
|
154
|
+
self.errors.append(f"Failed to load CSV: {str(e)}")
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
# Strip whitespace from column names
|
|
158
|
+
self.df.columns = self.df.columns.str.strip()
|
|
159
|
+
|
|
160
|
+
validations = [
|
|
161
|
+
self.validate_columns(),
|
|
162
|
+
self.validate_data_types(),
|
|
163
|
+
self.validate_email(),
|
|
164
|
+
self.validate_coa_names(),
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
return all(validations)
|
|
168
|
+
|
|
169
|
+
def transform_to_serializer_format(self) -> List[Dict[str, Any]]:
|
|
170
|
+
"""
|
|
171
|
+
Transform CSV data to match BulkUploadSerializer format.
|
|
172
|
+
Groups rows by unique assessment (study, site, participant, visit, coa)
|
|
173
|
+
and creates actual_scores JSON structure.
|
|
174
|
+
Missing items are filled with score of 0.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
List of dictionaries matching the serializer format
|
|
178
|
+
"""
|
|
179
|
+
if self.df is None:
|
|
180
|
+
raise ValueError("CSV not loaded. Call load_and_validate() first.")
|
|
181
|
+
|
|
182
|
+
# Group by unique assessment identifiers
|
|
183
|
+
grouping_cols = [
|
|
184
|
+
"study_id",
|
|
185
|
+
"site_id",
|
|
186
|
+
"rater_email",
|
|
187
|
+
"participant_id",
|
|
188
|
+
"visit_name",
|
|
189
|
+
"visit_order",
|
|
190
|
+
"coa_name",
|
|
191
|
+
"file_path",
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
# Add optional columns that are present
|
|
195
|
+
optional_present = [
|
|
196
|
+
col for col in self.OPTIONAL_COLUMNS if col in self.df.columns
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
results = []
|
|
200
|
+
|
|
201
|
+
for group_key, group_df in self.df.groupby(grouping_cols, dropna=False):
|
|
202
|
+
# Create base record
|
|
203
|
+
record = {}
|
|
204
|
+
for i, col in enumerate(grouping_cols):
|
|
205
|
+
record[col] = group_key[i]
|
|
206
|
+
|
|
207
|
+
# Add optional fields from first row of group
|
|
208
|
+
first_row = group_df.iloc[0]
|
|
209
|
+
for col in optional_present:
|
|
210
|
+
if col not in ["time_collected"]: # Exclude metadata
|
|
211
|
+
value = first_row[col]
|
|
212
|
+
if pd.notna(value):
|
|
213
|
+
record[col] = value
|
|
214
|
+
|
|
215
|
+
# Get expected item count for this COA
|
|
216
|
+
coa_name = record["coa_name"]
|
|
217
|
+
expected_items = self.COA_ITEM_COUNTS.get(coa_name, 10)
|
|
218
|
+
|
|
219
|
+
# Create a dictionary to store item scores
|
|
220
|
+
item_scores = {}
|
|
221
|
+
|
|
222
|
+
for _, row in group_df.iterrows():
|
|
223
|
+
item_num = int(row["coa_item_number"])
|
|
224
|
+
item_score = row["coa_item_value"]
|
|
225
|
+
|
|
226
|
+
# Convert to int if not null, otherwise use 0 as default
|
|
227
|
+
if pd.notna(item_score):
|
|
228
|
+
item_scores[item_num] = int(item_score)
|
|
229
|
+
else:
|
|
230
|
+
item_scores[item_num] = None
|
|
231
|
+
|
|
232
|
+
# Build sections with all expected items (fill missing with 0)
|
|
233
|
+
sections = []
|
|
234
|
+
total_score = 0
|
|
235
|
+
|
|
236
|
+
for item_num in range(1, expected_items + 1):
|
|
237
|
+
item_score = item_scores.get(item_num, None) # Default to 0 if missing
|
|
238
|
+
total_score = total_score + (
|
|
239
|
+
item_score if item_score is not None else 0
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
section = {
|
|
243
|
+
"section_id": f"s{item_num:02d}",
|
|
244
|
+
"section_notes": None,
|
|
245
|
+
"items": [
|
|
246
|
+
{"item_id": f"i{item_num:02d}", "item_score": item_score}
|
|
247
|
+
],
|
|
248
|
+
}
|
|
249
|
+
sections.append(section)
|
|
250
|
+
|
|
251
|
+
actual_scores = {
|
|
252
|
+
"sections": sections,
|
|
253
|
+
"total_score": total_score,
|
|
254
|
+
"total_severity": None,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
record["actual_scores"] = actual_scores
|
|
258
|
+
results.append(record)
|
|
259
|
+
|
|
260
|
+
return results
|
|
261
|
+
|
|
262
|
+
def create_final_csv(self) -> pd.DataFrame:
|
|
263
|
+
"""
|
|
264
|
+
Create a final grouped CSV with actual_scores as JSON string.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
output_path: Path where the final CSV should be saved
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
DataFrame containing the grouped data
|
|
271
|
+
"""
|
|
272
|
+
transformed_data = self.transform_to_serializer_format()
|
|
273
|
+
|
|
274
|
+
# Convert actual_scores dict to JSON string for CSV storage
|
|
275
|
+
for record in transformed_data:
|
|
276
|
+
record["actual_scores"] = json.dumps(record["actual_scores"])
|
|
277
|
+
record["force_upload"] = self.force_upload
|
|
278
|
+
|
|
279
|
+
self.transformed_df = pd.DataFrame(transformed_data)
|
|
280
|
+
return self.transformed_df
|
|
281
|
+
|
|
282
|
+
def get_errors(self) -> List[str]:
|
|
283
|
+
"""
|
|
284
|
+
Get list of validation errors.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
List of error messages
|
|
288
|
+
"""
|
|
289
|
+
return self.errors
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class UploadUtils:
|
|
293
|
+
def __init__(self, row):
|
|
294
|
+
self.row = row
|
|
295
|
+
|
|
296
|
+
def validate_row(self) -> Tuple[bool, str | None]:
|
|
297
|
+
if not os.path.exists(self.row.file_path):
|
|
298
|
+
return (False, "File path does not exist")
|
|
299
|
+
if self.row.language not in LANGUAGE_CHOICES:
|
|
300
|
+
return (False, f"Invalid language: {self.row.language}")
|
|
301
|
+
return (True, None)
|
|
302
|
+
|
|
303
|
+
def generate_payload(self) -> Dict[str, Any]:
|
|
304
|
+
payload = {
|
|
305
|
+
"study_id": self.row.study_id,
|
|
306
|
+
"site_id": self.row.site_id,
|
|
307
|
+
"rater_email": self.row.rater_email,
|
|
308
|
+
"participant_id": self.row.participant_id,
|
|
309
|
+
"age": self.row.age,
|
|
310
|
+
"sex": self.row.sex,
|
|
311
|
+
"race": self.row.race,
|
|
312
|
+
"language": self.row.language,
|
|
313
|
+
"visit_name": self.row.visit_name,
|
|
314
|
+
"visit_order": int(self.row.visit_order),
|
|
315
|
+
"coa_name": self.row.coa_name,
|
|
316
|
+
"file_path": self.row.file_path,
|
|
317
|
+
"filename": os.path.basename(self.row.file_path),
|
|
318
|
+
"force_upload": self.row.force_upload,
|
|
319
|
+
"actual_scores": json.loads(self.row.actual_scores),
|
|
320
|
+
}
|
|
321
|
+
return payload
|
|
322
|
+
|
|
323
|
+
def post(
|
|
324
|
+
self, api_key: str, url: str, headers: Dict[str, str], payload: Dict[str, Any]
|
|
325
|
+
) -> Dict[str, Any]:
|
|
326
|
+
try:
|
|
327
|
+
response = requests.post(url, headers=headers, json=payload)
|
|
328
|
+
res_json = response.json()
|
|
329
|
+
|
|
330
|
+
except Exception as ex:
|
|
331
|
+
return {"upload_status": "Failed", "error": str(ex), "response": None}
|
|
332
|
+
else:
|
|
333
|
+
if response.status_code not in [200, 201]:
|
|
334
|
+
return {"upload_status": "Failed", "error": res_json, "response": None}
|
|
335
|
+
else:
|
|
336
|
+
return {"upload_status": "Success", "response": res_json, "error": None}
|
|
@@ -22,6 +22,11 @@ class WillisapiClient:
|
|
|
22
22
|
return f"https://{self.env}-{self.api_uri}/v{self.api_version}/"
|
|
23
23
|
return f"https://{self.api_uri}/v{self.api_version}/"
|
|
24
24
|
|
|
25
|
+
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
|
+
|
|
25
30
|
def get_login_url(self):
|
|
26
31
|
return self.get_base_url() + "login"
|
|
27
32
|
|
|
@@ -37,5 +42,8 @@ class WillisapiClient:
|
|
|
37
42
|
def get_diarize(self):
|
|
38
43
|
return self.get_base_url() + "diarize"
|
|
39
44
|
|
|
45
|
+
def get_metadata_upload_url(self):
|
|
46
|
+
return self.get_base_v2_url() + "metadata/upload"
|
|
47
|
+
|
|
40
48
|
def get_headers(self):
|
|
41
49
|
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.
|
|
3
|
+
Version: 1.9.2
|
|
4
4
|
Summary: A Python client for willisapi
|
|
5
5
|
Home-page: https://github.com/bklynhlth/willsiapi_client
|
|
6
6
|
Author: bklynhlth
|
|
@@ -67,21 +67,13 @@ key, expiration = willisapi.login(username, password)
|
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
```
|
|
70
|
-
summary = willisapi.
|
|
70
|
+
summary = willisapi.metadata_upload(key, 'data.csv')
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
For more information on how to organize the `data.csv`, visit the [Github Wiki](http://www.github.com/bklynhlth/willisapi_client/wiki).
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
#### Download
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
measures = willisapi.download(key, 'project_name')
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
|
|
85
77
|
If you run into trouble while using the client, please raise it in the [Issues](http://www.github.com/bklynhlth/willisapi_client/issues) tab.
|
|
86
78
|
|
|
87
79
|
***
|
|
@@ -35,6 +35,9 @@ willisapi_client/services/diarize/willisdiarize_call_remaining.py
|
|
|
35
35
|
willisapi_client/services/download/__init__.py
|
|
36
36
|
willisapi_client/services/download/download_handler.py
|
|
37
37
|
willisapi_client/services/download/download_utils.py
|
|
38
|
+
willisapi_client/services/metadata/__init__.py
|
|
39
|
+
willisapi_client/services/metadata/upload.py
|
|
40
|
+
willisapi_client/services/metadata/utils.py
|
|
38
41
|
willisapi_client/services/upload/__init__.py
|
|
39
42
|
willisapi_client/services/upload/csv_validation.py
|
|
40
43
|
willisapi_client/services/upload/language_choices.py
|
|
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
|
|
File without changes
|
{willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/auth/__init__.py
RENAMED
|
File without changes
|
{willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/auth/auth_utils.py
RENAMED
|
File without changes
|
{willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/auth/login_manager.py
RENAMED
|
File without changes
|
{willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/diarize/__init__.py
RENAMED
|
File without changes
|
{willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/diarize/diarize_utils.py
RENAMED
|
File without changes
|
{willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/diarize/willisdiarize.py
RENAMED
|
File without changes
|
|
File without changes
|
{willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/download/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/upload/__init__.py
RENAMED
|
File without changes
|
{willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/upload/csv_validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client/services/upload/upload_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{willisapi_client-1.9.1 → willisapi_client-1.9.2}/willisapi_client.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|