From f13102f55661d32b2d7ce19abe3ae0647bdac6ac Mon Sep 17 00:00:00 2001 From: Sai Shanmukha Date: Tue, 10 Feb 2026 15:58:51 -0600 Subject: [PATCH 1/8] Add DELETE `user-bucket/objects` endpoint. Return 400 instead of 500 for invalid TES requests --- gen3workflow/aws_utils.py | 51 ++++++++++++++++++++++-------- gen3workflow/routes/ga4gh_tes.py | 8 ++++- gen3workflow/routes/storage.py | 54 ++++++++++++++++++++++++++++---- tests/test_ga4gh_tes.py | 17 ++++++++++ tests/test_misc.py | 48 ++++++++++++++++++++++++++-- 5 files changed, 156 insertions(+), 22 deletions(-) diff --git a/gen3workflow/aws_utils.py b/gen3workflow/aws_utils.py index 4ff1172..ae13b7d 100644 --- a/gen3workflow/aws_utils.py +++ b/gen3workflow/aws_utils.py @@ -1,6 +1,6 @@ import json import os -from typing import Tuple, Union +from typing import Optional, Tuple, Union from fastapi import HTTPException import boto3 @@ -409,6 +409,26 @@ def create_user_bucket(user_id: str) -> Tuple[str, str, str]: return user_bucket_name, "ga4gh-tes", config["USER_BUCKETS_REGION"], kms_key_arn +def _get_user_bucket_if_present(user_id: str) -> Optional[str]: + """ + Return the user's bucket name if it exists in S3, else None. + + Derives the bucket name from the user_id and checks its existence + using a HeadBucket call. Raises for any AWS errors other than + bucket-not-found. + """ + bucket = get_bucket_name_from_user_id(user_id) + try: + s3_client.head_bucket(Bucket=bucket) + return bucket + except ClientError as e: + code = e.response.get("Error", {}).get("Code") + if code == "404": + logger.warning(f"Bucket '{bucket}' not found for user '{user_id}'.") + return None + raise + + def get_all_bucket_objects(user_bucket_name: str) -> list: """ Get all objects from the specified S3 bucket. @@ -472,6 +492,20 @@ def delete_all_bucket_objects(user_id: str, user_bucket_name: str) -> None: raise Exception(response) +def empty_user_bucket(user_id: str) -> Optional[str]: + bucket = _get_user_bucket_if_present(user_id) + if not bucket: + return None + try: + delete_all_bucket_objects(user_id, bucket) + except Exception as e: + logger.error( + f"Failed to empty the bucket: '{bucket}' for user '{user_id}': {e}" + ) + raise + return bucket + + def delete_user_bucket(user_id: str) -> Union[str, None]: """ Deletes all objects from a user's S3 bucket before deleting the bucket itself. @@ -482,19 +516,10 @@ def delete_user_bucket(user_id: str) -> Union[str, None]: Raises: Exception: If there is an error during the deletion process. """ - user_bucket_name = get_bucket_name_from_user_id(user_id) - - try: - s3_client.head_bucket(Bucket=user_bucket_name) - except ClientError as e: - error_code = e.response["Error"]["Code"] - if error_code == "404": - logger.warning( - f"Bucket '{user_bucket_name}' not found for user '{user_id}'." - ) - return None - + user_bucket_name = _get_user_bucket_if_present(user_id) logger.info(f"Deleting bucket '{user_bucket_name}' for user '{user_id}'") + if not user_bucket_name: + return None try: delete_all_bucket_objects(user_id, user_bucket_name) s3_client.delete_bucket(Bucket=user_bucket_name) diff --git a/gen3workflow/routes/ga4gh_tes.py b/gen3workflow/routes/ga4gh_tes.py index ba98c05..2faa2fa 100644 --- a/gen3workflow/routes/ga4gh_tes.py +++ b/gen3workflow/routes/ga4gh_tes.py @@ -93,7 +93,13 @@ async def create_task(request: Request, auth=Depends(Auth)) -> dict: """ await auth.authorize("create", ["/services/workflow/gen3-workflow/tasks"]) - body = await get_request_body(request) + try: + body = await get_request_body(request) + except json.JSONDecodeError as e: + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=f"Invalid JSON in request body: {e.msg}", + ) logger.debug(f"Incoming task creation request body: {body}") # add the `_AUTHZ` tag to the task, so access can be checked by the other endpoints diff --git a/gen3workflow/routes/storage.py b/gen3workflow/routes/storage.py index 4d2dca7..63e2f91 100644 --- a/gen3workflow/routes/storage.py +++ b/gen3workflow/routes/storage.py @@ -1,5 +1,10 @@ from fastapi import APIRouter, Depends, Request, HTTPException -from starlette.status import HTTP_200_OK, HTTP_204_NO_CONTENT, HTTP_404_NOT_FOUND +from starlette.status import ( + HTTP_200_OK, + HTTP_202_ACCEPTED, + HTTP_204_NO_CONTENT, + HTTP_404_NOT_FOUND, +) from gen3workflow import aws_utils, logger from gen3workflow.auth import Auth @@ -30,13 +35,15 @@ async def get_storage_info(request: Request, auth=Depends(Auth)) -> dict: } -@router.delete("/user-bucket", status_code=HTTP_204_NO_CONTENT) -@router.delete( - "/user-bucket/", status_code=HTTP_204_NO_CONTENT, include_in_schema=False -) +@router.delete("/user-bucket", status_code=HTTP_202_ACCEPTED) +@router.delete("/user-bucket/", status_code=HTTP_202_ACCEPTED, include_in_schema=False) async def delete_user_bucket(request: Request, auth=Depends(Auth)) -> None: """ Delete the current user's S3 bucket + + Note: + Amazon S3 processes bucket deletion asynchronously. The bucket may + remain visible for a short period until deletion fully propagates. """ await auth.authorize("delete", ["/services/workflow/gen3-workflow/user-bucket"]) @@ -51,5 +58,40 @@ async def delete_user_bucket(request: Request, auth=Depends(Auth)) -> None: ) logger.info( - f"Bucket '{deleted_bucket_name}' for user '{user_id}' deleted successfully" + f"Bucket '{deleted_bucket_name}' for user '{user_id}' scheduled for deletion" + ) + + return { + "message": "Bucket deletion initiated.", + "bucket": deleted_bucket_name, + "details": ( + "Amazon S3 processes bucket deletion asynchronously. " + "The bucket may remain visible for a short period until " + "deletion fully propagates across AWS." + ), + } + + +@router.delete("/user-bucket/objects", status_code=HTTP_204_NO_CONTENT) +@router.delete( + "/user-bucket/objects/", status_code=HTTP_204_NO_CONTENT, include_in_schema=False +) +async def empty_user_bucket(request: Request, auth=Depends(Auth)) -> None: + """ + Deletes all the objects from current user's S3 bucket + """ + await auth.authorize("delete", ["/services/workflow/gen3-workflow/user-bucket"]) + + token_claims = await auth.get_token_claims() + user_id = token_claims.get("sub") + logger.info(f"User '{user_id}' emptying their storage bucket") + deleted_bucket_name = aws_utils.empty_user_bucket(user_id) + + if not deleted_bucket_name: + raise HTTPException( + HTTP_404_NOT_FOUND, "Deletion failed: No user bucket found." + ) + + logger.info( + f"All objects remvoved from bucket '{deleted_bucket_name}' for user '{user_id}'" ) diff --git a/tests/test_ga4gh_tes.py b/tests/test_ga4gh_tes.py index 758ed04..930a9b8 100644 --- a/tests/test_ga4gh_tes.py +++ b/tests/test_ga4gh_tes.py @@ -265,6 +265,23 @@ async def test_create_task_with_reserved_tags(client, access_token_patcher): } +@pytest.mark.asyncio +async def test_create_task_with_invalid_body(client, access_token_patcher): + """ + Users cannot specify the value of certain reserved tags ("_authz" "_worker_sa" etc.,) themselves + when creating a task, since these are strictly for internal use. + """ + res = await client.post( + "/ga4gh/tes/v1/tasks", + content='{"name": "test-task", "command": ["echo Hello World",""&&", "echo Goodbye!"]}', # Malformed command list + headers={ + "Authorization": f"bearer {TEST_USER_TOKEN}", + "Content-Type": "application/json", + }, + ) + assert res.status_code == 400, res.text + + @pytest.mark.asyncio @pytest.mark.parametrize( "req_body,status_code, error_message", diff --git a/tests/test_misc.py b/tests/test_misc.py index d6b6902..450ea03 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -255,7 +255,7 @@ async def test_delete_user_bucket( f"/storage/user-bucket{'/' if trailing_slash else ''}", headers={"Authorization": f"bearer {TEST_USER_TOKEN}"}, ) - assert res.status_code == 204, res.text + assert res.status_code == 202, res.text # Verify the bucket is deleted with pytest.raises(ClientError) as e: @@ -307,7 +307,7 @@ async def test_delete_user_bucket_with_files( res = await client.delete( "/storage/user-bucket", headers={"Authorization": f"bearer {TEST_USER_TOKEN}"} ) - assert res.status_code == 204, res.text + assert res.status_code == 202, res.text # Verify the bucket is deleted with pytest.raises(ClientError) as e: @@ -354,3 +354,47 @@ async def test_delete_user_bucket_unauthorized( assert res.status_code == 403, res.text assert res.json() == {"detail": "Permission denied"} mock_delete_bucket.assert_not_called() + + +@pytest.mark.asyncio +async def test_empty_user_bucket_with_files( + client, access_token_patcher, mock_aws_services +): + """ + Attempt to delete all the objects in a bucket that is not empty. + Endpoint must be able to delete all the files but should not delete the bucket. + """ + + # Create the bucket if it doesn't exist + res = await client.get( + "/storage/info", headers={"Authorization": f"bearer {TEST_USER_TOKEN}"} + ) + bucket_name = res.json()["bucket"] + + # Remove the bucket policy enforcing KMS encryption + # Moto has limitations that prevent adding objects to a bucket with KMS encryption enabled. + # More details: https://github.com/uc-cdis/gen3-workflow/blob/554fc3eb4c1d333f9ef81c1a5f8e75a6b208cdeb/tests/test_misc.py#L161-L171 + aws_utils.s3_client.delete_bucket_policy(Bucket=bucket_name) + + object_count = 10 + for i in range(object_count): + aws_utils.s3_client.put_object( + Bucket=bucket_name, Key=f"file_{i}", Body=b"Dummy file contents" + ) + + # Delete all the bucket objects + res = await client.delete( + "/storage/user-bucket/objects", + headers={"Authorization": f"bearer {TEST_USER_TOKEN}"}, + ) + assert res.status_code == 204, res.text + + # Verify the bucket still exists + bucket_exists = aws_utils.s3_client.head_bucket(Bucket=bucket_name) + assert bucket_exists, f"Bucket '{bucket_name} is expected to exist but not found" + + # Verify all the objects in the bucket are deleted + object_list = aws_utils.get_all_bucket_objects(bucket_name) + assert ( + len(object_list) == 0 + ), f"Expected bucket to have no objects, but found {len(object_list)}.\n{object_list=}" From 3dbae4717a6be5dcc6e9842cea3670b7482bc7c9 Mon Sep 17 00:00:00 2001 From: nss10 <24940419+nss10@users.noreply.github.com> Date: Tue, 10 Feb 2026 22:00:16 +0000 Subject: [PATCH 2/8] Apply automatic documentation changes --- docs/openapi.yaml | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 432dca4..8d8c47a 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -511,13 +511,35 @@ paths: - Storage /storage/user-bucket: delete: - description: Delete the current user's S3 bucket + description: 'Delete the current user''s S3 bucket + + + Note: + + Amazon S3 processes bucket deletion asynchronously. The bucket may + + remain visible for a short period until deletion fully propagates.' operationId: delete_user_bucket responses: - '204': + '202': + content: + application/json: + schema: {} description: Successful Response security: - HTTPBearer: [] summary: Delete User Bucket tags: - Storage + /storage/user-bucket/objects: + delete: + description: Deletes all the objects from current user's S3 bucket + operationId: empty_user_bucket + responses: + '204': + description: Successful Response + security: + - HTTPBearer: [] + summary: Empty User Bucket + tags: + - Storage From 29f7dd82eb84bf6e24763770e659c254e9442175 Mon Sep 17 00:00:00 2001 From: Sai Shanmukha Narumanchi Date: Fri, 6 Feb 2026 16:48:14 -0600 Subject: [PATCH 3/8] Update Dockerfile to fix permission error on poetry install (#100) --- Dockerfile | 11 +- docs/openapi.yaml | 5 + gen3workflow/auth.py | 6 +- poetry.lock | 269 ++++++++++++++++++++++--------------------- 4 files changed, 152 insertions(+), 139 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7d87a73..43cbd38 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,16 +12,21 @@ RUN chown -R gen3:gen3 /${appname} # Builder stage FROM base AS builder +USER root + # copy ONLY poetry artifact, install the dependencies but not the app; # this will make sure that the dependencies are cached COPY poetry.lock pyproject.toml /${appname}/ RUN poetry install -vv --no-root --only main --no-interaction -COPY --chown=gen3:gen3 . /${appname} - -# install the app +COPY . /${appname} RUN poetry install --without dev --no-interaction +# ensure the app dir + venv are owned by gen3 for runtime image +RUN chown -R gen3:gen3 /${appname} /venv + +USER gen3 + # Final stage FROM base diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 8d8c47a..f48dcfd 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -11,6 +11,11 @@ components: type: object ValidationError: properties: + ctx: + title: Context + type: object + input: + title: Input loc: items: anyOf: diff --git a/gen3workflow/auth.py b/gen3workflow/auth.py index 0a4c4a6..171352b 100644 --- a/gen3workflow/auth.py +++ b/gen3workflow/auth.py @@ -59,9 +59,9 @@ async def get_token_claims(self) -> dict: ) try: - token_claims = await access_token( - "user", "openid", audience="openid", purpose="access" - )(self.bearer_token) + token_claims = await access_token("user", "openid", purpose="access")( + self.bearer_token + ) except Exception as e: err_msg = "Could not verify, parse, and/or validate provided access token" logger.error( diff --git a/poetry.lock b/poetry.lock index a64e3cd..3f54427 100644 --- a/poetry.lock +++ b/poetry.lock @@ -56,14 +56,14 @@ files = [ [[package]] name = "authlib" -version = "1.6.6" +version = "1.6.7" description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd"}, - {file = "authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e"}, + {file = "authlib-1.6.7-py2.py3-none-any.whl", hash = "sha256:c637340d9a02789d2efa1d003a7437d10d3e565237bcb5fcbc6c134c7b95bab0"}, + {file = "authlib-1.6.7.tar.gz", hash = "sha256:dbf10100011d1e1b34048c9d120e83f13b35d69a826ae762b93d2fb5aafc337b"}, ] [package.dependencies] @@ -124,18 +124,18 @@ files = [ [[package]] name = "boto3" -version = "1.42.25" +version = "1.42.44" description = "The AWS SDK for Python" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "boto3-1.42.25-py3-none-any.whl", hash = "sha256:8128bde4f9d5ffce129c76d1a2efe220e3af967a2ad30bc305ba088bbc96343d"}, - {file = "boto3-1.42.25.tar.gz", hash = "sha256:ccb5e757dd62698d25766cc54cf5c47bea43287efa59c93cf1df8c8fbc26eeda"}, + {file = "boto3-1.42.44-py3-none-any.whl", hash = "sha256:32e995b0d56e19422cff22f586f698e8924c792eb00943de9c517ff4607e4e18"}, + {file = "boto3-1.42.44.tar.gz", hash = "sha256:d5601ea520d30674c1d15791a1f98b5c055e973c775e1d9952ccc09ee5913c4e"}, ] [package.dependencies] -botocore = ">=1.42.25,<1.43.0" +botocore = ">=1.42.44,<1.43.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.16.0,<0.17.0" @@ -144,14 +144,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.42.25" +version = "1.42.44" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "botocore-1.42.25-py3-none-any.whl", hash = "sha256:470261966aab1d09a1cd4ba56810098834443602846559ba9504f6613dfa52dc"}, - {file = "botocore-1.42.25.tar.gz", hash = "sha256:7ae79d1f77d3771e83e4dd46bce43166a1ba85d58a49cffe4c4a721418616054"}, + {file = "botocore-1.42.44-py3-none-any.whl", hash = "sha256:ba406b9243a20591ee87d53abdb883d46416705cebccb639a7f1c923f9dd82df"}, + {file = "botocore-1.42.44.tar.gz", hash = "sha256:47ba27360f2afd2c2721545d8909217f7be05fdee16dd8fc0b09589535a0701c"}, ] [package.dependencies] @@ -492,104 +492,104 @@ markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \" [[package]] name = "coverage" -version = "7.13.1" +version = "7.13.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147"}, - {file = "coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d"}, - {file = "coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0"}, - {file = "coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90"}, - {file = "coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d"}, - {file = "coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b"}, - {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6"}, - {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e"}, - {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae"}, - {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29"}, - {file = "coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f"}, - {file = "coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1"}, - {file = "coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88"}, - {file = "coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3"}, - {file = "coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9"}, - {file = "coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee"}, - {file = "coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf"}, - {file = "coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3"}, - {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef"}, - {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851"}, - {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb"}, - {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba"}, - {file = "coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19"}, - {file = "coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a"}, - {file = "coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c"}, - {file = "coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3"}, - {file = "coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e"}, - {file = "coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c"}, - {file = "coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62"}, - {file = "coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968"}, - {file = "coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e"}, - {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f"}, - {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee"}, - {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf"}, - {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c"}, - {file = "coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7"}, - {file = "coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6"}, - {file = "coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c"}, - {file = "coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78"}, - {file = "coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b"}, - {file = "coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd"}, - {file = "coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992"}, - {file = "coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4"}, - {file = "coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a"}, - {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766"}, - {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4"}, - {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398"}, - {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784"}, - {file = "coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461"}, - {file = "coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500"}, - {file = "coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9"}, - {file = "coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc"}, - {file = "coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a"}, - {file = "coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4"}, - {file = "coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6"}, - {file = "coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1"}, - {file = "coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd"}, - {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c"}, - {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0"}, - {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e"}, - {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53"}, - {file = "coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842"}, - {file = "coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2"}, - {file = "coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09"}, - {file = "coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894"}, - {file = "coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a"}, - {file = "coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f"}, - {file = "coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909"}, - {file = "coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4"}, - {file = "coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75"}, - {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9"}, - {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465"}, - {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864"}, - {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9"}, - {file = "coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5"}, - {file = "coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a"}, - {file = "coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0"}, - {file = "coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a"}, - {file = "coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6"}, - {file = "coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673"}, - {file = "coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5"}, - {file = "coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d"}, - {file = "coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8"}, - {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486"}, - {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564"}, - {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7"}, - {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416"}, - {file = "coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f"}, - {file = "coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79"}, - {file = "coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4"}, - {file = "coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573"}, - {file = "coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd"}, + {file = "coverage-7.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0"}, + {file = "coverage-7.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b"}, + {file = "coverage-7.13.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8"}, + {file = "coverage-7.13.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0"}, + {file = "coverage-7.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6"}, + {file = "coverage-7.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f"}, + {file = "coverage-7.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e"}, + {file = "coverage-7.13.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56"}, + {file = "coverage-7.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f"}, + {file = "coverage-7.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a"}, + {file = "coverage-7.13.3-cp310-cp310-win32.whl", hash = "sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be"}, + {file = "coverage-7.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b"}, + {file = "coverage-7.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73"}, + {file = "coverage-7.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00"}, + {file = "coverage-7.13.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2"}, + {file = "coverage-7.13.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c2be202a83dde768937a61cdc5d06bf9fb204048ca199d93479488e6247656c"}, + {file = "coverage-7.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f45e32ef383ce56e0ca099b2e02fcdf7950be4b1b56afaab27b4ad790befe5b"}, + {file = "coverage-7.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ed2e787249b922a93cd95c671cc9f4c9797a106e81b455c83a9ddb9d34590c0"}, + {file = "coverage-7.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:05dd25b21afffe545e808265897c35f32d3e4437663923e0d256d9ab5031fb14"}, + {file = "coverage-7.13.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46d29926349b5c4f1ea4fca95e8c892835515f3600995a383fa9a923b5739ea4"}, + {file = "coverage-7.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fae6a21537519c2af00245e834e5bf2884699cc7c1055738fd0f9dc37a3644ad"}, + {file = "coverage-7.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c672d4e2f0575a4ca2bf2aa0c5ced5188220ab806c1bb6d7179f70a11a017222"}, + {file = "coverage-7.13.3-cp311-cp311-win32.whl", hash = "sha256:fcda51c918c7a13ad93b5f89a58d56e3a072c9e0ba5c231b0ed81404bf2648fb"}, + {file = "coverage-7.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1a049b5c51b3b679928dd35e47c4a2235e0b6128b479a7596d0ef5b42fa6301"}, + {file = "coverage-7.13.3-cp311-cp311-win_arm64.whl", hash = "sha256:79f2670c7e772f4917895c3d89aad59e01f3dbe68a4ed2d0373b431fad1dcfba"}, + {file = "coverage-7.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595"}, + {file = "coverage-7.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6"}, + {file = "coverage-7.13.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395"}, + {file = "coverage-7.13.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8bb09e83c603f152d855f666d70a71765ca8e67332e5829e62cb9466c176af23"}, + {file = "coverage-7.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b607a40cba795cfac6d130220d25962931ce101f2f478a29822b19755377fb34"}, + {file = "coverage-7.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44f14a62f5da2e9aedf9080e01d2cda61df39197d48e323538ec037336d68da8"}, + {file = "coverage-7.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:debf29e0b157769843dff0981cc76f79e0ed04e36bb773c6cac5f6029054bd8a"}, + {file = "coverage-7.13.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:824bb95cd71604031ae9a48edb91fd6effde669522f960375668ed21b36e3ec4"}, + {file = "coverage-7.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8f1010029a5b52dc427c8e2a8dbddb2303ddd180b806687d1acd1bb1d06649e7"}, + {file = "coverage-7.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cd5dee4fd7659d8306ffa79eeaaafd91fa30a302dac3af723b9b469e549247e0"}, + {file = "coverage-7.13.3-cp312-cp312-win32.whl", hash = "sha256:f7f153d0184d45f3873b3ad3ad22694fd73aadcb8cdbc4337ab4b41ea6b4dff1"}, + {file = "coverage-7.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:03a6e5e1e50819d6d7436f5bc40c92ded7e484e400716886ac921e35c133149d"}, + {file = "coverage-7.13.3-cp312-cp312-win_arm64.whl", hash = "sha256:51c4c42c0e7d09a822b08b6cf79b3c4db8333fffde7450da946719ba0d45730f"}, + {file = "coverage-7.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25"}, + {file = "coverage-7.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a"}, + {file = "coverage-7.13.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627"}, + {file = "coverage-7.13.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8"}, + {file = "coverage-7.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1"}, + {file = "coverage-7.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b"}, + {file = "coverage-7.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc"}, + {file = "coverage-7.13.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea"}, + {file = "coverage-7.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67"}, + {file = "coverage-7.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86"}, + {file = "coverage-7.13.3-cp313-cp313-win32.whl", hash = "sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43"}, + {file = "coverage-7.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587"}, + {file = "coverage-7.13.3-cp313-cp313-win_arm64.whl", hash = "sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051"}, + {file = "coverage-7.13.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9"}, + {file = "coverage-7.13.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e"}, + {file = "coverage-7.13.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107"}, + {file = "coverage-7.13.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43"}, + {file = "coverage-7.13.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3"}, + {file = "coverage-7.13.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a"}, + {file = "coverage-7.13.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e"}, + {file = "coverage-7.13.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155"}, + {file = "coverage-7.13.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e"}, + {file = "coverage-7.13.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96"}, + {file = "coverage-7.13.3-cp313-cp313t-win32.whl", hash = "sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f"}, + {file = "coverage-7.13.3-cp313-cp313t-win_amd64.whl", hash = "sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c"}, + {file = "coverage-7.13.3-cp313-cp313t-win_arm64.whl", hash = "sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9"}, + {file = "coverage-7.13.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b"}, + {file = "coverage-7.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10"}, + {file = "coverage-7.13.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39"}, + {file = "coverage-7.13.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f"}, + {file = "coverage-7.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4"}, + {file = "coverage-7.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef"}, + {file = "coverage-7.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75"}, + {file = "coverage-7.13.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895"}, + {file = "coverage-7.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c"}, + {file = "coverage-7.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a"}, + {file = "coverage-7.13.3-cp314-cp314-win32.whl", hash = "sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4"}, + {file = "coverage-7.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0"}, + {file = "coverage-7.13.3-cp314-cp314-win_arm64.whl", hash = "sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3"}, + {file = "coverage-7.13.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8"}, + {file = "coverage-7.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca"}, + {file = "coverage-7.13.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba"}, + {file = "coverage-7.13.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f"}, + {file = "coverage-7.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508"}, + {file = "coverage-7.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba"}, + {file = "coverage-7.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd"}, + {file = "coverage-7.13.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab"}, + {file = "coverage-7.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e"}, + {file = "coverage-7.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024"}, + {file = "coverage-7.13.3-cp314-cp314t-win32.whl", hash = "sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3"}, + {file = "coverage-7.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8"}, + {file = "coverage-7.13.3-cp314-cp314t-win_arm64.whl", hash = "sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3"}, + {file = "coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910"}, + {file = "coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac"}, ] [package.extras] @@ -689,24 +689,25 @@ requirements-parser = ">=0.11.0,<1" [[package]] name = "fastapi" -version = "0.128.0" +version = "0.128.3" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d"}, - {file = "fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a"}, + {file = "fastapi-0.128.3-py3-none-any.whl", hash = "sha256:c8cdf7c2182c9a06bf9cfa3329819913c189dc86389b90d5709892053582db29"}, + {file = "fastapi-0.128.3.tar.gz", hash = "sha256:ed99383fd96063447597d5aa2a9ec3973be198e3b4fc10c55f15c62efdb21c60"}, ] [package.dependencies] annotated-doc = ">=0.0.2" pydantic = ">=2.7.0" -starlette = ">=0.40.0,<0.51.0" +starlette = ">=0.40.0,<1.0.0" typing-extensions = ">=4.8.0" +typing-inspection = ">=0.4.2" [package.extras] -all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.9.3)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=5.8.0)", "uvicorn[standard] (>=0.12.0)"] standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] @@ -927,14 +928,14 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "jmespath" -version = "1.0.1" +version = "1.1.0" description = "JSON Matching Expressions" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, - {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, + {file = "jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64"}, + {file = "jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d"}, ] [[package]] @@ -1075,14 +1076,14 @@ files = [ [[package]] name = "moto" -version = "5.1.19" +version = "5.1.20" description = "A library that allows you to easily mock out tests based on AWS infrastructure" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "moto-5.1.19-py3-none-any.whl", hash = "sha256:7adb0caacf0e2d0dbb09550bcb49a7f158ee7c460a09cb54d4599a9a94cfef70"}, - {file = "moto-5.1.19.tar.gz", hash = "sha256:a13423e402366b6affab07ed28e1df5f3fcc54ef68fc8d83dc9f824da7a4024e"}, + {file = "moto-5.1.20-py3-none-any.whl", hash = "sha256:58c82c8e6b2ef659ef3a562fa415dce14da84bc7a797943245d9a338496ea0ea"}, + {file = "moto-5.1.20.tar.gz", hash = "sha256:6d12d781e26a550d80e4b7e01d5538178e3adec6efbdec870e06e84750f13ec0"}, ] [package.dependencies] @@ -1121,14 +1122,14 @@ xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] [[package]] name = "packaging" -version = "25.0" +version = "26.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, ] [[package]] @@ -1149,30 +1150,32 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "prometheus-client" -version = "0.23.1" +version = "0.24.1" description = "Python client for the Prometheus monitoring system." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99"}, - {file = "prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce"}, + {file = "prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055"}, + {file = "prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9"}, ] [package.extras] +aiohttp = ["aiohttp"] +django = ["django"] twisted = ["twisted"] [[package]] name = "pycparser" -version = "2.23" +version = "3.0" description = "C parser in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main", "dev"] markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" files = [ - {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, - {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, ] [[package]] @@ -1348,14 +1351,14 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjwt" -version = "2.10.1" +version = "2.11.0" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, - {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, + {file = "pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469"}, + {file = "pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623"}, ] [package.dependencies] @@ -1363,9 +1366,9 @@ cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryp [package.extras] crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +dev = ["coverage[toml] (==7.10.7)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=8.4.2,<9.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] +tests = ["coverage[toml] (==7.10.7)", "pytest (>=8.4.2,<9.0.0)"] [[package]] name = "pytest" @@ -1768,14 +1771,14 @@ files = [ [[package]] name = "starlette" -version = "0.50.0" +version = "0.52.1" description = "The little ASGI library that shines." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca"}, - {file = "starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca"}, + {file = "starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74"}, + {file = "starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933"}, ] [package.dependencies] From 5c6a43e0ef47582654c4bb23ee5ddd692d507c4b Mon Sep 17 00:00:00 2001 From: Sai Shanmukha Date: Tue, 10 Feb 2026 16:41:17 -0600 Subject: [PATCH 4/8] Simplify implementation and reduce code duplication --- gen3workflow/aws_utils.py | 71 +++++++++++++--------------------- gen3workflow/routes/storage.py | 4 +- tests/test_misc.py | 4 +- 3 files changed, 31 insertions(+), 48 deletions(-) diff --git a/gen3workflow/aws_utils.py b/gen3workflow/aws_utils.py index ae13b7d..79f4418 100644 --- a/gen3workflow/aws_utils.py +++ b/gen3workflow/aws_utils.py @@ -409,26 +409,6 @@ def create_user_bucket(user_id: str) -> Tuple[str, str, str]: return user_bucket_name, "ga4gh-tes", config["USER_BUCKETS_REGION"], kms_key_arn -def _get_user_bucket_if_present(user_id: str) -> Optional[str]: - """ - Return the user's bucket name if it exists in S3, else None. - - Derives the bucket name from the user_id and checks its existence - using a HeadBucket call. Raises for any AWS errors other than - bucket-not-found. - """ - bucket = get_bucket_name_from_user_id(user_id) - try: - s3_client.head_bucket(Bucket=bucket) - return bucket - except ClientError as e: - code = e.response.get("Error", {}).get("Code") - if code == "404": - logger.warning(f"Bucket '{bucket}' not found for user '{user_id}'.") - return None - raise - - def get_all_bucket_objects(user_bucket_name: str) -> list: """ Get all objects from the specified S3 bucket. @@ -492,41 +472,44 @@ def delete_all_bucket_objects(user_id: str, user_bucket_name: str) -> None: raise Exception(response) -def empty_user_bucket(user_id: str) -> Optional[str]: - bucket = _get_user_bucket_if_present(user_id) - if not bucket: - return None - try: - delete_all_bucket_objects(user_id, bucket) - except Exception as e: - logger.error( - f"Failed to empty the bucket: '{bucket}' for user '{user_id}': {e}" - ) - raise - return bucket - - -def delete_user_bucket(user_id: str) -> Union[str, None]: +def cleanup_user_bucket(user_id: str, delete_bucket: bool = False) -> Union[str, None]: """ - Deletes all objects from a user's S3 bucket before deleting the bucket itself. + Empty a user's S3 bucket and optionally delete the bucket. Args: - user_id (str): The user's unique Gen3 ID + user_id: User identifier used to derive the bucket name. + delete_bucket: If True, delete the bucket after removing all objects. + Defaults to False (only objects are removed). + + Returns: + Bucket name if it exists and cleanup was performed, otherwise None + if the bucket does not exist. Raises: - Exception: If there is an error during the deletion process. + Exception: Propagates unexpected errors during cleanup or deletion. """ - user_bucket_name = _get_user_bucket_if_present(user_id) - logger.info(f"Deleting bucket '{user_bucket_name}' for user '{user_id}'") - if not user_bucket_name: - return None + user_bucket_name = get_bucket_name_from_user_id(user_id) + + try: + s3_client.head_bucket(Bucket=user_bucket_name) + except ClientError as e: + error_code = e.response["Error"]["Code"] + if error_code == "404": + logger.warning( + f"Bucket '{user_bucket_name}' not found for user '{user_id}'." + ) + return None try: delete_all_bucket_objects(user_id, user_bucket_name) - s3_client.delete_bucket(Bucket=user_bucket_name) + if delete_bucket: + logger.info( + f"Initializing delete for bucket '{user_bucket_name}' for user '{user_id}'" + ) + s3_client.delete_bucket(Bucket=user_bucket_name) return user_bucket_name except Exception as e: logger.error( - f"Failed to delete bucket '{user_bucket_name}' for user '{user_id}': {e}" + f"Failed to cleanup bucket '{user_bucket_name}' for user '{user_id}': {e}" ) raise diff --git a/gen3workflow/routes/storage.py b/gen3workflow/routes/storage.py index 63e2f91..0956d07 100644 --- a/gen3workflow/routes/storage.py +++ b/gen3workflow/routes/storage.py @@ -50,7 +50,7 @@ async def delete_user_bucket(request: Request, auth=Depends(Auth)) -> None: token_claims = await auth.get_token_claims() user_id = token_claims.get("sub") logger.info(f"User '{user_id}' deleting their storage bucket") - deleted_bucket_name = aws_utils.delete_user_bucket(user_id) + deleted_bucket_name = aws_utils.cleanup_user_bucket(user_id, delete_bucket=True) if not deleted_bucket_name: raise HTTPException( @@ -85,7 +85,7 @@ async def empty_user_bucket(request: Request, auth=Depends(Auth)) -> None: token_claims = await auth.get_token_claims() user_id = token_claims.get("sub") logger.info(f"User '{user_id}' emptying their storage bucket") - deleted_bucket_name = aws_utils.empty_user_bucket(user_id) + deleted_bucket_name = aws_utils.cleanup_user_bucket(user_id) if not deleted_bucket_name: raise HTTPException( diff --git a/tests/test_misc.py b/tests/test_misc.py index 450ea03..afa3e39 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -324,7 +324,7 @@ async def test_delete_user_bucket_no_token(client, mock_aws_services): """ mock_delete_bucket = MagicMock() # Delete the bucket - with patch("gen3workflow.aws_utils.delete_user_bucket", mock_delete_bucket): + with patch("gen3workflow.aws_utils.cleanup_user_bucket", mock_delete_bucket): res = await client.delete("/storage/user-bucket") assert res.status_code == 401, res.text assert res.json() == {"detail": "Must provide an access token"} @@ -346,7 +346,7 @@ async def test_delete_user_bucket_unauthorized( """ mock_delete_bucket = MagicMock() # Delete the bucket - with patch("gen3workflow.aws_utils.delete_user_bucket", mock_delete_bucket): + with patch("gen3workflow.aws_utils.cleanup_user_bucket", mock_delete_bucket): res = await client.delete( "/storage/user-bucket", headers={"Authorization": f"bearer {TEST_USER_TOKEN}"}, From a3070d99f2e55cb08bf73848e64ab767f1d0ce45 Mon Sep 17 00:00:00 2001 From: Sai Shanmukha Date: Tue, 10 Feb 2026 16:44:41 -0600 Subject: [PATCH 5/8] Improve method name --- tests/test_misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_misc.py b/tests/test_misc.py index afa3e39..af67cc0 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -357,7 +357,7 @@ async def test_delete_user_bucket_unauthorized( @pytest.mark.asyncio -async def test_empty_user_bucket_with_files( +async def test_delete_user_bucket_objects_with_existing_files( client, access_token_patcher, mock_aws_services ): """ From 8f3e15bd2c0333ca9503ee1fff3bf0e26e089d9d Mon Sep 17 00:00:00 2001 From: Sai Shanmukha Date: Tue, 10 Feb 2026 18:04:44 -0600 Subject: [PATCH 6/8] Respond to PR comments --- gen3workflow/routes/ga4gh_tes.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/gen3workflow/routes/ga4gh_tes.py b/gen3workflow/routes/ga4gh_tes.py index 2faa2fa..6b043e1 100644 --- a/gen3workflow/routes/ga4gh_tes.py +++ b/gen3workflow/routes/ga4gh_tes.py @@ -37,7 +37,14 @@ async def get_request_body(request: Request) -> dict: body = body_bytes.decode() except UnicodeDecodeError: body = str(body_bytes) # in case of binary data - return json.loads(body) + + try: + return json.loads(body) + except json.JSONDecodeError as e: + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=f"Invalid JSON in request body: {e.msg}", + ) @router.get("/service-info", status_code=HTTP_200_OK) @@ -93,13 +100,7 @@ async def create_task(request: Request, auth=Depends(Auth)) -> dict: """ await auth.authorize("create", ["/services/workflow/gen3-workflow/tasks"]) - try: - body = await get_request_body(request) - except json.JSONDecodeError as e: - raise HTTPException( - status_code=HTTP_400_BAD_REQUEST, - detail=f"Invalid JSON in request body: {e.msg}", - ) + body = await get_request_body(request) logger.debug(f"Incoming task creation request body: {body}") # add the `_AUTHZ` tag to the task, so access can be checked by the other endpoints From 322fde49e54a475b995d49e66a641fa613a9c778 Mon Sep 17 00:00:00 2001 From: Sai Shanmukha Date: Tue, 10 Feb 2026 18:12:24 -0600 Subject: [PATCH 7/8] Update test_repo branch for CI to pass --- .github/workflows/integration_tests.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/integration_tests.yaml b/.github/workflows/integration_tests.yaml index 8d15f0d..a116995 100644 --- a/.github/workflows/integration_tests.yaml +++ b/.github/workflows/integration_tests.yaml @@ -8,6 +8,7 @@ jobs: uses: uc-cdis/.github/.github/workflows/integration_tests.yaml@master with: SERVICE_TO_TEST: gen3_workflow + TEST_REPO_BRANCH: cleanup_gen3wf_aws_resources secrets: CI_TEST_ORCID_USERID: ${{ secrets.CI_TEST_ORCID_USERID }} CI_TEST_ORCID_PASSWORD: ${{ secrets.CI_TEST_ORCID_PASSWORD }} From aa492af12024d8cccb895da08fb99094db39ef4e Mon Sep 17 00:00:00 2001 From: Sai Shanmukha Date: Wed, 11 Feb 2026 11:09:22 -0600 Subject: [PATCH 8/8] Remove TEST REPO branch --- .github/workflows/integration_tests.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/integration_tests.yaml b/.github/workflows/integration_tests.yaml index a116995..8d15f0d 100644 --- a/.github/workflows/integration_tests.yaml +++ b/.github/workflows/integration_tests.yaml @@ -8,7 +8,6 @@ jobs: uses: uc-cdis/.github/.github/workflows/integration_tests.yaml@master with: SERVICE_TO_TEST: gen3_workflow - TEST_REPO_BRANCH: cleanup_gen3wf_aws_resources secrets: CI_TEST_ORCID_USERID: ${{ secrets.CI_TEST_ORCID_USERID }} CI_TEST_ORCID_PASSWORD: ${{ secrets.CI_TEST_ORCID_PASSWORD }}