Skip to content

Commit

Permalink
Merge pull request #104 from bento-platform/feat/extended-drs-responses
Browse files Browse the repository at this point in the history
feat: extended Bento DRS responses with access data
  • Loading branch information
davidlougheed authored Apr 15, 2024
2 parents ed265e6 + 6cf5e0d commit 5927783
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 13 deletions.
42 changes: 35 additions & 7 deletions chord_drs/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
from .constants import BENTO_SERVICE_KIND, SERVICE_NAME, SERVICE_TYPE
from .data_sources import DATA_SOURCE_LOCAL, DATA_SOURCE_MINIO
from .db import db
from .models import DrsBlob, DrsBundle
from .types import DRSAccessMethodDict, DRSContentsDict, DRSObjectDict
from .models import DrsMixin, DrsBlob, DrsBundle
from .types import DRSAccessMethodDict, DRSContentsDict, DRSObjectBentoDict, DRSObjectDict
from .utils import drs_file_checksum


Expand Down Expand Up @@ -153,7 +153,22 @@ def build_contents(bundle: DrsBundle, expand: bool) -> list[DRSContentsDict]:
return content


def build_bundle_json(drs_bundle: DrsBundle, expand: bool = False) -> DRSObjectDict:
def build_bento_object_json(drs_object: DrsMixin) -> DRSObjectBentoDict:
return {
"bento": {
"project_id": drs_object.project_id,
"dataset_id": drs_object.dataset_id,
"data_type": drs_object.data_type,
"public": drs_object.public,
}
}


def build_bundle_json(
drs_bundle: DrsBundle,
expand: bool = False,
with_bento_properties: bool = False,
) -> DRSObjectDict:
return {
"contents": build_contents(drs_bundle, expand),
"checksums": [
Expand All @@ -169,10 +184,15 @@ def build_bundle_json(drs_bundle: DrsBundle, expand: bool = False) -> DRSObjectD
**({"description": drs_bundle.description} if drs_bundle.description is not None else {}),
"id": drs_bundle.id,
"self_uri": create_drs_uri(drs_bundle.id),
**(build_bento_object_json(drs_bundle) if with_bento_properties else {}),
}


def build_blob_json(drs_blob: DrsBlob, inside_container: bool = False) -> DRSObjectDict:
def build_blob_json(
drs_blob: DrsBlob,
inside_container: bool = False,
with_bento_properties: bool = False,
) -> DRSObjectDict:
data_source = current_app.config["SERVICE_DATA_SOURCE"]

blob_url: str = urllib.parse.urljoin(
Expand Down Expand Up @@ -227,6 +247,7 @@ def build_blob_json(drs_blob: DrsBlob, inside_container: bool = False) -> DRSObj
**({"description": drs_blob.description} if drs_blob.description is not None else {}),
"id": drs_blob.id,
"self_uri": create_drs_uri(drs_blob.id),
**(build_bento_object_json(drs_blob) if with_bento_properties else {}),
}


Expand Down Expand Up @@ -272,13 +293,19 @@ def get_drs_object(object_id: str) -> tuple[DrsBlob | DrsBundle | None, bool]:
def object_info(object_id: str):
drs_object, is_bundle = fetch_and_check_object_permissions(object_id, P_QUERY_DATA)

# The requester can ask for additional, non-spec-compliant Bento properties to be included in the response
with_bento_properties: bool = str_to_bool(request.args.get("with_bento_properties", ""))

if is_bundle:
expand: bool = str_to_bool(request.args.get("expand", ""))
return jsonify(build_bundle_json(drs_object, expand=expand))
return jsonify(build_bundle_json(drs_object, expand=expand, with_bento_properties=with_bento_properties))

# The requester can specify object internal path to be added to the response
use_internal_path: bool = str_to_bool(request.args.get("internal_path", ""))
return jsonify(build_blob_json(drs_object, inside_container=use_internal_path))

return jsonify(
build_blob_json(drs_object, inside_container=use_internal_path, with_bento_properties=with_bento_properties)
)


@drs_service.route("/objects/<string:object_id>/access/<string:access_id>", methods=["GET"])
Expand All @@ -304,6 +331,7 @@ def object_search():
fuzzy_name: str | None = request.args.get("fuzzy_name")
search_q: str | None = request.args.get("q")
internal_path: bool = str_to_bool(request.args.get("internal_path", ""))
with_bento_properties: bool = str_to_bool(request.args.get("with_bento_properties", ""))

if name:
objects = DrsBlob.query.filter_by(name=name).all()
Expand All @@ -325,7 +353,7 @@ def object_search():
# TODO: map objects to resources to avoid duplicate calls to same resource in check_objects_permission
for obj, p in zip(objects, check_objects_permission(list(objects), P_QUERY_DATA)):
if p: # Only include the blob in the search results if we have permissions to view it.
response.append(build_blob_json(obj, internal_path))
response.append(build_blob_json(obj, internal_path, with_bento_properties=with_bento_properties))

authz_middleware.mark_authz_done(request)
return jsonify(response)
Expand Down
9 changes: 9 additions & 0 deletions chord_drs/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"DRSAccessMethodDict",
"DRSChecksumDict",
"DRSContentsDict",
"DRSObjectBentoDict",
"DRSObjectDict",
]

