From 12d1f009b4307bc66fee5e5b9b39da0c8bcca0b8 Mon Sep 17 00:00:00 2001 From: jtyoung84 <104453205+jtyoung84@users.noreply.github.com> Date: Sun, 28 Apr 2024 14:56:19 -0700 Subject: [PATCH] feat: adds new fields to template --- .../configs/job_configs.py | 13 +++ .../configs/job_upload_template.py | 102 ++++++++++++------ src/aind_data_transfer_service/server.py | 44 +++++--- .../templates/index.html | 15 +-- tests/resources/job_upload_template.xlsx | Bin 5686 -> 6164 bytes tests/resources/sample.csv | 8 +- tests/resources/sample.xlsx | Bin 9133 -> 5611 bytes tests/resources/sample_alt_modality_case.csv | 8 +- tests/resources/sample_empty_rows.csv | 8 +- tests/resources/sample_empty_rows.xlsx | Bin 10893 -> 5607 bytes tests/resources/sample_invalid_ext.txt | 8 +- tests/resources/sample_malformed.csv | 8 +- tests/resources/sample_malformed.xlsx | Bin 9113 -> 5628 bytes tests/test_configs.py | 16 +++ tests/test_hpc_models.py | 2 + tests/test_job_upload_template.py | 53 +++++++-- tests/test_server.py | 101 +++++++++++++---- 17 files changed, 283 insertions(+), 103 deletions(-) diff --git a/src/aind_data_transfer_service/configs/job_configs.py b/src/aind_data_transfer_service/configs/job_configs.py index 8c2a91a..72c4ce9 100644 --- a/src/aind_data_transfer_service/configs/job_configs.py +++ b/src/aind_data_transfer_service/configs/job_configs.py @@ -131,6 +131,19 @@ class BasicUploadJobConfigs(BaseSettings): aws_param_store_name: Optional[str] = Field(None) + processor_full_name: str = Field( + ..., + description="Name of person uploading data", + title="Processor Full Name", + ) + project_name: str = Field( + ..., description="Name of project", title="Project Name" + ) + process_capsule_id: Optional[str] = Field( + None, + description="Use custom codeocean capsule or pipeline id", + title="Process Capsule ID", + ) s3_bucket: Optional[str] = Field( None, description="Bucket where data will be uploaded", diff --git a/src/aind_data_transfer_service/configs/job_upload_template.py b/src/aind_data_transfer_service/configs/job_upload_template.py index f49c946..a934809 100644 --- a/src/aind_data_transfer_service/configs/job_upload_template.py +++ b/src/aind_data_transfer_service/configs/job_upload_template.py @@ -1,6 +1,7 @@ """Module to configure and create xlsx job upload template""" import datetime from io import BytesIO +from typing import Any, Dict, List, Optional from aind_data_schema.models.modalities import Modality from aind_data_schema.models.platforms import Platform @@ -14,10 +15,23 @@ class JobUploadTemplate: """Class to configure and create xlsx job upload template""" + def __init__(self, project_names: Optional[List[str]] = None): + """ + Class constructor to create template files. + Parameters + ---------- + project_names : Optional[List[str]] + Optional list of project names. Default is None. + """ + self.project_names = [] if project_names is None else project_names + FILE_NAME = "job_upload_template.xlsx" NUM_TEMPLATE_ROWS = 20 XLSX_DATETIME_FORMAT = "YYYY-MM-DDTHH:mm:ss" HEADERS = [ + "processor_full_name", + "project_name", + "process_capsule_id", "platform", "acq_datetime", "subject_id", @@ -29,6 +43,9 @@ class JobUploadTemplate: ] SAMPLE_JOBS = [ [ + "Anna Apple", + "Behavior Platform", + "1f999652-00a0-4c4b-99b5-64c2985ad070", Platform.BEHAVIOR.abbreviation, datetime.datetime(2023, 10, 4, 4, 0, 0), "123456", @@ -39,6 +56,9 @@ class JobUploadTemplate: "/allen/aind/stage/fake/dir", ], [ + "John Smith", + "Ophys Platform - SLAP2", + None, Platform.SMARTSPIM.abbreviation, datetime.datetime(2023, 3, 4, 16, 30, 0), "654321", @@ -47,6 +67,9 @@ class JobUploadTemplate: "/allen/aind/stage/fake/dir", ], [ + "Anna Apple", + "Ephys Platform", + None, Platform.ECEPHYS.abbreviation, datetime.datetime(2023, 1, 30, 19, 1, 0), "654321", @@ -57,42 +80,57 @@ class JobUploadTemplate: "/allen/aind/stage/fake/dir", ], ] - VALIDATORS = [ - { - "name": "platform", - "type": "list", - "options": [p().abbreviation for p in Platform._ALL], - "column_indexes": [HEADERS.index("platform")], - }, - { - "name": "modality", - "type": "list", - "options": [m().abbreviation for m in Modality._ALL], - "column_indexes": [ - HEADERS.index("modality0"), - HEADERS.index("modality1"), - ], - }, - { - "name": "datetime", - "type": "date", - "column_indexes": [HEADERS.index("acq_datetime")], - }, - ] - @staticmethod - def create_job_template(): + @property + def validators(self) -> List[Dict[str, Any]]: + """ + Returns + ------- + List[Dict[str, Any]] + A list of validators for fields that require validation. + + """ + return [ + { + "name": "platform", + "type": "list", + "options": [p().abbreviation for p in Platform._ALL], + "column_indexes": [self.HEADERS.index("platform")], + }, + { + "name": "project_name", + "type": "list", + "options": [p for p in self.project_names], + "column_indexes": [self.HEADERS.index("project_name")], + }, + { + "name": "modality", + "type": "list", + "options": [m().abbreviation for m in Modality._ALL], + "column_indexes": [ + self.HEADERS.index("modality0"), + self.HEADERS.index("modality1"), + ], + }, + { + "name": "datetime", + "type": "date", + "column_indexes": [self.HEADERS.index("acq_datetime")], + }, + ] + + @property + def excel_sheet_filestream(self) -> BytesIO: """Create job template as xlsx filestream""" - # job template xl_io = BytesIO() workbook = Workbook() workbook.iso_dates = True worksheet = workbook.active - worksheet.append(JobUploadTemplate.HEADERS) - for job in JobUploadTemplate.SAMPLE_JOBS: + worksheet.append(self.HEADERS) + for job in self.SAMPLE_JOBS: worksheet.append(job) # data validators - for validator in JobUploadTemplate.VALIDATORS: + for validator in self.validators: dv_type = validator["type"] dv_name = validator["name"] dv_params = { @@ -108,19 +146,17 @@ def create_job_template(): dv_params["prompt"] = f"Select a {dv_name} from the dropdown" elif dv_type == "date": dv_params["prompt"] = "Provide a {} using {}".format( - dv_name, JobUploadTemplate.XLSX_DATETIME_FORMAT + dv_name, self.XLSX_DATETIME_FORMAT ) dv = DataValidation(**dv_params) for i in validator["column_indexes"]: col = get_column_letter(i + 1) - col_range = ( - f"{col}2:{col}{JobUploadTemplate.NUM_TEMPLATE_ROWS}" - ) + col_range = f"{col}2:{col}{self.NUM_TEMPLATE_ROWS}" dv.add(col_range) if dv_type != "date": continue for (cell,) in worksheet[col_range]: - cell.number_format = JobUploadTemplate.XLSX_DATETIME_FORMAT + cell.number_format = self.XLSX_DATETIME_FORMAT worksheet.add_data_validation(dv) # formatting bold = Font(bold=True) diff --git a/src/aind_data_transfer_service/server.py b/src/aind_data_transfer_service/server.py index 26c871d..d1c83f7 100644 --- a/src/aind_data_transfer_service/server.py +++ b/src/aind_data_transfer_service/server.py @@ -7,12 +7,14 @@ from asyncio import sleep from pathlib import PurePosixPath +import requests from fastapi import Request from fastapi.responses import JSONResponse, StreamingResponse from fastapi.templating import Jinja2Templates from openpyxl import load_workbook from pydantic import SecretStr from starlette.applications import Starlette +from starlette.concurrency import run_in_threadpool from starlette.routing import Route from aind_data_transfer_service import OPEN_DATA_BUCKET_NAME @@ -48,6 +50,7 @@ # BASIC_JOB_SCRIPT # OPEN_DATA_AWS_SECRET_ACCESS_KEY # OPEN_DATA_AWS_ACCESS_KEY_ID +# AIND_PROJECT_NAMES_URL async def validate_csv(request: Request): @@ -329,11 +332,36 @@ async def jobs(request: Request): ) -def download_job_template(request: Request): +async def download_job_template(_: Request): """Get job template as xlsx filestream for download""" + + # TODO: Cache list of project names try: - xl_io = JobUploadTemplate.create_job_template() + smart_sheet_response = await run_in_threadpool( + requests.get, url=os.getenv("AIND_PROJECT_NAMES_URL") + ) + if smart_sheet_response.status_code == 200: + project_names = smart_sheet_response.json()["data"] + else: + raise Exception("Unable to get project names!") + + job_template = JobUploadTemplate(project_names=project_names) + xl_io = job_template.excel_sheet_filestream + return StreamingResponse( + io.BytesIO(xl_io.getvalue()), + media_type=( + "application/" + "vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ), + headers={ + "Content-Disposition": ( + f"attachment; filename={job_template.FILE_NAME}" + ) + }, + status_code=200, + ) except Exception as e: + logging.error(e) return JSONResponse( content={ "message": "Error creating job template", @@ -341,18 +369,6 @@ def download_job_template(request: Request): }, status_code=500, ) - return StreamingResponse( - io.BytesIO(xl_io.getvalue()), - media_type=( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - ), - headers={ - "Content-Disposition": ( - f"attachment; filename={JobUploadTemplate.FILE_NAME}" - ) - }, - status_code=200, - ) routes = [ diff --git a/src/aind_data_transfer_service/templates/index.html b/src/aind_data_transfer_service/templates/index.html index d20a1e4..2901740 100644 --- a/src/aind_data_transfer_service/templates/index.html +++ b/src/aind_data_transfer_service/templates/index.html @@ -144,7 +144,7 @@

Submit Jobs

let jobsLength = jobs.length; var table = document.createElement('table'), tr, td, row; addTableRow( - [ "s3_bucket", "platform", "subject_id", "acq_datetime", "metadata_dir", "modality", "modality.source" ], + [ "processor_full_name", "project_name", "process_capsule_id", "s3_bucket", "platform", "subject_id", "acq_datetime", "metadata_dir", "modality", "modality.source" ], table, tr, td, true ); for (row = 0; row < jobsLength; row++) { @@ -152,11 +152,14 @@

Submit Jobs

let modalities = job.modalities; let modalitiesLength = modalities.length; addTableRow( - [ { value: job.s3_bucket, rowspan: modalitiesLength }, - { value: job.platform.abbreviation, rowspan: modalitiesLength }, - { value: job.subject_id, rowspan: modalitiesLength }, - { value: job.acq_datetime, rowspan: modalitiesLength }, - { value: job.metadata_dir ?? "", rowspan: modalitiesLength }, + [ { value: job.processor_full_name, rowspan: modalitiesLength }, + { value: job.project_name, rowspan: modalitiesLength }, + { value: job.process_capsule_id ?? "", rowspan: modalitiesLength }, + { value: job.s3_bucket, rowspan: modalitiesLength }, + { value: job.platform.abbreviation, rowspan: modalitiesLength }, + { value: job.subject_id, rowspan: modalitiesLength }, + { value: job.acq_datetime, rowspan: modalitiesLength }, + { value: job.metadata_dir ?? "", rowspan: modalitiesLength }, modalities ? modalities[0].modality.abbreviation : "", modalities ? modalities[0].source : "" ], table, tr, td, false diff --git a/tests/resources/job_upload_template.xlsx b/tests/resources/job_upload_template.xlsx index 5c0a8755150f55f01d14871d4c552ebe7e5dd805..3738771dd4a556fadb636aac69f4720b57e21f56 100644 GIT binary patch delta 2314 zcmZuzc{tQ-8~zzfqQNi|SxOBWTgl#(C5(p2mXl?WWg1z=5M#?S_Cyh5YmjBagh`fT zq6H~U7)z8aEpsB3CEIX5%kimiIrUxN`^S4qv>5kRQO>hSB-*puR3F?ox1 z$z-1r7hh2Y7EmS3dJ}%vNteU^xh%AAcl>} z6|1mALLm7s63E=NtOZ=e=W=^(!;g>z^2={uQ``5sB$}Qz&q}Z;xbm#PEyw(5_X1?x z*+bhSUBo~UkuFYD^CW%gPdV<)IyjpAb>s2pnwv6uDje=2C-S@@(=cmET5D8Fon(e` z%Nas(PBwv~Rdw@6-TAd^E+v$Pv|EK)ov+@Qi_N#_izseHPKQVJnYrz5jxBUGIZ6za~Uq;mUn#WK;{QHpL1E;^32mnBnBmhVOlwPPb`FAH5`$NZ} zf2k_p3agRcc!DY$P~nG=3d)i$`IpoV$SL?EzgWC1-Hx?c7x2?V*yEgyvzLpDdh#il zK=@=M7E(TlVckd8F`6=3n;Dz1-StQagrm^_)qE;VU3%e=rJ#>CKGsjuW^khJ(+ z`UiCOROH;txp$G)APckoh*5g+dsalF*dL?XT^*6^W9AU^=m3((pIC~aXD=OOAB)HO zk2`~d*TV0{7VrVHE*_}$ge_CvN65P3SGLA^`rhm33FZnR)wyKE3cvk}!(+Ff5NRoU z&AuG8GFZb^pbC5w!qAcH*WH&`(XctZ&o=}KZmO4IqNhb#MvzoZnHeTbEz$;J#W%&! zKqw}!tUtpfT$417XquTMUCQW`B=zh)WjU6##9OWvx;1$H_RO(%st?w0BghE*EdA;7*}}k8oN4*7-fIVVRWJiIULS~uIk(0h zBu5s%TyCberO6C5s_IaWe-|T;MYe!f)b(d!va2fO9Jk<%K|Hkn>`RBTyDXu)J2!l= zY-%j*jawM9IIO4Ugh<#|tF{(;I5OSg(c6_rea^zW`h1-&1PG(2+YQ*eye|NRiF8S?&>c)wxw^w!?#K73TzHw9M~=PbarKNH<4ET`jp9liGYm zmQm+RHcnEZ%}J^&>wQ>FMZe#g?+uI?M?{R%ZKG5jWZ5Rwj`)WRldV*Tx@vj#YT78X z=Z>R!vkF*Czx&H5>7+8wg;o?`-Fc545H6!gsG-u|`sqGw_!?xNOiv}bFDS;Sd~~kJ zoSMJhroZ?XR4*N>m#JcPjy|KOd;}8!%l?!6)T2jyycS*S5~v@-915Mtyvm?fYSz>0 zWOVN)Lr&r`NRdk;?$5&J}-Q?C0cJR%+qY+raTl119Rr&f?g~Nn8 z`eILwC@Zk-LhI-cV4#rfb=$MogLCen08Q;C$x{jBT&B zmhsxKgsIX#;R$QOIa`MDnlA@xn*8ia2zi~1K;D8eHG!n=wX{CUgaJ%NhmJUdRW|@dFM|i(^M4wXZSn_m% z7wVAWOee#;y35whw4Va>i?iUXv@g!c>_lpd%-I3NV?G>t^{E3U6Gbsnk3B2_26@d6lKQ ze&mOzF6iVYTuJiB$H1L};d0WtW1@Ej?Xi7I`QxKXe6!wfEQ_3MJDy6=UCVnW@$7X7 z#Bdp9Rx5h3n*;USAE(a4m3XdWchzXB%EmuWv!w8RW zjkByPG&pespKu6W7k;TNVKVo{Z9}Qv(=?(~SZ=o|L1Bo!@b+E)V{e;#N%%Wra7;n_ z@#D#z@n_tx45jR~^I9?SQGYTyo%SIP4F^7A4w42nhM=Nc$!*?WeWzhhCx~~)cPJ5} zpT1+Grx=#!2k{v4#}9%gZpI_T`(HDsEJ|pL{eAqGsZ(^IM}O83p)x$(lr*S}(of1? zoo&be|Gnbkg(*EydDKtRQm?tyLJ$BH2?GH5Ke`=^<4F_&fC$3T5K<%oAM!mj*kX7< z(7&%zG+;9OEq2m{doZ35a`ede{eLCCFc~9F@gH@<1B3m&0t5a3PbDX)EpPq-wfJjh^@LolMcG!3QKLPZm2DAVG delta 1857 zcmZvdc{tSj7stOd)&`SphD+BTZkYy!$kO7mmMGa$W1H$4OvXORaHC=n2HD4m>|rp4 zkPM~8B;#R1W4og){Rm@A{YKr}b$`$A{Bh3nInO!g^ZuMaUY~SrnwFToEg!!$006=O z*DW?xteU_JF1}Y)+-G>nmmmO;;6)#-w>=JfH3;dA#i0*``}>uop2Bo$i{AWZ(_`Y$ zCfixu)-c!5;SrZKnpj1nEDF&q-V)l?F#SpM`U_onMU%9^8y5>jmiOe9K0$O1co@kG zY=q!^xL~D;)Wigq)sW6ei-+``!K*+GVWAkxk1k-xC_A~;{b8N8(Us+%E+62gTJ?-GQ+w;d)u9H^=UM0!H7P!l6x=1ojpGvig_d5g;Eglju zY<*6?^&ke@#nIiYJc`$|6OZIJ*gfdMG_pP^NV;;+`J8?hY_+J${6hQb2Z~#Q29&_& z(TAjV5R8wviuys2si5TcPHtqkokQ~jfGckyvH+<9E=Oo|`A<-hk@S|ctf{whG!4zn zc55_R$_w#jf0`?`OD~KNc&hF&{O#-LLVxHnw=mkIG%l#OwZ~9RcGI#OMNbwlZ?*8$ z65OzRK|gq^j+H7*+@F~uiwAeBYEgO~-nTm35oMEbyq{K8@QbJ1Y-g0*`Olu@d9;-` zbt-4bQi8z2s!CEPk6Pj-!i$IVPx5hU$zuptiFGWP|4I&8fo!1$Pw$e`=s4Pb;{7Yy z85_vDU5`WF2*y_*(|ys=`_t}VTd{;#AJubE(Yt(28lGimI_#tVbX}>a@G5vO^RcuX z&~DR%PZh+Ol_A6Uwk!rCCC9F7DzXACK3j+M#}ee>3Cr!Fubw>bYt)m#+0C#e&n0fZa@rwRI2oDI-NP| z3Xk02j5Fu%8$Ayl38?9vP`?@9nL=0hBP_Q4rsg+rDH?T~#fca;KO!_Er9NZ)ZNh3s zXyIzi+j_I=FTKH~hem(5YWc&N;*OXm8V%;-2N9M{$@qe^llZfgi-RF^&6bfP^eb75 zCPcpG>ZO*zy#FH_iY0sHzjmkCp| ze7X7#Z8T^OwE;SF`0bIl2!`h6!ju`Xb_1I*$ zxl?@X@RGgbF$WRmgThb|W_h8V2(!8{K%_x;SL37DeYy6;(`CAl#=~*@D(#8RWx67b zkI>EmtWyEm8YmX?+We8n!{ZPkAp*sJ$TX96^J`nB@rj}L7jhz=C42X#=^W{vSxXh< z1XAycsPJp0&x!JP?3zv_3wm1DO={8?ZQrR?5DFQSH}>6!u^~0Vh;c2ODX9!)abmcH z!iUP`$M?q5iUi)=>-n`WQZ{?H>W!xMy+@(tDcXAl-rbTFTk*2h)~|hCF9szfMTY4< z-|r;fq?3B{W7@K~iiw>6n}tZ$VBADi^IGAgAE zn1$I6YqF#9Y^`T~2JFg_;te`iomOHp^u7}XYhNo`b@-xkQ8lVBBz{d%pTmBgrKOPQ zvFN|z^TxO+Pxyn}$QTIL!i*)juZwGRmw@m1VV9jV5Xt)$GY}IQRNLP4Jxcz4rSk)4 zOSWR$d%gO8xkJ8m5Sd`C42g&7o*!3)2XjfC;3Y)dz^`cl#Kn2Ac5-n}n5?H}`}SB( zcC0rF-=aFC;SMS^^EjC)cJBTcJ@lH{zd-F$=rzx+&w#dpw;@GxlHh!Ym6@a)=zAhh zYT|n$M%o+%0eZba4r}C@~Av7Z!{8f3gqGPBvHbS~htDE2M*1ovE;u2z-S%Hee>wU9 diff --git a/tests/resources/sample.csv b/tests/resources/sample.csv index 370494a..2305c47 100644 --- a/tests/resources/sample.csv +++ b/tests/resources/sample.csv @@ -1,4 +1,4 @@ -modality0, modality0.source, modality1, modality1.source, s3-bucket, subject-id, platform, acq-datetime -ECEPHYS, dir/data_set_1, ,, some_bucket, 123454, ecephys, 2020-10-10 14:10:10 -BEHAVIOR_VIDEOS, dir/data_set_2, MRI, dir/data_set_3, open, 123456, BEHAVIOR, 10/13/2020 1:10:10 PM -BEHAVIOR_VIDEOS, dir/data_set_2, BEHAVIOR_VIDEOS, dir/data_set_3, scratch, 123456, BEHAVIOR, 10/13/2020 1:10:10 PM +processor_full_name, project_name, process_capsule_id, modality0, modality0.source, modality1, modality1.source, s3-bucket, subject-id, platform, acq-datetime +Anna Apple, Ephys Platform, , ECEPHYS, dir/data_set_1, ,, some_bucket, 123454, ecephys, 2020-10-10 14:10:10 +John Smith, Behavior Platform, 1f999652-00a0-4c4b-99b5-64c2985ad070, BEHAVIOR_VIDEOS, dir/data_set_2, MRI, dir/data_set_3, open, 123456, BEHAVIOR, 10/13/2020 1:10:10 PM +Anna Apple, Behavior Platform, , BEHAVIOR_VIDEOS, dir/data_set_2, BEHAVIOR_VIDEOS, dir/data_set_3, scratch, 123456, BEHAVIOR, 10/13/2020 1:10:10 PM diff --git a/tests/resources/sample.xlsx b/tests/resources/sample.xlsx index 52a0fa1b47aa07443e083ab73f98378af5bb8252..94578d741b0711f91a810eb36137c13683546ced 100644 GIT binary patch literal 5611 zcmaJ_bzD^279P60RggxyVMswh8IbPo5*T7=7&=D~Boq)(8l)QpRA5w)knZm8M(_>Z z`>s6Sd#>|4bIu=Ut=aoqYp?iJ6;V)00hpMWfXRY1UBC^&LA;x|S~)TnMnR$(mkBDPXT2C@JCrc=o9EX+7%vW z`{e_hi1Dn#vAw6QpbecfJBj#IxIFSoL@()YmYECU&kRCzHHuSax+bKSb|t zA`a!Tar0GTfXYAKeoaQrZ=KjVAnGKs1p24+~QKib0I{by&b0zzqs9lmhJoyB|u!sgWM@yGoo+5o#o9~4S#Sho~@aMX1 zfNizeo6tLQ@Lv9CT_9#X5-x*_{x%CWtmTsjW^>Tx_SPJgc6ZkFtSwV)`Ca0oCX&%W z3J|w`)d5hC%e*yC%qXWrLAh|G<6X#>8h7VDXdn%#n@pp`0HqqtuG$D`{ZVc)f`@OC z?faUi9S^J9mF78rpL(qMEIT(J#-ZcclEl(|dg@VzuVo>x)rCBwv z`PKv_Lr31u1n{)hsYm`;L)@{f?&8%Bfy`4l9nqmgW;C!6rm|03OIa@c^ zi^}D@3zNoK(^TB)V z{AQg_*9jnD(~eHLSL`XFV<#7=P`C1X7h8?1kd|+Vys9h}V?BWJ)73fd#|U}C#=Q<< z)8198$0+KM^D`Uebd=@ba`R<$h8cHfqN-5R#*~Xo?<2c;(Fa-d(urhV%kRzS{4;*= zO7LCL^F`;PpoS$kq$DmStwaIaM2H!*=aIG5KSLPJzItZd6L}alov6WmcnRTtl;g}E z&VEIM|00j^vm*Hp#IPL}Y5`+RTvHbG;AEX1G&Rn6EKksc&}{Z2R$LLNb4g zx(H0q_@$8eQm*Him<`Q`{PH_{FQI2xk&3{{7pn9o?lIebZ#zD?urO(wUnMcUQG^wI zEZ5GWo;E#e9^BYZfrdy`ZFc7yH+xGeVOFblY&)m*OmepjS>zDj+7)hj)JG*oZ}Ej} zur6O-z)jSv6SOajjjPsi;=ar6VaVfy5fk`o$mkS9=1VGor4g6SueH#Dm1!Kz^Qi(3 z@|^MEW@Q{}S~hrS5R95{tCGux)XzH&Cs5t%;?xvlRjpLd)iqTZ;x%H?9`&8QA8WW0 zQ{hL#fUSh>_ksV1hLdpi+jN!Hq%C)Yb*&@H+UVwjxcQ?b`E$~@%}nY9`K7Jygt7$n zZczl0dTX~))JM_-xxUjzby;s)x=A(w?F~?aC(VYy1zzI8 z!k6d|1fIV;Dqi0sku=5jGEx*qUmWUdGTwfd$P{lYxwN?&i2Js~mrl<(Xq+_vI~9o7 zuo{Q#JOI0YDA1WS>48zq^7}7r>BL&Ls1D?W5?z;to`DJrw_GG-QK4}2NX(1KB|_e9 z25@*}L98wwkqs2SMd&j9XcCWz0t&w*IPkcIVPxK#I;qE7Up%((Mnl3n2B(qF2)42b zh9%E0pT_tXUAngO5h6YctC5QK^|B92ZpEjw$ez4dg|_O)r?l$R=9L?-sMIg!#BHei znzNvgpi3TGj(f1QSwH*;6e4G{NNcKiK>rSq9dicKII5uqi5u>+ot%;FQ3t^_Cqx6> z)NVKX;7ZEJethMS5~?`V#kWO1w8??VEIk{saKLlOFq*)}yrpQo;dR19Sweh(=iPQr z6ZNccK)K%Ip|phKW!%O+nBd45i3djpzhUuQqH~)_kj#47(-Z?fE)jauxmwas+$ONa zM-MISOQ}y^tWu6q;!XuGt@F#>rQ>sH{)V#9d2plIB})$R0lXM9=k5HpyB&)XQz&LROAmgL$8D=G%NM z1*M>U^7h09yf(HB};u0%BecRk7DYWa6kuMst>_ z^&ezcV1wCvda=E@ah_aS*hZr}0F2DU=Z4s=J4J*xq4qGb)u(>s>^#`OZd^?E7Ze@> zmHWk!yp^FHFbfT_B{1?D$=9=V$F&Ft=!s+`SJf6dX*aS5>}Xm!UA>W3>(hp=K2bJ# zHJYR_9m;BIm0_YA!>%?+hGbx~9sL<{CAMss<|bOs7E0FywW)BqSyb5MrMcy5|M$3X zAldF=vAiHkIXgXxT>@q4+5_G_U;^(3mrTmr9`t!W@0_)-mM!XUj_I4NYSmZ+jPm;MG-RYGxLuY*P{39Eb*0oothXX%`p68+TA8PeKU}cMcw~$5vOt|lY-oS<4 zbTHyor^&R7EBbXQa9)zK_$k)Yqm%qq;p;&Ex}A4XifF9h#D5BOf?qZE-wkJs#)RuQ z4u6G*0RL^;ookjzf?#qz$h3iL4Q zNTiPEqZ0}OFG^nI#_f&@F7D->b=ha7JE!mm94TBB?eS&YeWs)^#>?JNEfu4Ee#mrz zFQWd)P+^(u4qY0?g5(kbW=tZ;s(nn91}$xK$ID(6Zr})is@=ZCC9MB7Ia9>Ah)9j} z>;rwHhbn=1RrA4jM$JqW-fS|X=}DFCb4Ki9+n|9yhsg`D^d@`s7)RQpE}4v}x)9hH z;~Se?9&#h1h}Oacn>Risa(HJ>qOM{aDk=>@?_QcICiOH1vx%kChGm42r7(WBrn_gl z4Qr=6<{1PlA5dV1?~_0^W$7G5sP5fg3#J_o^@#bvwerMCik>CDjTW<&oTBHnFH5XU zJwA0ipaTT!l47CFXIiF?i5!bP8L&<5%Il-mkI0s$1ha$?WbawVfd>+qUpmBlpC`C2 zsZty&ie8c9K^ad}x4Eh(AYHX9cNWK8_E}3VJO{rUlN6a;$BjU0tlOa+Leq^r`@e~HM z`vN{7_cwG~I(g1OFSp+}GFMq>iDJaThQ#H9Rv0jVC6LcgNganFp0GQ)Sl>W;5cxhV z(o`#9EHd5_Ch<=Fz(L$Zi_m&lp?sgRJ&{H6Dn475c-w4VeTkJ^HOMLmE%S8_;LiCK zs?HmbvOr788}?#)QN`s!1z0nOSWuezy-eg%GW;e$07S4|!3f%`#g$Apz=EVxDAQLN z3)3?n5HRLnFR;D>eBh!e>UrDg)yRf@Pq8Niu8G_iC-#tbdjc=%WnbHZ&$G&20)`QS zNgjO+&pj{V*z7vgA3ihK$1a>zEEefvIG+ttqDEIMyfO-VzY*JUhe;AMTh<0oa}|}QAM=H zVlf6+u#YefrNkF=n9x@As@v}k!2*8cgPGx)k`_cYBK0NJi-+4}e*Jpj=dbbFaM49u zW=E7Lkl)YK{5gCK#rY4oOUsUFl!%ZlV$rc4}Z*)ifI3eJMn#l$X65O(<8+@5!@HW*G5bM&ee+X9}= zU38z_y~c8YtF{^+fn`17{{OQ&$xSRR+#zmGjz8h-slZ0S88$bSk2yA>iSfL@`YBLM z^CWJ%7760iuJjb@%MiT%EaI-D@_@vpOflgY(US&lc2)o<9zF}F(`7CY)Y36)N)!5K z37w~+NIvP4+|iy{yq6*tTnutf>KQKT0?&F{Q#R+35=1qJ)W*U{ z&glI_&i2*?FBS1EXLe4fo#=cWv;A!*D6aQ4Hw*}TiCFa>;{Nl#3}l?ZZdPD76HRYt zD~Qq0Su3#+>o%qQuaw<$#N?|f>1%DxCQyXp{9e@k?w|gjFoJ0>q$YQZ-F=%Ux zLsq(|#d^N5Yq2Og8{aaDGZUi4vIU0@T1gw{$FAW~#d7VhjR&jn=v)MA(v2D@NR&zO zzNW&t#lXKnigUWA@Pz7h74hnjsk2f;ACjBAOtJLCGl{OzEc}i(>^fn?#Dqm~0)-ly z+3;|DRiN3e>}su*H^sCXpB`)k3O4d0y2kOrnc@J<5o=6MG{oo0jnmh5qtha`N zranE|gHvtnSvfoVU^Kp>7%KeqU^aNuqlv|Qt%_6T`v_X3#+ct+h?>oUu$6x+x9fOQ ze>V5Er#6GXxZ5G3ZxHR;Q%K09fM2xQO(yA@R{Kx6L9+d>c#~+kCYpW;3*xW-Kkf9p zotty}H7)Z?q!GynVduX{n%^zlY`?GP`(Gl8Xvz^5{^zv+yOo>0&2_8(OKLFxTE{<) z`|qkZ%iDGH^Gh-ikp8`Zf44%vE8k4D*A?)W3?r8DU-j^J^_$t{Iv@X%2*gtUQU5zT z|E_)W3}0uUUowC&_K)_jeDu44o1yie28s}y`5y;gRS^wA&;S6~h$|4`V6C58{{kN< BcA@|P literal 9133 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 Sb^(AV&|d&lz40l3|M!0Z0MyR_ diff --git a/tests/resources/sample_alt_modality_case.csv b/tests/resources/sample_alt_modality_case.csv index 1ce7623..c58f227 100644 --- a/tests/resources/sample_alt_modality_case.csv +++ b/tests/resources/sample_alt_modality_case.csv @@ -1,4 +1,4 @@ -modality0, modality0.source, modality1, modality1.source, s3-bucket, subject-id, platform, acq-datetime -ecephys, dir/data_set_1, ,, some_bucket, 123454, ecephys, 2020-10-10 14:10:10 -behavior-videos, dir/data_set_2, MRI, dir/data_set_3, open, 123456, BEHAVIOR, 10/13/2020 1:10:10 PM -behavior-videos, dir/data_set_2, BEHAVIOR_VIDEOS, dir/data_set_3, scratch, 123456, BEHAVIOR, 10/13/2020 1:10:10 PM +processor_full_name, project_name, process_capsule_id, modality0, modality0.source, modality1, modality1.source, s3-bucket, subject-id, platform, acq-datetime +Anna Apple, Ephys Platform, , ecephys, dir/data_set_1, ,, some_bucket, 123454, ecephys, 2020-10-10 14:10:10 +John Smith, Behavior Platform, 1f999652-00a0-4c4b-99b5-64c2985ad070, behavior-videos, dir/data_set_2, MRI, dir/data_set_3, open, 123456, BEHAVIOR, 10/13/2020 1:10:10 PM +Anna Apple, Behavior Platform, , behavior-videos, dir/data_set_2, BEHAVIOR_VIDEOS, dir/data_set_3, scratch, 123456, BEHAVIOR, 10/13/2020 1:10:10 PM diff --git a/tests/resources/sample_empty_rows.csv b/tests/resources/sample_empty_rows.csv index 0dadca7..9013962 100644 --- a/tests/resources/sample_empty_rows.csv +++ b/tests/resources/sample_empty_rows.csv @@ -1,7 +1,7 @@ -modality0, modality0.source, modality1, modality1.source, s3-bucket, subject-id, platform, acq-datetime -ECEPHYS, dir/data_set_1, ,, some_bucket, 123454, ecephys, 2020-10-10 14:10:10 -BEHAVIOR_VIDEOS, dir/data_set_2, MRI, dir/data_set_3, open, 123456, BEHAVIOR, 10/13/2020 1:10:10 PM -BEHAVIOR_VIDEOS, dir/data_set_2, BEHAVIOR_VIDEOS, dir/data_set_3, scratch, 123456, BEHAVIOR, 10/13/2020 1:10:10 PM +processor_full_name, project_name, process_capsule_id, modality0, modality0.source, modality1, modality1.source, s3-bucket, subject-id, platform, acq-datetime +Anna Apple, Ephys Platform, , ECEPHYS, dir/data_set_1, ,, some_bucket, 123454, ecephys, 2020-10-10 14:10:10 +John Smith, Behavior Platform, 1f999652-00a0-4c4b-99b5-64c2985ad070, BEHAVIOR_VIDEOS, dir/data_set_2, MRI, dir/data_set_3, open, 123456, BEHAVIOR, 10/13/2020 1:10:10 PM +Anna Apple, Behavior Platform, , BEHAVIOR_VIDEOS, dir/data_set_2, BEHAVIOR_VIDEOS, dir/data_set_3, scratch, 123456, BEHAVIOR, 10/13/2020 1:10:10 PM ,,,,,, ,,,,,, ,,,,,, diff --git a/tests/resources/sample_empty_rows.xlsx b/tests/resources/sample_empty_rows.xlsx index 8c3c02e15de6a115445ecca335f5f76e73648718..62817e5fcc2b71cad3d66caa5f5dea7f4ec1c9d8 100644 GIT binary patch literal 5607 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%0D!VmC=R3?+!82E~$Fb-eS7)8as*0V@H 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 N;t4`H*!ri|zW}8Yf;j*H literal 9113 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 F{{s@)-iZJJ diff --git a/tests/test_configs.py b/tests/test_configs.py index f11cb56..0bbc4a2 100644 --- a/tests/test_configs.py +++ b/tests/test_configs.py @@ -27,6 +27,9 @@ class TestJobConfigs(unittest.TestCase): expected_job_configs = [ BasicUploadJobConfigs( aws_param_store_name="/some/param/store", + processor_full_name="Anna Apple", + project_name="Ephys Platform", + process_capsule_id=None, s3_bucket="private", platform=Platform.ECEPHYS, modalities=[ @@ -50,6 +53,9 @@ class TestJobConfigs(unittest.TestCase): ), BasicUploadJobConfigs( aws_param_store_name="/some/param/store", + processor_full_name="John Smith", + project_name="Behavior Platform", + process_capsule_id="1f999652-00a0-4c4b-99b5-64c2985ad070", s3_bucket="open", platform=Platform.BEHAVIOR, modalities=[ @@ -80,6 +86,9 @@ class TestJobConfigs(unittest.TestCase): ), BasicUploadJobConfigs( aws_param_store_name="/some/param/store", + processor_full_name="Anna Apple", + project_name="Behavior Platform", + process_capsule_id=None, s3_bucket="scratch", platform=Platform.BEHAVIOR, modalities=[ @@ -149,6 +158,8 @@ def test_parse_csv_file(self): # not formatted correctly with self.assertRaises(Exception) as e1: BasicUploadJobConfigs( + processor_full_name="Anna Apple", + project_name="Behavior Platform", s3_bucket="", platform=Platform.BEHAVIOR, modalities=[Modality.BEHAVIOR_VIDEOS], @@ -214,6 +225,8 @@ def test_malformed_platform(self): with self.assertRaises(AttributeError) as e: BasicUploadJobConfigs( aws_param_store_name="/some/param/store", + processor_full_name="Anna Apple", + project_name="Behavior Platform", s3_bucket="some_bucket2", platform="MISSING", modalities=[ @@ -308,6 +321,9 @@ def test_from_job_and_server_configs(self): " python -m aind_data_transfer.jobs.basic_job" " --json-args ' " '{"aws_param_store_name":"/some/param/store",' + '"processor_full_name":"Anna Apple",' + '"project_name":"Ephys Platform",' + '"process_capsule_id":null,' '"s3_bucket":"private",' '"platform":{"name":"Electrophysiology platform",' '"abbreviation":"ecephys"},' diff --git a/tests/test_hpc_models.py b/tests/test_hpc_models.py index 1860d54..7f7094d 100644 --- a/tests/test_hpc_models.py +++ b/tests/test_hpc_models.py @@ -133,6 +133,8 @@ class TestHpcJobSubmitSettings(unittest.TestCase): example_config = BasicUploadJobConfigs( aws_param_store_name="/some/param/store", + processor_full_name="John Smith", + project_name="Behavior Platform", s3_bucket="some_bucket", platform=Platform.ECEPHYS, modalities=[ diff --git a/tests/test_job_upload_template.py b/tests/test_job_upload_template.py index 11c52b3..9fd6bf9 100644 --- a/tests/test_job_upload_template.py +++ b/tests/test_job_upload_template.py @@ -18,7 +18,31 @@ class TestJobUploadTemplate(unittest.TestCase): """Tests job upload template class""" - def read_xl_helper(self, source, return_validators=False): + EXAMPLE_PROJECT_NAMES = [ + "AIND Viral Genetic Tools", + "Behavior Platform", + "Brain Computer Interface", + "Cell Type LUT", + "Cognitive flexibility in patch foraging", + "Discovery-Brain Wide Circuit Dynamics", + "Discovery-Neuromodulator circuit dynamics during foraging", + "Dynamic Routing", + "Ephys Platform", + "Force Foraging", + "Information seeking in partially observable environments", + "Learning mFISH/V1omFISH", + "MSMA Platform", + "Medulla", + "Neurobiology of Action", + "OpenScope", + "Ophys Platform - FP and indicator testing", + "Ophys Platform - SLAP2", + "Single-neuron computations within brain-wide circuits (SCBC)", + "Thalamus in the middle", + ] + + @staticmethod + def _read_xl_helper(source, return_validators=False): """Helper function to read xlsx contents and validators""" lines = [] workbook = load_workbook(source, read_only=(not return_validators)) @@ -42,15 +66,28 @@ def read_xl_helper(self, source, return_validators=False): workbook.close() return result + @classmethod + def setUpClass(cls): + """Set up test class""" + expected_lines = cls._read_xl_helper(SAMPLE_JOB_TEMPLATE) + job_template = JobUploadTemplate( + project_names=cls.EXAMPLE_PROJECT_NAMES + ) + (template_lines, template_validators) = cls._read_xl_helper( + job_template.excel_sheet_filestream, True + ) + cls.job_template = job_template + cls.expected_lines = expected_lines + cls.template_lines = template_lines + cls.template_validators = template_validators + return cls + def test_create_job_template(self): """Tests that xlsx job template is created with correct contents and validators""" - expected_lines = self.read_xl_helper(SAMPLE_JOB_TEMPLATE) - (template_lines, template_validators) = self.read_xl_helper( - JobUploadTemplate.create_job_template(), True - ) - self.assertEqual(expected_lines, template_lines) - for validator in template_validators: + expected_lines = self.expected_lines + self.assertEqual(expected_lines, self.template_lines) + for validator in self.template_validators: validator["column_indexes"] = [] for r in validator["ranges"]: rb = (col, *_) = range_boundaries(r) @@ -60,7 +97,7 @@ def test_create_job_template(self): validator["column_indexes"].append(col - 1) del validator["ranges"] self.assertCountEqual( - JobUploadTemplate.VALIDATORS, template_validators + self.job_template.validators, self.template_validators ) diff --git a/tests/test_server.py b/tests/test_server.py index 6d75725..59ae086 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -8,10 +8,14 @@ from pathlib import Path, PurePosixPath from unittest.mock import MagicMock, patch +from fastapi.responses import StreamingResponse from fastapi.testclient import TestClient from pydantic import SecretStr from requests import Response +from aind_data_transfer_service.configs.job_upload_template import ( + JobUploadTemplate, +) from aind_data_transfer_service.server import app from tests.test_configs import TestJobConfigs @@ -294,7 +298,10 @@ def test_submit_hpc_jobs( { "hpc_settings": '{"qos":"production", "name": "job1"}', "upload_job_settings": ( - '{"s3_bucket": "private", ' + '{"processor_full_name":"Anna Apple", ' + '"project_name":"Ephys Platform", ' + '"process_capsule_id":null, ' + '"s3_bucket": "private", ' '"platform": {"name": "Behavior platform", ' '"abbreviation": "behavior"}, ' '"modalities": [' @@ -360,7 +367,10 @@ def test_submit_hpc_jobs_open_data( { "hpc_settings": '{"qos":"production", "name": "job1"}', "upload_job_settings": ( - '{"s3_bucket": "open", ' + '{"processor_full_name":"Anna Apple", ' + '"project_name":"Ephys Platform", ' + '"process_capsule_id":null, ' + '"s3_bucket": "open", ' '"platform": {"name": "Behavior platform", ' '"abbreviation": "behavior"}, ' '"modalities": [' @@ -534,44 +544,91 @@ def test_jobs_failure(self, mock_get: MagicMock): self.assertEqual(response.status_code, 200) self.assertIn("Submit Jobs", response.text) - @patch( - "aind_data_transfer_service.configs.job_upload_template" - ".JobUploadTemplate.create_job_template" - ) - def test_download_job_template(self, mock_create_template: MagicMock): + @patch("requests.get") + def test_download_job_template(self, mock_get: MagicMock): """Tests that job template downloads as xlsx file.""" - mock_create_template.return_value = BytesIO(b"mock_template_stream") + + mock_response = Response() + mock_response.status_code = 200 + mock_project_names = ["Ephys Platform", "Behavior Platform"] + mock_response._content = json.dumps( + {"data": mock_project_names} + ).encode("utf-8") + mock_get.return_value = mock_response + + # mock_create_template.return_value = BytesIO(b"mock_template_stream") with TestClient(app) as client: response = client.get("/api/job_upload_template") - expected_file_name_header = ( - "attachment; filename=job_upload_template.xlsx" + + expected_job_template = JobUploadTemplate( + project_names=mock_project_names ) - self.assertEqual(1, mock_create_template.call_count) - self.assertEqual(200, response.status_code) + expected_file_stream = expected_job_template.excel_sheet_filestream + expected_streaming_response = StreamingResponse( + BytesIO(expected_file_stream.getvalue()), + media_type=( + "application/" + "vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ), + headers={ + "Content-Disposition": ( + f"attachment; filename={expected_job_template.FILE_NAME}" + ) + }, + status_code=200, + ) + self.assertEqual( - expected_file_name_header, response.headers["Content-Disposition"] + expected_streaming_response.headers.items(), + list(response.headers.items()), ) + self.assertEqual(200, response.status_code) - @patch( - "aind_data_transfer_service.configs.job_upload_template" - ".JobUploadTemplate.create_job_template" - ) + @patch("requests.get") + @patch("logging.error") def test_download_invalid_job_template( - self, mock_create_template: MagicMock + self, mock_log_error: MagicMock, mock_get: MagicMock ): """Tests that download invalid job template returns errors.""" - mock_create_template.side_effect = Exception( - "mock invalid job template" - ) + mock_get.side_effect = Exception("mock invalid job template") with TestClient(app) as client: response = client.get("/api/job_upload_template") expected_response = { "message": "Error creating job template", "data": {"error": "Exception('mock invalid job template',)"}, } - self.assertEqual(1, mock_create_template.call_count) self.assertEqual(500, response.status_code) self.assertEqual(expected_response, response.json()) + mock_log_error.assert_called_once() + + @patch("requests.get") + @patch("logging.error") + def test_download_job_template_no_project_names( + self, mock_log_error: MagicMock, mock_get: MagicMock + ): + """Tests error is raised if there's an issue getting project names""" + + mock_response = Response() + mock_response.status_code = 500 + mock_project_names = [] + mock_response._content = json.dumps( + {"data": mock_project_names} + ).encode("utf-8") + mock_get.return_value = mock_response + + with TestClient(app) as client: + response = client.get("/api/job_upload_template") + + expected_response_content = { + "message": "Error creating job template", + "data": {"error": "Exception('Unable to get project names!',)"}, + } + self.assertEqual( + expected_response_content, + json.loads(response.content.decode("utf-8")), + ) + self.assertEqual(500, response.status_code) + mock_log_error.assert_called_once() if __name__ == "__main__":