Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 70 additions & 67 deletions hitapp_server/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,19 @@


@app.get("/")
async def root():
return {"message": "Welcome to the HIT App server."}
async def root():
"""Health check endpoint."""
return {"message": "Welcome to the HIT App server."}


@app.get("/version")
def test():
return {'version': '0.1'}
def test():
"""Return the service version."""
return {'version': '0.1'}


def set_base_url(url):
def set_base_url(url):
"""Extract and store the base URL used for redirections."""
global BASE_URL
if BASE_URL is not None:
return
Expand All @@ -107,14 +110,15 @@ def set_base_url(url):


@app.post("/projects")
async def create_project(response: Response,
request: Request,
background_tasks: BackgroundTasks,
html_template: UploadFile = File(...),
csv_variables: UploadFile = File(...),
project_name: str = Form(...),
num_assignment: int = Form(...),
platform: str = Form(...)):
async def create_project(response: Response,
request: Request,
background_tasks: BackgroundTasks,
html_template: UploadFile = File(...),
csv_variables: UploadFile = File(...),
project_name: str = Form(...),
num_assignment: int = Form(...),
platform: str = Form(...)):
"""Create a new project and start HIT creation in the background."""
set_base_url(request.headers['referer'])
with conn.cursor() as cursor:
response.status_code = status.HTTP_202_ACCEPTED
Expand Down Expand Up @@ -147,21 +151,19 @@ async def create_project(response: Response,
return res


def generate_hit_id(row):
def generate_hit_id(row):
"""Generate a unique identifier for a HIT."""
flatten_row = ''.join(row.values())+str(random.randint(1000, 9999))
return hashlib.md5(flatten_row.encode()).hexdigest()


def get_customized_html(hit_app_template, row):
return hit_app_template.render(row)
def get_customized_html(hit_app_template, row):
"""Render the HIT HTML template with row values."""
return hit_app_template.render(row)


async def create_hits(project_id, project_name, html_template, input_csv):
"""
Runs in the background and create the HITs from the template
:param project_id:
:return:
"""
async def create_hits(project_id, project_name, html_template, input_csv):
"""Background task that creates the HITs from the template."""
print('start background task')
with conn.cursor() as cursor:
config = {}
Expand Down Expand Up @@ -220,7 +222,8 @@ async def create_hits(project_id, project_name, html_template, input_csv):


@app.get("/projects")
def list_project(request: Request, skip: int = 0, limit: int = 20):
def list_project(request: Request, skip: int = 0, limit: int = 20):
"""Return a list of projects."""
set_base_url(request.headers['referer'])
projects = []
with conn.cursor() as cursor:
Expand All @@ -231,7 +234,8 @@ def list_project(request: Request, skip: int = 0, limit: int = 20):


@app.get("/projects/{id}")
def get_project(request: Request, id: int):
def get_project(request: Request, id: int):
"""Return details of a single project."""
set_base_url(request.headers['referer'])
with conn.cursor() as cursor:
print(id)
Expand All @@ -240,17 +244,14 @@ def get_project(request: Request, id: int):
return{'project': project}

@app.get("/projects/{id}/answers/download")
def get_project_results(request: Request, id:int, background_tasks: BackgroundTasks):
def get_project_results(request: Request, id:int, background_tasks: BackgroundTasks):
"""Start generation of the result CSV for a project."""
set_base_url(request.headers['referer'])
background_tasks.add_task(create_ans_csv, id)


async def create_ans_csv(project_id):
"""
Runs in the background and create the answer.csv to download
:param project_id:
:return:
"""
async def create_ans_csv(project_id):
"""Background task that creates the answer CSV to download."""
hits = {}
# get the HITs
with conn.cursor() as cursor:
Expand All @@ -272,24 +273,24 @@ async def create_ans_csv(project_id):
conn.commit()


async def write_dict_as_csv(dic_to_write, file_name):
"""
async with aiofiles.open(file_name, 'w', newline='', encoding="utf8") as output_file:
if len(dic_to_write)>0:
headers = list(dic_to_write[0].keys())
writer = csv.DictWriter(output_file, fieldnames=headers)
await writer.writeheader()
for d in dic_to_write:
await writer.writerow(d)
"""
async def write_dict_as_csv(dic_to_write, file_name):
"""Write a list of dictionaries to a CSV file."""
async with aiofiles.open(file_name, 'w', newline='', encoding="utf8") as output_file:
if len(dic_to_write) > 0:
headers = list(dic_to_write[0].keys())
writer = csv.DictWriter(output_file, fieldnames=headers)
await writer.writeheader()
for d in dic_to_write:
await writer.writerow(d)
df = pd.DataFrame(dic_to_write)
df = df.fillna("")
df.to_csv(file_name, index=False)


@app.get("/projects/{id}/answers/count")
def get_amt_data(request: Request, response: Response, id: int):
set_base_url(request.headers['referer'])
def get_amt_data(request: Request, response: Response, id: int):
"""Return the number of answers stored for a project."""
set_base_url(request.headers['referer'])
with conn.cursor() as cursor:
cursor.execute("""SELECT count(id) as count FROM "Answers" where "ProjectId"=%s """, (id,))
result = cursor.fetchone()
Expand All @@ -298,8 +299,9 @@ def get_amt_data(request: Request, response: Response, id: int):


@app.post("/answers/{project_id}")
async def add_answer(response: Response, info : Request, x_real_ip: str = Header(None, alias='X-Real-IP')):
req_info = await info.json()
async def add_answer(response: Response, info : Request, x_real_ip: str = Header(None, alias='X-Real-IP')):
"""Store an answer coming from the HIT application."""
req_info = await info.json()
key_data, answers = json_formater(req_info, 'Answer.')
with conn.cursor() as cursor:
v_code = generate_vcode()
Expand All @@ -316,7 +318,8 @@ async def add_answer(response: Response, info : Request, x_real_ip: str = Header
return {'vcode': v_code}


def json_formater(ajax_post, prefix=""):
def json_formater(ajax_post, prefix=""):
"""Split AJAX post data into key data and answer dictionary."""
key_prop = ["hittypeid", "hitid", "assignmentid", "workerid", "url", "campaignid", "projectid"]
key_remove = ["start_working_time","submission_time"]
key_without_prefix = ["work_duration_sec"]
Expand All @@ -334,16 +337,15 @@ def json_formater(ajax_post, prefix=""):
return key_data, data


def generate_vcode():
rand = str(random.randint(1000, 9999)) + str(random.randint(1000, 9999)) + str(random.randint(1000, 9999))
return hashlib.md5(rand.encode()).hexdigest()
def generate_vcode():
"""Generate a random verification code."""
rand = str(random.randint(1000, 9999)) + str(random.randint(1000, 9999)) + str(random.randint(1000, 9999))
return hashlib.md5(rand.encode()).hexdigest()


@app.delete("/projects/{id}")
def del_project(id: int, background_tasks: BackgroundTasks):
"""
Deletes a project
"""
def del_project(id: int, background_tasks: BackgroundTasks):
"""Delete a project and its associated files."""
with conn.cursor() as cursor:
# delete answers
#cursor.execute(""" DELETE FROM public."Answers" WHERE ProjectId= %s""", (id,))
Expand Down Expand Up @@ -372,19 +374,20 @@ def del_project(id: int, background_tasks: BackgroundTasks):
# delete project
cursor.execute(""" DELETE FROM "Projects" WHERE id= %s""", (id,))

def delete_file(filename):
if filename[:4] == "http":
def delete_file(filename):
"""Remove a file from disk."""
if filename[:4] == "http":
filename = "/".join(filename.split("/")[3:])
print(f"delete_file: {filename}")
try:
os.remove(f'./{filename}')
except Exception as e:
print(e)

"""
@app.post("/rec")
async def store_recordings(assignment_id: str = Form(...) , file: UploadFile = File(...)):
v_code = generate_vcode()
@app.post("/rec")
async def store_recordings(assignment_id: str = Form(...) , file: UploadFile = File(...)):
"""Store uploaded audio recordings."""
v_code = generate_vcode()
print(f'store_recordings: {assignment_id}, {v_code}')
out_file_path_html=Path(BASE_DIR, f"static/rec/{assignment_id}.wav")
# store html file
Expand All @@ -396,17 +399,17 @@ async def store_recordings(assignment_id: str = Form(...) , file: UploadFile = F


@app.post("/recjson")
async def store_recordings2(response: Response, info : Request):
req_info = await info.json()
async def store_recordings2(response: Response, info : Request):
"""Store recordings provided as JSON payload."""
req_info = await info.json()
print(req_info)


@app.get("/rec_exist/{assignment_id}")
def check_recording_exist(response: Response, assignment_id:str):
out_file_path_html = Path(BASE_DIR, f"static/rec/{assignment_id}.wav")
def check_recording_exist(response: Response, assignment_id:str):
"""Check if a recording exists for the given assignment."""
out_file_path_html = Path(BASE_DIR, f"static/rec/{assignment_id}.wav")
if os.path.isfile(out_file_path_html):
return {'exist': 1}
else:
return {'exist': 0}

"""
else:
return {'exist': 0}
1 change: 1 addition & 0 deletions hitapp_server/configure/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def parse_args():
return args

def main():
"""Run the configuration helper."""
args = parse_args()
with open(args.config, 'r') as f:
config = yaml.safe_load(f)
Expand Down
27 changes: 24 additions & 3 deletions src/azure_clip_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,18 @@


class AzureClipStorage:
"""Helper class to access clips stored in Azure blob containers."""

def __init__(self, config, alg):
"""Initialize the storage helper.

Parameters
----------
config : dict
Dictionary containing the Azure storage configuration.
alg : str
Name of the algorithm this storage belongs to.
"""
self._account_name = os.path.basename(
config['StorageUrl']).split('.')[0]

Expand Down Expand Up @@ -51,8 +62,9 @@ def modified_clip_names(self):
return self._modified_clip_names

async def retrieve_contents(self, list_generator, dirname=''):
"""Populate ``_clip_names`` from an Azure blob listing."""
for e in list_generator:
if not '.wav' in e.name:
if '.wav' not in e.name:
continue

if dirname:
Expand All @@ -62,6 +74,7 @@ async def retrieve_contents(self, list_generator, dirname=''):
self._clip_names.append(clip_path)

async def get_clips(self):
"""Retrieve clip names from the container."""
blobs = self.store_service.list_blobs(
name_starts_with=self.clips_path)

Expand All @@ -77,15 +90,19 @@ async def get_clips(self):
await self.retrieve_contents(blobs)

def make_clip_url(self, filename):
"""Create a full URL for a clip inside the container."""
return f"https://{self._account_name}.blob.core.windows.net/{self._container}/{filename}?{self._SAS_token}"

# todo what about dcr
class GoldSamplesInStore(AzureClipStorage):
"""Storage helper for gold standard clips."""

def __init__(self, config, alg):
super().__init__(config, alg)
self._SAS_token = ''

async def get_dataframe(self):
"""Return a DataFrame describing all gold clips."""
clips = await self.clip_names
df = pd.DataFrame(columns=['gold_clips_pvs', 'gold_clips_ans'])
clipsList = []
Expand All @@ -102,7 +119,10 @@ async def get_dataframe(self):

# todo what about dcr
class TrappingSamplesInStore(AzureClipStorage):
"""Storage helper for trapping question clips."""

async def get_dataframe(self):
"""Return a DataFrame describing all trapping clips."""
clips = await self.clip_names
df = pd.DataFrame(columns=['trapping_pvs', 'trapping_ans'])
clipsList = []
Expand All @@ -129,14 +149,15 @@ async def get_dataframe(self):
df = df.append(clipsList)
return df

"""
class PairComparisonSamplesInStore(AzureClipStorage):
"""Storage helper for pair-comparison clips."""

async def get_dataframe(self):
"""Return a DataFrame describing all pair-comparison clips."""
clips = await self.clip_names
pair_a_clips = [self.make_clip_url(clip)
for clip in clips if '40S_' in clip]
pair_b_clips = [clip.replace('40S_', '50S_') for clip in pair_a_clips]

df = pd.DataFrame({'pair_a': pair_a_clips, 'pair_b': pair_b_clips})
return df
"""
Loading