Skip to content

Commit 9025ef2

Browse files
authored
Improved metadata in artifact download file names (#52)
* Improved metadata in artifact download file names * Added default for pytest * Added pytest-env
1 parent ce57850 commit 9025ef2

File tree

4 files changed

+48
-17
lines changed

4 files changed

+48
-17
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ dev = [
2727
"types-requests",
2828
"types-Authlib",
2929
]
30-
test = ["pytest"]
30+
test = ["pytest", "pytest-env"]
3131

3232
[tool.setuptools]
3333
package-dir = { "" = "src" }
@@ -55,6 +55,7 @@ line-length = 120
5555
[tool.pytest.ini_options]
5656
pythonpath = ["src/"]
5757
testpaths = "tests"
58+
env = ["D:CACTUS_ORCHESTRATOR_BASEURL=http://localhost/"]
5859

5960
[tool.isort]
6061
profile = "black"

src/cactus_ui/orchestrator.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import re
23
from dataclasses import dataclass
34
from datetime import datetime
45
from enum import IntEnum, auto
@@ -22,6 +23,13 @@
2223
CACTUS_ORCHESTRATOR_REQUEST_TIMEOUT_SPAWN = int(env.get("CACTUS_ORCHESTRATOR_REQUEST_TIMEOUT_SPAWN", "120"))
2324

2425

26+
HEADER_USER_NAME = "CACTUS-User-Name"
27+
HEADER_TEST_ID = "CACTUS-Test-Id"
28+
HEADER_RUN_ID = "CACTUS-Run-Id"
29+
HEADER_GROUP_ID = "CACTUS-Group-Id"
30+
HEADER_GROUP_NAME = "CACTUS-Group-Name"
31+
32+
2533
@dataclass
2634
class RunResponse:
2735
"""Ideally this would be defined in a shared cactus-schema but that doesn't exist. Instead, ensure this remains
@@ -191,6 +199,18 @@ def generate_uri(path: str) -> str:
191199
return CACTUS_ORCHESTRATOR_BASEURL.rstrip("/") + "/" + path
192200

193201

202+
def file_name_safe(v: str) -> str:
203+
return re.sub(r"[^A-Za-z0-9_\-]", "_", v)
204+
205+
206+
def generate_run_artifact_file_name(response: requests.Response, run_id: str) -> str:
207+
raw_run_id = response.headers.get(HEADER_RUN_ID, run_id)
208+
user = response.headers.get(HEADER_USER_NAME, "")
209+
test_id = response.headers.get(HEADER_TEST_ID, "")
210+
group_name = response.headers.get(HEADER_GROUP_NAME, "")
211+
return file_name_safe(f"{raw_run_id}_{test_id}_{user}_{group_name}_artifacts") + ".zip"
212+
213+
194214
def fetch_procedures(access_token: str, page: int) -> Pagination[ProcedureResponse] | None:
195215
"""Fetch the list of test procedures for the dropdown"""
196216
uri = generate_uri(f"/procedure?page={page}")
@@ -391,14 +411,14 @@ def finalise_run(access_token: str, run_id: str) -> bytes | None:
391411
return response.content
392412

393413

394-
def fetch_run_artifact(access_token: str, run_id: str) -> bytes | None:
395-
"""Given an already started run - finalise it and return the resulting ZIP file bytes"""
414+
def fetch_run_artifact(access_token: str, run_id: str) -> tuple[bytes | None, str]:
415+
"""Given an already started run - finalise it and return the resulting ZIP file bytes / file name"""
396416
uri = generate_uri(f"/run/{run_id}/artifact")
397417
response = safe_request("GET", uri, generate_headers(access_token), CACTUS_ORCHESTRATOR_REQUEST_TIMEOUT_DEFAULT)
398418
if response is None or not is_success_response(response):
399-
return None
419+
return (None, "")
400420

401-
return response.content
421+
return (response.content, generate_run_artifact_file_name(response, run_id))
402422

403423

404424
def fetch_runs_for_group(
@@ -750,14 +770,14 @@ def admin_fetch_group_procedure_run_summaries(
750770
]
751771

752772

753-
def admin_fetch_run_artifact(access_token: str, run_id: str) -> bytes | None:
754-
"""Given an already started run - finalise it and return the resulting ZIP file bytes"""
773+
def admin_fetch_run_artifact(access_token: str, run_id: str) -> tuple[bytes | None, str]:
774+
"""Given an already started run - finalise it and return the resulting ZIP file bytes and ZIP file name"""
755775
uri = generate_uri(f"/admin/run/{run_id}/artifact")
756776
response = safe_request("GET", uri, generate_headers(access_token), CACTUS_ORCHESTRATOR_REQUEST_TIMEOUT_DEFAULT)
757777
if response is None or not is_success_response(response):
758-
return None
778+
return (None, "")
759779

760-
return response.content
780+
return (response.content, generate_run_artifact_file_name(response, run_id))
761781

762782

763783
def admin_fetch_run_group_artifact(access_token: str, run_group_id: int) -> bytes | None:

src/cactus_ui/server.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -328,14 +328,14 @@ def admin_group_runs_page(access_token: str, run_group_id: int) -> str | Respons
328328
if not run_id:
329329
error = "No run ID specified."
330330
else:
331-
artifact_data = orchestrator.admin_fetch_run_artifact(access_token, run_id)
331+
artifact_data, download_name = orchestrator.admin_fetch_run_artifact(access_token, run_id)
332332
if artifact_data is None:
333333
error = "Failed to retrieve artifacts."
334334
else:
335335
return send_file(
336336
io.BytesIO(artifact_data),
337337
as_attachment=True,
338-
download_name=f"{run_id}_artifacts.zip",
338+
download_name=download_name,
339339
mimetype="application/zip",
340340
)
341341

@@ -439,14 +439,14 @@ def admin_run_status_page(access_token: str, run_id: str) -> str | Response:
439439
if request.method == "POST":
440440
# Handle downloading a prior run's artifacts
441441
if request.form.get("action") == "artifact":
442-
artifact_data = orchestrator.admin_fetch_run_artifact(access_token, run_id)
442+
artifact_data, download_name = orchestrator.admin_fetch_run_artifact(access_token, run_id)
443443
if artifact_data is None:
444444
error = "Failed to retrieve artifacts."
445445
else:
446446
return send_file(
447447
io.BytesIO(artifact_data),
448448
as_attachment=True,
449-
download_name=f"{run_id}_artifacts.zip",
449+
download_name=download_name,
450450
mimetype="application/zip",
451451
)
452452

@@ -814,14 +814,14 @@ def group_runs_page(access_token: str, run_group_id: int) -> str | Response: #
814814
if not run_id:
815815
error = "No run ID specified."
816816
else:
817-
artifact_data = orchestrator.fetch_run_artifact(access_token, run_id)
817+
artifact_data, download_name = orchestrator.fetch_run_artifact(access_token, run_id)
818818
if artifact_data is None:
819819
error = "Failed to retrieve artifacts."
820820
else:
821821
return send_file(
822822
io.BytesIO(artifact_data),
823823
as_attachment=True,
824-
download_name=f"{run_id}_artifacts.zip",
824+
download_name=download_name,
825825
mimetype="application/zip",
826826
)
827827
# Handle deleting a prior run
@@ -919,14 +919,14 @@ def run_status_page(access_token: str, run_id: str) -> str | Response:
919919

920920
# Handle downloading a prior run's artifacts
921921
elif request.form.get("action") == "artifact":
922-
artifact_data = orchestrator.fetch_run_artifact(access_token, run_id)
922+
artifact_data, download_name = orchestrator.fetch_run_artifact(access_token, run_id)
923923
if artifact_data is None:
924924
error = "Failed to retrieve artifacts."
925925
else:
926926
return send_file(
927927
io.BytesIO(artifact_data),
928928
as_attachment=True,
929-
download_name=f"{run_id}_artifacts.zip",
929+
download_name=download_name,
930930
mimetype="application/zip",
931931
)
932932

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import pytest
2+
3+
from cactus_ui.orchestrator import file_name_safe
4+
5+
6+
@pytest.mark.parametrize(
7+
"input, expected", [("", ""), ("hello-VALID_123", "hello-VALID_123"), ("abc 123@DEF./com", "abc_123_DEF__com")]
8+
)
9+
def test_file_name_safe(input: str, expected: str):
10+
assert file_name_safe(input) == expected

0 commit comments

Comments
 (0)