aind-data-transfer-service 1.16.0__py3-none-any.whl → 1.17.0__py3-none-any.whl
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.
Potentially problematic release.
This version of aind-data-transfer-service might be problematic. Click here for more details.
- aind_data_transfer_service/__init__.py +1 -1
- aind_data_transfer_service/models/internal.py +35 -10
- aind_data_transfer_service/server.py +120 -7
- aind_data_transfer_service/templates/admin.html +11 -2
- aind_data_transfer_service/templates/job_params.html +242 -32
- {aind_data_transfer_service-1.16.0.dist-info → aind_data_transfer_service-1.17.0.dist-info}/METADATA +2 -2
- {aind_data_transfer_service-1.16.0.dist-info → aind_data_transfer_service-1.17.0.dist-info}/RECORD +10 -10
- {aind_data_transfer_service-1.16.0.dist-info → aind_data_transfer_service-1.17.0.dist-info}/WHEEL +0 -0
- {aind_data_transfer_service-1.16.0.dist-info → aind_data_transfer_service-1.17.0.dist-info}/licenses/LICENSE +0 -0
- {aind_data_transfer_service-1.16.0.dist-info → aind_data_transfer_service-1.17.0.dist-info}/top_level.txt +0 -0
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import ast
|
|
4
4
|
import os
|
|
5
5
|
from datetime import datetime, timedelta, timezone
|
|
6
|
-
from typing import List, Optional, Union
|
|
6
|
+
from typing import ClassVar, List, Optional, Union
|
|
7
7
|
|
|
8
|
+
from aind_data_schema_models.modalities import Modality
|
|
8
9
|
from mypy_boto3_ssm.type_defs import ParameterMetadataTypeDef
|
|
9
10
|
from pydantic import AwareDatetime, BaseModel, Field, field_validator
|
|
10
11
|
from starlette.datastructures import QueryParams
|
|
@@ -223,11 +224,30 @@ class JobTasks(BaseModel):
|
|
|
223
224
|
class JobParamInfo(BaseModel):
|
|
224
225
|
"""Model for job parameter info from AWS Parameter Store"""
|
|
225
226
|
|
|
227
|
+
_MODALITIES_LIST: ClassVar[list[str]] = list(
|
|
228
|
+
Modality.abbreviation_map.keys()
|
|
229
|
+
)
|
|
230
|
+
_MODALITY_TASKS: ClassVar[list[str]] = [
|
|
231
|
+
"modality_transformation_settings",
|
|
232
|
+
"codeocean_pipeline_settings",
|
|
233
|
+
]
|
|
234
|
+
|
|
226
235
|
name: Optional[str]
|
|
227
236
|
last_modified: Optional[datetime]
|
|
228
|
-
job_type: str
|
|
229
|
-
task_id: str
|
|
237
|
+
job_type: str = Field(..., pattern=r"^[^\s/]+$")
|
|
238
|
+
task_id: str = Field(..., pattern=r"^[^\s/]+$")
|
|
230
239
|
modality: Optional[str]
|
|
240
|
+
version: Optional[str] = Field(..., pattern=r"^(v1|v2)?$")
|
|
241
|
+
|
|
242
|
+
@field_validator("modality", mode="after")
|
|
243
|
+
def validate_modality(cls, v):
|
|
244
|
+
"""Check that modality is one of aind-data-schema modalities"""
|
|
245
|
+
if v is not None and v not in JobParamInfo._MODALITIES_LIST:
|
|
246
|
+
raise ValueError(
|
|
247
|
+
"Invalid modality: modality must be one of "
|
|
248
|
+
f"{JobParamInfo._MODALITIES_LIST}"
|
|
249
|
+
)
|
|
250
|
+
return v
|
|
231
251
|
|
|
232
252
|
@classmethod
|
|
233
253
|
def from_aws_describe_parameter(
|
|
@@ -236,6 +256,7 @@ class JobParamInfo(BaseModel):
|
|
|
236
256
|
job_type: str,
|
|
237
257
|
task_id: str,
|
|
238
258
|
modality: Optional[str],
|
|
259
|
+
version: Optional[str],
|
|
239
260
|
):
|
|
240
261
|
"""Map the parameter to the model"""
|
|
241
262
|
return cls(
|
|
@@ -244,13 +265,14 @@ class JobParamInfo(BaseModel):
|
|
|
244
265
|
job_type=job_type,
|
|
245
266
|
task_id=task_id,
|
|
246
267
|
modality=modality,
|
|
268
|
+
version=version,
|
|
247
269
|
)
|
|
248
270
|
|
|
249
271
|
@staticmethod
|
|
250
272
|
def get_parameter_prefix(version: Optional[str] = None) -> str:
|
|
251
273
|
"""Get the prefix for job_type parameters"""
|
|
252
274
|
prefix = os.getenv("AIND_AIRFLOW_PARAM_PREFIX")
|
|
253
|
-
if version is None:
|
|
275
|
+
if version is None or version == "v1":
|
|
254
276
|
return prefix
|
|
255
277
|
return f"{prefix}/{version}"
|
|
256
278
|
|
|
@@ -262,16 +284,19 @@ class JobParamInfo(BaseModel):
|
|
|
262
284
|
"(?P<job_type>[^/]+)/tasks/(?P<task_id>[^/]+)"
|
|
263
285
|
"(?:/(?P<modality>[^/]+))?"
|
|
264
286
|
)
|
|
265
|
-
if version is None:
|
|
287
|
+
if version is None or version == "v1":
|
|
266
288
|
return f"{prefix}/{regex}"
|
|
267
289
|
return f"{prefix}/{version}/{regex}"
|
|
268
290
|
|
|
269
291
|
@staticmethod
|
|
270
292
|
def get_parameter_name(
|
|
271
|
-
job_type: str,
|
|
293
|
+
job_type: str,
|
|
294
|
+
task_id: str,
|
|
295
|
+
modality: Optional[str],
|
|
296
|
+
version: Optional[str] = None,
|
|
272
297
|
) -> str:
|
|
273
298
|
"""Create the parameter name from job_type and task_id"""
|
|
274
|
-
prefix =
|
|
275
|
-
if
|
|
276
|
-
return f"{prefix}/{job_type}/tasks/{task_id}"
|
|
277
|
-
return f"{prefix}/{
|
|
299
|
+
prefix = JobParamInfo.get_parameter_prefix(version)
|
|
300
|
+
if modality:
|
|
301
|
+
return f"{prefix}/{job_type}/tasks/{task_id}/{modality}"
|
|
302
|
+
return f"{prefix}/{job_type}/tasks/{task_id}"
|
|
@@ -7,7 +7,7 @@ import os
|
|
|
7
7
|
import re
|
|
8
8
|
from asyncio import gather, sleep
|
|
9
9
|
from pathlib import PurePosixPath
|
|
10
|
-
from typing import List, Optional, Union
|
|
10
|
+
from typing import Any, List, Optional, Union
|
|
11
11
|
|
|
12
12
|
import boto3
|
|
13
13
|
import requests
|
|
@@ -156,6 +156,7 @@ def get_parameter_infos(version: Optional[str] = None) -> List[JobParamInfo]:
|
|
|
156
156
|
job_type=match.group("job_type"),
|
|
157
157
|
task_id=match.group("task_id"),
|
|
158
158
|
modality=match.group("modality"),
|
|
159
|
+
version=version,
|
|
159
160
|
)
|
|
160
161
|
params.append(param_info)
|
|
161
162
|
else:
|
|
@@ -173,6 +174,19 @@ def get_parameter_value(param_name: str) -> dict:
|
|
|
173
174
|
return param_value
|
|
174
175
|
|
|
175
176
|
|
|
177
|
+
def put_parameter_value(param_name: str, param_value: dict) -> Any:
|
|
178
|
+
"""Set a parameter value in AWS param store based on parameter name"""
|
|
179
|
+
param_value_str = json.dumps(param_value)
|
|
180
|
+
ssm_client = boto3.client("ssm")
|
|
181
|
+
result = ssm_client.put_parameter(
|
|
182
|
+
Name=param_name,
|
|
183
|
+
Value=param_value_str,
|
|
184
|
+
Type="String",
|
|
185
|
+
Overwrite=True,
|
|
186
|
+
)
|
|
187
|
+
return result
|
|
188
|
+
|
|
189
|
+
|
|
176
190
|
async def get_airflow_jobs(
|
|
177
191
|
params: AirflowDagRunsRequestParameters, get_confs: bool = False
|
|
178
192
|
) -> tuple[int, Union[List[JobStatus], List[dict]]]:
|
|
@@ -1001,16 +1015,20 @@ async def jobs(request: Request):
|
|
|
1001
1015
|
|
|
1002
1016
|
async def job_params(request: Request):
|
|
1003
1017
|
"""Get Job Parameters page"""
|
|
1018
|
+
user = request.session.get("user")
|
|
1004
1019
|
return templates.TemplateResponse(
|
|
1005
1020
|
request=request,
|
|
1006
1021
|
name="job_params.html",
|
|
1007
1022
|
context=(
|
|
1008
1023
|
{
|
|
1024
|
+
"user_signed_in": user is not None,
|
|
1009
1025
|
"project_names_url": os.getenv(
|
|
1010
1026
|
"AIND_METADATA_SERVICE_PROJECT_NAMES_URL"
|
|
1011
1027
|
),
|
|
1012
1028
|
"versions": ["v1", "v2"],
|
|
1013
1029
|
"default_version": "v1",
|
|
1030
|
+
"modalities": JobParamInfo._MODALITIES_LIST,
|
|
1031
|
+
"modality_tasks": JobParamInfo._MODALITY_TASKS,
|
|
1014
1032
|
}
|
|
1015
1033
|
),
|
|
1016
1034
|
)
|
|
@@ -1074,7 +1092,10 @@ def get_parameter_v2(request: Request):
|
|
|
1074
1092
|
# path params are auto validated
|
|
1075
1093
|
job_type = request.path_params.get("job_type")
|
|
1076
1094
|
task_id = request.path_params.get("task_id")
|
|
1077
|
-
|
|
1095
|
+
modality = request.path_params.get("modality")
|
|
1096
|
+
param_name = JobParamInfo.get_parameter_name(
|
|
1097
|
+
job_type=job_type, task_id=task_id, modality=modality, version="v2"
|
|
1098
|
+
)
|
|
1078
1099
|
try:
|
|
1079
1100
|
param_value = get_parameter_value(param_name)
|
|
1080
1101
|
return JSONResponse(
|
|
@@ -1095,12 +1116,79 @@ def get_parameter_v2(request: Request):
|
|
|
1095
1116
|
)
|
|
1096
1117
|
|
|
1097
1118
|
|
|
1119
|
+
async def put_parameter(request: Request):
|
|
1120
|
+
"""Set v1/v2 parameter in AWS param store based on job_type and task_id"""
|
|
1121
|
+
# User must be signed in
|
|
1122
|
+
user = request.session.get("user")
|
|
1123
|
+
if not user:
|
|
1124
|
+
return JSONResponse(
|
|
1125
|
+
content={
|
|
1126
|
+
"message": "User not authenticated",
|
|
1127
|
+
"data": {"error": "User not authenticated"},
|
|
1128
|
+
},
|
|
1129
|
+
status_code=401,
|
|
1130
|
+
)
|
|
1131
|
+
try:
|
|
1132
|
+
# path params
|
|
1133
|
+
param_info = JobParamInfo(
|
|
1134
|
+
name=None,
|
|
1135
|
+
last_modified=None,
|
|
1136
|
+
job_type=request.path_params.get("job_type"),
|
|
1137
|
+
task_id=request.path_params.get("task_id"),
|
|
1138
|
+
modality=request.path_params.get("modality"),
|
|
1139
|
+
version=request.path_params.get("version"),
|
|
1140
|
+
)
|
|
1141
|
+
param_name = JobParamInfo.get_parameter_name(
|
|
1142
|
+
job_type=param_info.job_type,
|
|
1143
|
+
task_id=param_info.task_id,
|
|
1144
|
+
modality=param_info.modality,
|
|
1145
|
+
version=param_info.version,
|
|
1146
|
+
)
|
|
1147
|
+
# update param store
|
|
1148
|
+
logger.info(
|
|
1149
|
+
f"Received request from {user} to set parameter {param_name}"
|
|
1150
|
+
)
|
|
1151
|
+
param_value = await request.json()
|
|
1152
|
+
logger.info(f"Setting parameter {param_name} to {param_value}")
|
|
1153
|
+
result = put_parameter_value(
|
|
1154
|
+
param_name=param_name, param_value=param_value
|
|
1155
|
+
)
|
|
1156
|
+
logger.info(result)
|
|
1157
|
+
return JSONResponse(
|
|
1158
|
+
content={
|
|
1159
|
+
"message": f"Set parameter for {param_name}",
|
|
1160
|
+
"data": param_value,
|
|
1161
|
+
},
|
|
1162
|
+
status_code=200,
|
|
1163
|
+
)
|
|
1164
|
+
except ValidationError as error:
|
|
1165
|
+
return JSONResponse(
|
|
1166
|
+
content={
|
|
1167
|
+
"message": "Invalid parameter",
|
|
1168
|
+
"data": {"errors": json.loads(error.json())},
|
|
1169
|
+
},
|
|
1170
|
+
status_code=400,
|
|
1171
|
+
)
|
|
1172
|
+
except Exception as e:
|
|
1173
|
+
logger.exception(f"Error setting parameter {param_name}: {e}")
|
|
1174
|
+
return JSONResponse(
|
|
1175
|
+
content={
|
|
1176
|
+
"message": f"Error setting parameter {param_name}",
|
|
1177
|
+
"data": {"error": f"{e.__class__.__name__}{e.args}"},
|
|
1178
|
+
},
|
|
1179
|
+
status_code=500,
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1182
|
+
|
|
1098
1183
|
def get_parameter(request: Request):
|
|
1099
1184
|
"""Get parameter from AWS parameter store based on job_type and task_id"""
|
|
1100
1185
|
# path params are auto validated
|
|
1101
1186
|
job_type = request.path_params.get("job_type")
|
|
1102
1187
|
task_id = request.path_params.get("task_id")
|
|
1103
|
-
|
|
1188
|
+
modality = request.path_params.get("modality")
|
|
1189
|
+
param_name = JobParamInfo.get_parameter_name(
|
|
1190
|
+
job_type=job_type, task_id=task_id, modality=modality
|
|
1191
|
+
)
|
|
1104
1192
|
try:
|
|
1105
1193
|
param_value = get_parameter_value(param_name)
|
|
1106
1194
|
return JSONResponse(
|
|
@@ -1124,8 +1212,6 @@ def get_parameter(request: Request):
|
|
|
1124
1212
|
async def admin(request: Request):
|
|
1125
1213
|
"""Get admin page if authenticated, else redirect to login."""
|
|
1126
1214
|
user = request.session.get("user")
|
|
1127
|
-
if os.getenv("ENV_NAME") == "local":
|
|
1128
|
-
user = {"name": "local user"}
|
|
1129
1215
|
if user:
|
|
1130
1216
|
return templates.TemplateResponse(
|
|
1131
1217
|
request=request,
|
|
@@ -1143,6 +1229,9 @@ async def admin(request: Request):
|
|
|
1143
1229
|
|
|
1144
1230
|
async def login(request: Request):
|
|
1145
1231
|
"""Redirect to Azure login page"""
|
|
1232
|
+
if os.getenv("ENV_NAME") == "local":
|
|
1233
|
+
request.session["user"] = {"name": "local user"}
|
|
1234
|
+
return RedirectResponse(url="/admin")
|
|
1146
1235
|
oauth = set_oauth()
|
|
1147
1236
|
redirect_uri = request.url_for("auth")
|
|
1148
1237
|
response = await oauth.azure.authorize_redirect(request, redirect_uri)
|
|
@@ -1193,7 +1282,13 @@ routes = [
|
|
|
1193
1282
|
Route("/api/v1/get_task_logs", endpoint=get_task_logs, methods=["GET"]),
|
|
1194
1283
|
Route("/api/v1/parameters", endpoint=list_parameters, methods=["GET"]),
|
|
1195
1284
|
Route(
|
|
1196
|
-
"/api/v1/parameters/job_types/{job_type:str}/tasks/{task_id:
|
|
1285
|
+
"/api/v1/parameters/job_types/{job_type:str}/tasks/{task_id:str}",
|
|
1286
|
+
endpoint=get_parameter,
|
|
1287
|
+
methods=["GET"],
|
|
1288
|
+
),
|
|
1289
|
+
Route(
|
|
1290
|
+
"/api/v1/parameters/job_types/{job_type:str}/tasks/{task_id:str}"
|
|
1291
|
+
"/{modality:str}",
|
|
1197
1292
|
endpoint=get_parameter,
|
|
1198
1293
|
methods=["GET"],
|
|
1199
1294
|
),
|
|
@@ -1204,10 +1299,28 @@ routes = [
|
|
|
1204
1299
|
Route("/api/v2/submit_jobs", endpoint=submit_jobs_v2, methods=["POST"]),
|
|
1205
1300
|
Route("/api/v2/parameters", endpoint=list_parameters_v2, methods=["GET"]),
|
|
1206
1301
|
Route(
|
|
1207
|
-
"/api/v2/parameters/job_types/{job_type:str}/tasks/{task_id:
|
|
1302
|
+
"/api/v2/parameters/job_types/{job_type:str}/tasks/{task_id:str}",
|
|
1303
|
+
endpoint=get_parameter_v2,
|
|
1304
|
+
methods=["GET"],
|
|
1305
|
+
),
|
|
1306
|
+
Route(
|
|
1307
|
+
"/api/v2/parameters/job_types/{job_type:str}/tasks/{task_id:str}"
|
|
1308
|
+
"/{modality:str}",
|
|
1208
1309
|
endpoint=get_parameter_v2,
|
|
1209
1310
|
methods=["GET"],
|
|
1210
1311
|
),
|
|
1312
|
+
Route(
|
|
1313
|
+
"/api/{version:str}/parameters/job_types/{job_type:str}"
|
|
1314
|
+
"/tasks/{task_id:str}",
|
|
1315
|
+
endpoint=put_parameter,
|
|
1316
|
+
methods=["PUT"],
|
|
1317
|
+
),
|
|
1318
|
+
Route(
|
|
1319
|
+
"/api/{version:str}/parameters/job_types/{job_type:str}"
|
|
1320
|
+
"/tasks/{task_id:str}/{modality:str}",
|
|
1321
|
+
endpoint=put_parameter,
|
|
1322
|
+
methods=["PUT"],
|
|
1323
|
+
),
|
|
1211
1324
|
Route("/jobs", endpoint=jobs, methods=["GET"]),
|
|
1212
1325
|
Route("/job_tasks_table", endpoint=job_tasks_table, methods=["GET"]),
|
|
1213
1326
|
Route("/task_logs", endpoint=task_logs, methods=["GET"]),
|
|
@@ -24,13 +24,22 @@
|
|
|
24
24
|
<a title="List of project names" href="{{ project_names_url }}" target="_blank">Project Names</a> |
|
|
25
25
|
<a title="For more information click here" href="https://aind-data-transfer-service.readthedocs.io"
|
|
26
26
|
target="_blank">Help</a> |
|
|
27
|
-
<a href="/admin">Admin</a>
|
|
28
|
-
<a href="/logout">Log out</a>
|
|
27
|
+
<a href="/admin">Admin</a>
|
|
28
|
+
<a href="/logout" class="float-end">Log out</a>
|
|
29
29
|
</nav>
|
|
30
30
|
<div>
|
|
31
31
|
<h3>Admin</h3>
|
|
32
32
|
<div>Hello {{user_name}}, welcome to the admin page</div>
|
|
33
33
|
<div>Email: {{user_email}}</div>
|
|
34
|
+
<h4 class="mt-4">Job Type Parameters</h4>
|
|
35
|
+
<div class="mb-2">
|
|
36
|
+
<div class="alert alert-info mb-0 p-4" role="alert">
|
|
37
|
+
To manage job type parameters, go to the <a href="/job_params" class="alert-link">Job Parameters</a> page.
|
|
38
|
+
Select an existing parameter, or click the
|
|
39
|
+
<button type="button" class="btn btn-success btn-sm" disabled><i class="bi bi-plus-circle"></i> Add New Parameter</button>
|
|
40
|
+
button.
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
34
43
|
</div>
|
|
35
44
|
</body>
|
|
36
45
|
</html>
|
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
<a title="List of project names" href= "{{ project_names_url }}" target="_blank" >Project Names</a> |
|
|
37
37
|
<a title="For more information click here" href="https://aind-data-transfer-service.readthedocs.io" target="_blank" >Help</a> |
|
|
38
38
|
<a href="/admin">Admin</a>
|
|
39
|
+
{% if user_signed_in %}
|
|
40
|
+
<a href="/logout" class="float-end">Log out</a>
|
|
41
|
+
{% else %}
|
|
42
|
+
<a href="/login" class="float-end">Log in</a>
|
|
43
|
+
{% endif %}
|
|
39
44
|
</nav>
|
|
40
45
|
<div class="content">
|
|
41
46
|
<h4 class="mb-2">
|
|
@@ -52,6 +57,14 @@
|
|
|
52
57
|
</div>
|
|
53
58
|
<span>Job Parameters</span>
|
|
54
59
|
</h4>
|
|
60
|
+
<!-- button and modal for adding new parameters-->
|
|
61
|
+
{% if user_signed_in %}
|
|
62
|
+
<div class="mb-2">
|
|
63
|
+
<button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#param-modal" data-bs-action="new">
|
|
64
|
+
<i class="bi bi-plus-circle"></i> Add New Parameter
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
{% endif %}
|
|
55
68
|
<!-- job params table -->
|
|
56
69
|
<div>
|
|
57
70
|
<table id="job-params-table" class="display compact table table-bordered table-sm" style="font-size: small">
|
|
@@ -66,59 +79,128 @@
|
|
|
66
79
|
</thead>
|
|
67
80
|
</table>
|
|
68
81
|
<!-- modal for displaying param value as json -->
|
|
82
|
+
<!-- if user is signed in, the textarea will be editable and the footer will display the Submit button -->
|
|
69
83
|
<div class="modal fade" id="param-modal" tabindex="-1" aria-labelledby="param-modal-label" aria-hidden="true">
|
|
70
84
|
<div class="modal-dialog modal-xl">
|
|
71
85
|
<div class="modal-content">
|
|
72
86
|
<div class="modal-header">
|
|
73
87
|
<h5 class="modal-title" id="param-modal-label"></h5>
|
|
88
|
+
<span class="badge bg-primary ms-2" id="param-modal-version"></span>
|
|
74
89
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
75
90
|
</div>
|
|
76
91
|
<div class="modal-body">
|
|
77
|
-
|
|
92
|
+
<!-- dropdowns for Add New Parameter -->
|
|
93
|
+
<div class="row mb-2" id="param-modal-dropdowns" style="display:none;">
|
|
94
|
+
<div class="col-md-4">
|
|
95
|
+
<label for="param-modal-job-type-select" class="form-label">Job Type</label>
|
|
96
|
+
<select id="param-modal-job-type-select" class="form-select form-select-sm mb-1"></select>
|
|
97
|
+
<input type="text" id="param-modal-job-type-input" class="form-control form-control-sm mt-1" placeholder="Enter new Job Type" style="display:none;" />
|
|
98
|
+
</div>
|
|
99
|
+
<div class="col-md-4">
|
|
100
|
+
<label for="param-modal-task-id-select" class="form-label">Task ID</label>
|
|
101
|
+
<select id="param-modal-task-id-select" class="form-select form-select-sm"></select>
|
|
102
|
+
<input type="text" id="param-modal-task-id-input" class="form-control form-control-sm mt-1" placeholder="Enter new Task ID" style="display:none;" />
|
|
103
|
+
</div>
|
|
104
|
+
<div class="col-md-4" style="display:none;">
|
|
105
|
+
<label for="param-modal-modality-select" class="form-label">Modality</label>
|
|
106
|
+
<select id="param-modal-modality-select" class="form-select form-select-sm">
|
|
107
|
+
<option value="">Select Modality</option>
|
|
108
|
+
{% for modality in modalities %}
|
|
109
|
+
<option value="{{ modality }}">{{ modality }}</option>
|
|
110
|
+
{% endfor %}
|
|
111
|
+
</select>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
<!-- textarea for parameter value -->
|
|
115
|
+
<textarea
|
|
116
|
+
id="param-modal-content" class="bg-light form-control form-control-sm font-monospace" rows="15"
|
|
117
|
+
placeholder='{"skip_task": false}' {% if not user_signed_in %}readonly{% endif %}
|
|
118
|
+
></textarea>
|
|
119
|
+
<!-- message if parameter already exists -->
|
|
120
|
+
<div id="param-modal-param-exists-alert" class="alert alert-info" role="alert" style="display:none;">
|
|
121
|
+
This parameter already exists! To edit, please click on the parameter from the Job Parameters table.
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
{% if user_signed_in %}
|
|
125
|
+
<div class="modal-footer">
|
|
126
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
127
|
+
<button type="button" class="btn btn-secondary" id="param-modal-reset-btn">Reset</button>
|
|
128
|
+
<button type="button" class="btn btn-primary" id="param-modal-submit-btn">Submit</button>
|
|
78
129
|
</div>
|
|
130
|
+
{% endif %}
|
|
79
131
|
</div>
|
|
80
132
|
</div>
|
|
81
133
|
</div>
|
|
82
134
|
</div>
|
|
83
135
|
</div>
|
|
84
136
|
<script>
|
|
137
|
+
const MODALITY_TASKS = {{ modality_tasks | tojson }}; // from Jinja context
|
|
138
|
+
const MODAL_ID = 'param-modal';
|
|
85
139
|
$(document).ready(function() {
|
|
86
140
|
createJobParamsTable();
|
|
87
|
-
// Event listeners for
|
|
88
|
-
$(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
modal.find(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
141
|
+
// Event listeners for modal to display/edit params
|
|
142
|
+
$(`#${MODAL_ID}`).on('show.bs.modal', function(event) {
|
|
143
|
+
// Add New Parameter: set label and dropdown options
|
|
144
|
+
// Edit Existing Parameter: set label and load intital value
|
|
145
|
+
const eventTarget = $(event.relatedTarget);
|
|
146
|
+
const action = eventTarget.data('bs-action');
|
|
147
|
+
const modal = $(`#${MODAL_ID}`);
|
|
148
|
+
modal.data('bs-action', action); // save action type
|
|
149
|
+
const isNew = action === 'new';
|
|
150
|
+
modal.find(`#${MODAL_ID}-label`).text(isNew ? 'Add New Parameter' : eventTarget.data('bs-param-name'));
|
|
151
|
+
modal.find(`#${MODAL_ID}-version`).text(getCurrentVersion()).toggle(isNew);
|
|
152
|
+
modal.find(`#${MODAL_ID}-dropdowns`).toggle(isNew);
|
|
153
|
+
if (isNew) {
|
|
154
|
+
['Job Type', 'Task ID'].forEach(field => {
|
|
155
|
+
const select = modal.find(`#${MODAL_ID}-${field.toLowerCase().replace(' ', '-')}-select`);
|
|
156
|
+
select.empty()
|
|
157
|
+
.append(`<option value="">Select ${field}</option>`)
|
|
158
|
+
.append(getUniqueColumnValues(field).map(val => `<option value="${val}">${val}</option>`))
|
|
159
|
+
.append(`<option value="__new__">Create new ${field}</option>`);
|
|
160
|
+
});
|
|
161
|
+
} else {
|
|
162
|
+
loadParameterValue(getParamUrlFromParamName(eventTarget.data('bs-param-name')));
|
|
101
163
|
}
|
|
102
|
-
$.ajax({
|
|
103
|
-
url: getParameterUrl,
|
|
104
|
-
type: 'GET',
|
|
105
|
-
success: function(response) {
|
|
106
|
-
jsonStr = JSON.stringify(response.data, null, 3);
|
|
107
|
-
modal.find('#param-modal-content').text(jsonStr);
|
|
108
|
-
},
|
|
109
|
-
error: function(xhr, status, error) {
|
|
110
|
-
modal.find('#param-modal-content').text(error);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
164
|
});
|
|
114
|
-
$(
|
|
115
|
-
|
|
116
|
-
|
|
165
|
+
$(`#${MODAL_ID}`).on('hidden.bs.modal', function() {
|
|
166
|
+
onResetModal();
|
|
167
|
+
});
|
|
168
|
+
$(`#${MODAL_ID}`).on('click', `#${MODAL_ID}-reset-btn`, function() {
|
|
169
|
+
onResetModal(true);
|
|
170
|
+
});
|
|
171
|
+
$(`#${MODAL_ID}`).on('click', `#${MODAL_ID}-submit-btn`, function() {
|
|
172
|
+
const modal = $(`#${MODAL_ID}`);
|
|
173
|
+
const action = modal.data('bs-action');
|
|
174
|
+
let url;
|
|
175
|
+
if (action === "new") {
|
|
176
|
+
const version = getCurrentVersion();
|
|
177
|
+
const jobType = getValidatedInputValue('Job Type', modal);
|
|
178
|
+
const taskId = getValidatedInputValue('Task ID', modal);
|
|
179
|
+
const modality = MODALITY_TASKS.includes(taskId) ? getValidatedInputValue('Modality', modal) : null;
|
|
180
|
+
url = getParamUrlFromParamInfo(version, jobType, taskId, modality);
|
|
181
|
+
} else {
|
|
182
|
+
const paramName = modal.find(`#${MODAL_ID}-label`).text();
|
|
183
|
+
url = getParamUrlFromParamName(paramName);
|
|
184
|
+
}
|
|
185
|
+
const paramValue = modal.find(`#${MODAL_ID}-content`).val();
|
|
186
|
+
submitParameterValue(url, paramValue);
|
|
187
|
+
});
|
|
188
|
+
$(`#${MODAL_ID}-job-type-select`).on('change', function() {
|
|
189
|
+
$(`#${MODAL_ID}-job-type-input`).toggle($(this).val() === '__new__').focus();
|
|
190
|
+
handleExistingParam();
|
|
191
|
+
});
|
|
192
|
+
$(`#${MODAL_ID}-task-id-select`).on('change', function() {
|
|
193
|
+
$(`#${MODAL_ID}-task-id-input`).toggle($(this).val() === '__new__').focus();
|
|
194
|
+
$(`#${MODAL_ID}-modality-select`).val('').parent().toggle(MODALITY_TASKS.includes($(this).val()));
|
|
195
|
+
handleExistingParam();
|
|
196
|
+
});
|
|
197
|
+
$(`#${MODAL_ID}-modality-select`).on('change', function() {
|
|
198
|
+
handleExistingParam();
|
|
117
199
|
});
|
|
118
200
|
// Event listener for version dropdown
|
|
119
201
|
$('#version-dropdown .dropdown-item').on('click', function() {
|
|
120
202
|
var version = $(this).text();
|
|
121
|
-
if (version !=
|
|
203
|
+
if (version != getCurrentVersion()) {
|
|
122
204
|
$('#version-button').text(version);
|
|
123
205
|
$('#job-params-table').DataTable().ajax.url(`/api/${version}/parameters`).load();
|
|
124
206
|
}
|
|
@@ -179,7 +261,8 @@
|
|
|
179
261
|
return (
|
|
180
262
|
`<button type="button" class="btn btn-link btn-sm"
|
|
181
263
|
data-bs-toggle="modal"
|
|
182
|
-
data-bs-target="
|
|
264
|
+
data-bs-target="#${MODAL_ID}"
|
|
265
|
+
data-bs-action="edit"
|
|
183
266
|
data-bs-param-name=${data}
|
|
184
267
|
data-bs-job-type=${row.job_type}
|
|
185
268
|
data-bs-task-id=${row.task_id}
|
|
@@ -190,6 +273,133 @@
|
|
|
190
273
|
}
|
|
191
274
|
return data;
|
|
192
275
|
}
|
|
276
|
+
// Methods to load/submit param values in the modal
|
|
277
|
+
function getParamUrlFromParamInfo(version, jobType, taskId, modality) {
|
|
278
|
+
var baseUrl = `/api/${version}/parameters/job_types/${jobType}/tasks/${taskId}`;
|
|
279
|
+
return modality ? `${baseUrl}/${modality}` : baseUrl;
|
|
280
|
+
}
|
|
281
|
+
function getParamUrlFromParamName(paramName) {
|
|
282
|
+
const match = paramName.match(/job_types\/(v\d+)?\/?(.*)/);
|
|
283
|
+
const version = match && match[1] ? match[1] : 'v1';
|
|
284
|
+
const cleanedParamName = match ? match[2] : paramName;
|
|
285
|
+
return `/api/${version}/parameters/job_types/${cleanedParamName}`;
|
|
286
|
+
}
|
|
287
|
+
function loadParameterValue(paramUrl) {
|
|
288
|
+
const modal = $(`#${MODAL_ID}`);
|
|
289
|
+
$.ajax({
|
|
290
|
+
url: paramUrl,
|
|
291
|
+
type: 'GET',
|
|
292
|
+
success: function (response) {
|
|
293
|
+
jsonStr = JSON.stringify(response.data, null, 3);
|
|
294
|
+
modal.find(`#${MODAL_ID}-content`).val(jsonStr);
|
|
295
|
+
},
|
|
296
|
+
error: function (xhr, status, error) {
|
|
297
|
+
console.error(`Error fetching ${paramUrl}: ${error}`);
|
|
298
|
+
modal.find(`#${MODAL_ID}-content`).val(error);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function submitParameterValue(paramUrl, paramValue) {
|
|
303
|
+
try {
|
|
304
|
+
JSON.parse(paramValue);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
alert('Parameter value must be valid JSON:\n' + e.message);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
$.ajax({
|
|
310
|
+
url: paramUrl,
|
|
311
|
+
type: 'PUT',
|
|
312
|
+
contentType: 'application/json',
|
|
313
|
+
data: paramValue,
|
|
314
|
+
success: function (response) {
|
|
315
|
+
alert('Parameter updated successfully');
|
|
316
|
+
// reload the content to verify and reformat changes
|
|
317
|
+
loadParameterValue(paramUrl);
|
|
318
|
+
},
|
|
319
|
+
error: function (xhr, status, error) {
|
|
320
|
+
var msg = `Error submitting parameter: ${error}`;
|
|
321
|
+
try {
|
|
322
|
+
msg += `\n\n${JSON.stringify(JSON.parse(xhr.responseText), null, 3)}`;
|
|
323
|
+
} catch (e) {
|
|
324
|
+
console.error('Failed to parse error response:', e.message);
|
|
325
|
+
}
|
|
326
|
+
console.error(msg);
|
|
327
|
+
alert(msg);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
// Methods for param modal updates
|
|
332
|
+
function toggleParamTextArea(isEnabled){
|
|
333
|
+
const modal = $(`#${MODAL_ID}`);
|
|
334
|
+
modal.find(`#${MODAL_ID}-content`).toggle(isEnabled);
|
|
335
|
+
modal.find(`#${MODAL_ID}-param-exists-alert`).toggle(!isEnabled);
|
|
336
|
+
modal.find(`#${MODAL_ID}-submit-btn`).prop('disabled', !isEnabled);
|
|
337
|
+
}
|
|
338
|
+
function handleExistingParam() {
|
|
339
|
+
let exists = false;
|
|
340
|
+
const modal = $(`#${MODAL_ID}`);
|
|
341
|
+
const version = getCurrentVersion();
|
|
342
|
+
const jobType = getInputValues('Job Type', modal).input;
|
|
343
|
+
const taskId = getInputValues('Task ID', modal).input;
|
|
344
|
+
const modality = MODALITY_TASKS.includes(taskId) ? getInputValues('Modality', modal).input : null;
|
|
345
|
+
if (jobType && taskId && (!MODALITY_TASKS.includes(taskId) || modality)) {
|
|
346
|
+
const table = $('#job-params-table').DataTable();
|
|
347
|
+
const searchStr = `/${jobType}/tasks/${taskId}` + (modality ? `/${modality}` : '');
|
|
348
|
+
exists = table.column('Parameter Name:title').data().toArray().some(val => val.endsWith(searchStr));
|
|
349
|
+
}
|
|
350
|
+
toggleParamTextArea(!exists);
|
|
351
|
+
}
|
|
352
|
+
function onResetModal(reload = false) {
|
|
353
|
+
const modal = $(`#${MODAL_ID}`);
|
|
354
|
+
const action = modal.data('bs-action');
|
|
355
|
+
if (action === "new") {
|
|
356
|
+
modal.find(`#${MODAL_ID}-dropdowns input`).val('').hide();
|
|
357
|
+
modal.find(`#${MODAL_ID}-dropdowns select`).val('');
|
|
358
|
+
modal.find(`#${MODAL_ID}-modality-select`).parent().hide();
|
|
359
|
+
modal.find(`#${MODAL_ID}-content`).val('');
|
|
360
|
+
toggleParamTextArea(true);
|
|
361
|
+
} else if (action === "edit") {
|
|
362
|
+
if (reload) {
|
|
363
|
+
const paramName = modal.find(`#${MODAL_ID}-label`).text();
|
|
364
|
+
loadParameterValue(getParamUrlFromParamName(paramName));
|
|
365
|
+
} else {
|
|
366
|
+
modal.find(`#${MODAL_ID}-label, #${MODAL_ID}-content`).text('').val('');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Helper methods to get current values
|
|
371
|
+
function getCurrentVersion() {
|
|
372
|
+
return $('#version-button').text().trim();
|
|
373
|
+
}
|
|
374
|
+
function getUniqueColumnValues(columnTitle) {
|
|
375
|
+
return Array.from(new Set($('#job-params-table').DataTable().column(`${columnTitle}:title`).data().toArray()));
|
|
376
|
+
}
|
|
377
|
+
function getInputValues(inputField, modal) {
|
|
378
|
+
const selector = `#${MODAL_ID}-${inputField.toLowerCase().replace(' ', '-')}`;
|
|
379
|
+
const dropdown = modal.find(`${selector}-select`).val()?.trim() || '';
|
|
380
|
+
const input = (dropdown === '__new__') ? (modal.find(`${selector}-input`).val()?.trim() || '') : dropdown;
|
|
381
|
+
return { dropdown, input };
|
|
382
|
+
}
|
|
383
|
+
function getValidatedInputValue(inputField, modal) {
|
|
384
|
+
// Check for empty string, spaces, slashes, or existing values
|
|
385
|
+
const { dropdown, input } = getInputValues(inputField, modal);
|
|
386
|
+
var error = false;
|
|
387
|
+
if (!input) {
|
|
388
|
+
error = `${inputField} cannot be empty`;
|
|
389
|
+
} else if (input.includes(' ') || input.includes('/')) {
|
|
390
|
+
error = `${inputField} cannot contain spaces or slashes`;
|
|
391
|
+
} else if (
|
|
392
|
+
dropdown === '__new__' &&
|
|
393
|
+
getUniqueColumnValues(inputField).some(val => val.toLowerCase() === input.toLowerCase())
|
|
394
|
+
) {
|
|
395
|
+
error = `${inputField} "${input}" already exists. Please select it from the dropdown.`;
|
|
396
|
+
}
|
|
397
|
+
if (error) {
|
|
398
|
+
alert(error);
|
|
399
|
+
throw new Error(error);
|
|
400
|
+
}
|
|
401
|
+
return input;
|
|
402
|
+
}
|
|
193
403
|
</script>
|
|
194
|
-
|
|
404
|
+
</body>
|
|
195
405
|
</html>
|
{aind_data_transfer_service-1.16.0.dist-info → aind_data_transfer_service-1.17.0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aind-data-transfer-service
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.17.0
|
|
4
4
|
Summary: Service that handles requests to upload data to the cloud
|
|
5
5
|
Author: Allen Institute for Neural Dynamics
|
|
6
6
|
License: MIT
|
|
@@ -25,7 +25,7 @@ Requires-Dist: furo; extra == "docs"
|
|
|
25
25
|
Provides-Extra: server
|
|
26
26
|
Requires-Dist: aind-data-schema<2.0,>=1.0.0; extra == "server"
|
|
27
27
|
Requires-Dist: aind-data-transfer-models==0.17.0; extra == "server"
|
|
28
|
-
Requires-Dist: aind-metadata-mapper
|
|
28
|
+
Requires-Dist: aind-metadata-mapper>=0.23.0; extra == "server"
|
|
29
29
|
Requires-Dist: boto3; extra == "server"
|
|
30
30
|
Requires-Dist: boto3-stubs[ssm]; extra == "server"
|
|
31
31
|
Requires-Dist: fastapi>=0.115.13; extra == "server"
|
{aind_data_transfer_service-1.16.0.dist-info → aind_data_transfer_service-1.17.0.dist-info}/RECORD
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
aind_data_transfer_service/__init__.py,sha256=
|
|
1
|
+
aind_data_transfer_service/__init__.py,sha256=3zeB4HVxFXx2bXdNaQXcSZw0Irq7ZulbJJ5bI0ixIYo,272
|
|
2
2
|
aind_data_transfer_service/log_handler.py,sha256=c7a-gLmZeRpeCUBwCz6XsTszWXQeQdR7eKZtas4llXM,1700
|
|
3
|
-
aind_data_transfer_service/server.py,sha256=
|
|
3
|
+
aind_data_transfer_service/server.py,sha256=1vtmqMKF_7mv2FWrRktvjPmi-slYwt4pfUy51P3WRVc,48341
|
|
4
4
|
aind_data_transfer_service/configs/__init__.py,sha256=9W5GTuso9Is1B9X16RXcdb_GxasZvj6qDzOBDv0AbTc,36
|
|
5
5
|
aind_data_transfer_service/configs/csv_handler.py,sha256=hCdfAYZW_49-l1rbua5On2Tw2ks674Z-MgB_NJlIkU4,5746
|
|
6
6
|
aind_data_transfer_service/configs/job_configs.py,sha256=T-h5N6lyY9xTZ_xg_5FxkyYuMdagApbE6xalxFQ-bqA,18848
|
|
@@ -10,15 +10,15 @@ aind_data_transfer_service/hpc/client.py,sha256=-JSxAWn96_XOIDwhsXAHK3TZAdckddUh
|
|
|
10
10
|
aind_data_transfer_service/hpc/models.py,sha256=-7HhV16s_MUyKPy0x0FGIbnq8DPL2qJAzJO5G7003AE,16184
|
|
11
11
|
aind_data_transfer_service/models/__init__.py,sha256=Meym73bEZ9nQr4QoeyhQmV3nRTYtd_4kWKPNygsBfJg,25
|
|
12
12
|
aind_data_transfer_service/models/core.py,sha256=uXtPUqjxKalg-sE8MxaJr11w_T_KKBRBSJuUgwoMZlQ,10135
|
|
13
|
-
aind_data_transfer_service/models/internal.py,sha256=
|
|
14
|
-
aind_data_transfer_service/templates/admin.html,sha256=
|
|
13
|
+
aind_data_transfer_service/models/internal.py,sha256=tWO1yRMu9hHLv0mt7QhOwTPWGbKApWmCf1wwajx5qjI,10681
|
|
14
|
+
aind_data_transfer_service/templates/admin.html,sha256=owmWgcTFzfWh5S2L82OT-0r5pOt1bl8Fh-C0zyVo1wU,1682
|
|
15
15
|
aind_data_transfer_service/templates/index.html,sha256=TDmmHlhWFPnQrk6nk1OhNjC3SapZtX0TXeuUonoCO7g,11326
|
|
16
|
-
aind_data_transfer_service/templates/job_params.html,sha256=
|
|
16
|
+
aind_data_transfer_service/templates/job_params.html,sha256=Q5oAsuQqBiTvOOS_7BejcqPJ4rskg_ZtG1E0NAjcGJw,21063
|
|
17
17
|
aind_data_transfer_service/templates/job_status.html,sha256=5lUUGZL-5urppG610qDOgpfIE-OcQH57gFWvRA5pBNM,16938
|
|
18
18
|
aind_data_transfer_service/templates/job_tasks_table.html,sha256=rWFukhjZ4dhPyabe372tmi4lbQS2fyELZ7Awbn5Un4g,6181
|
|
19
19
|
aind_data_transfer_service/templates/task_logs.html,sha256=y1GnQft0S50ghPb2xJDjAlefymB9a4zYdMikUFV7Tl4,918
|
|
20
|
-
aind_data_transfer_service-1.
|
|
21
|
-
aind_data_transfer_service-1.
|
|
22
|
-
aind_data_transfer_service-1.
|
|
23
|
-
aind_data_transfer_service-1.
|
|
24
|
-
aind_data_transfer_service-1.
|
|
20
|
+
aind_data_transfer_service-1.17.0.dist-info/licenses/LICENSE,sha256=U0Y7B3gZJHXpjJVLgTQjM8e_c8w4JJpLgGhIdsoFR1Y,1092
|
|
21
|
+
aind_data_transfer_service-1.17.0.dist-info/METADATA,sha256=pBXxvTxzxZRam6dubbJn7oPNQfBClr1TPUvwrPB3W7U,2478
|
|
22
|
+
aind_data_transfer_service-1.17.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
+
aind_data_transfer_service-1.17.0.dist-info/top_level.txt,sha256=XmxH0q27Jholj2-VYh-6WMrh9Lw6kkuCX_fdsj3SaFE,27
|
|
24
|
+
aind_data_transfer_service-1.17.0.dist-info/RECORD,,
|
{aind_data_transfer_service-1.16.0.dist-info → aind_data_transfer_service-1.17.0.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|