Expand Down Expand Up @@ -52,6 +53,13 @@ class DRSContentsDict(_DRSContentsDictBase, total=False):
contents: list["DRSContentsDict"]


class DRSObjectBentoDict(TypedDict):
project_id: str | None
dataset_id: str | None
data_type: str | None
public: bool


class DRSObjectDict(_DRSObjectDictBase, total=False):
access_methods: list[DRSAccessMethodDict]
name: str
Expand All @@ -61,3 +69,4 @@ class DRSObjectDict(_DRSObjectDictBase, total=False):
mime_type: str
contents: list[DRSContentsDict]
aliases: list[str]
bento: DRSObjectBentoDict
32 changes: 26 additions & 6 deletions tests/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
NON_EXISTENT_ID = "123"


def validate_object_fields(data, existing_id=None, with_internal_path=False):
def validate_object_fields(
data,
existing_id: bool = None,
with_internal_path: bool = False,
with_bento_properties: bool = False,
):
is_local = current_app.config["SERVICE_DATA_SOURCE"] == DATA_SOURCE_LOCAL
is_minio = current_app.config["SERVICE_DATA_SOURCE"] == DATA_SOURCE_MINIO

Expand All @@ -36,6 +41,16 @@ def validate_object_fields(data, existing_id=None, with_internal_path=False):
if existing_id:
assert "id" in data and data["id"] == existing_id

if with_bento_properties:
assert "bento" in data
bento_data = data["bento"]
assert "project_id" in bento_data
assert "dataset_id" in bento_data
assert "data_type" in bento_data
assert "public" in bento_data
else:
assert "bento" not in data


def test_service_info(client):
from chord_drs.app import application
Expand Down Expand Up @@ -100,10 +115,15 @@ def test_object_access_fail(client):
def _test_object_and_download(client, obj, test_range=False):
res = client.get(f"/objects/{obj.id}")
data = res.get_json()

assert res.status_code == 200
validate_object_fields(data, existing_id=obj.id)

# Check that we can get extra Bento data
res = client.get(f"/objects/{obj.id}?with_bento_properties=true")
data = res.get_json()
assert res.status_code == 200
validate_object_fields(data, existing_id=obj.id, with_bento_properties=True)

# Check that we don't have access via an access ID (since we don't generate them)
res = client.get(f"/objects/{obj.id}/access/no_access")
assert res.status_code == 404
Expand Down Expand Up @@ -178,8 +198,8 @@ def test_object_and_download_minio(client_minio, drs_object_minio):

@responses.activate
def test_object_and_download_minio_specific_perms(client_minio, drs_object_minio):
# _test_object_and_download does 3 different accesses
authz_drs_specific_obj(iters=4)
# _test_object_and_download does 5 different accesses
authz_drs_specific_obj(iters=5)
_test_object_and_download(client_minio, drs_object_minio)


Expand All @@ -191,8 +211,8 @@ def test_object_and_download(client, drs_object):

@responses.activate
def test_object_and_download_specific_perms(client, drs_object):
# _test_object_and_download does 3 different accesses
authz_drs_specific_obj(iters=4)
# _test_object_and_download does 5 different accesses
authz_drs_specific_obj(iters=5)
_test_object_and_download(client, drs_object)


Expand Down

0 comments on commit 5927783

Please sign in to comment.